mirror of https://github.com/grafana/grafana
Create annotations (#8197)
* annotations: add 25px space for events section * annotations: restored create annotation action * annotations: able to use fa icons as event markers * annotations: initial emoji support from twemoji lib * annotations: adjust fa icon position * annotations: initial emoji picker * annotation: include user info into annotation requests * annotation: add icon info * annotation: display user info in tooltip * annotation: fix region saving * annotation: initial region markers * annotation: fix region clearing (add flot-temp-elem class) * annotation: adjust styles a bit * annotations: minor fixes * annoations: removed userId look in loop, need a sql join or a user cache for this * annotation: fix invisible events * lib: changed twitter emoij lib to be npm dependency * annotation: add icon picker to Add Annotation dialog * annotation: save icon to annotation table * annotation: able to set custom icon for annotation added by user * annotations: fix emoji after library upgrade (switch to 72px) * emoji: temporary remove bad code points * annotations: improve icon picker * annotations: icon show icon picker at the top * annotations: use svg for emoji * annotations: fix region drawing when add annotation editor opened * annotations: use flot lib for drawing region fill * annotations: move regions building into event_manager * annotations: don't draw additional space if no events are got * annotations: deduplicate events * annotations: properly render cut regions * annotations: fix cut region building * annotations: refactor * annotations: adjust event section size * add-annotations: fix undefined default icon * create-annotations: edit event (frontend part) * fixed bug causes error when hover event marker * create-annotations: update event (backend) * ignore grafana-server debug binary in git (created VS Code) * create-annotations: use PUT request for updating annotation. * create-annotations: fixed time format when editing existing event * create-annotations: support for region update * create-annotations: fix bug with limit and event type * create-annotations: delete annotation * create-annotations: show only selected icon in edit mode * create-annotations: show event editor only for users with at least Editor role * create-annotations: handle double-sized emoji codepoints * create-annotations: refactor use CP_SEPARATOR from emojiDef * create-annotations: update emoji list, add categories. * create-annotations: copy SVG emoji into public/vendor/npm and use it as a base path * create-annotations: initial tabs for emoji picker * emoji-picker: adjust styles * emoji-picker: minor refactor * emoji-picker: refactor - rename and move into one directory * emoji-picker: build emoji elements on app load, not on picker open * emoji-picker: fix emoji searching * emoji-picker: refactor * emoji-picker: capitalize category name * emoji-picker: refactor move buildEmojiElem() into emoji_converter.ts for future reuse. * jquery.flot.events: refactor use buildEmojiElem() for making emojis, remove unused code for font awesome based icons. * emoji_converter: handle converting error * tech: updated * merged with master * shore: clean up some stuff * annotation: wip tags * annotation: filtering by tags * tags: parse out spaces etc. from a tags string * annotations: use tagsinput component for tag filtering * annotation: wip work on how we query alert & panel annotations * annotations: support for updating tags in an annotation * linting * annotations: work on unifying how alert history annotations and manual panel annotations are created * tslint: fixes * tags: create tag on blur as well Currently, the tags directive only creates the tag when the user presses enter. This change means the tag is created on blur as well (when the user clicks outside the input field). * annotations: fix update after refactoring * annotations: progress on how alert annotations are fetched * annotations: minor progress * annotations: progress * annotation: minor progress * annotations: move tag parsing from tooltip to ds Instead of parsing a tag string into an array in the annotation_tooltip class, this moves the parsing to the datasources. InfluxDB ds already does that parsing. Graphite now has it. * annotations: more work on querying * annotations: change from tags as string to array when saving in the db and in the api. * annotations: delete tag link if removed on edit * annotation: more work on depricating annotation title * annotations: delete tag links on delete * annotations: fix for find * annotation: added user to annotation tooltip and added alertName to annoation dto * annotations: use id from route instead from cmd for updating * annotations: http api docs * create annotation: last edits * annotations: minor fix for querying annotations before dashboard saved * annotations: fix for popover placement when legend is on the side (and doubel render pass is causing original marker to be removed) * annotations: changing how the built in query gets added * annotation: added time to header in edit mode * tests: fixed jshint built issuepull/9460/head
parent
43903d71ec
commit
25aa9df270
@ -0,0 +1,189 @@ |
||||
+++ |
||||
title = "Annotations HTTP API " |
||||
description = "Grafana Annotations HTTP API" |
||||
keywords = ["grafana", "http", "documentation", "api", "annotation", "annotations", "comment"] |
||||
aliases = ["/http_api/annotations/"] |
||||
type = "docs" |
||||
[menu.docs] |
||||
name = "Annotations" |
||||
identifier = "annotationshttp" |
||||
parent = "http_api" |
||||
+++ |
||||
|
||||
# Annotations resources / actions |
||||
|
||||
This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be global annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel. |
||||
|
||||
## Find Annotations |
||||
|
||||
`GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100` |
||||
|
||||
**Example Request**: |
||||
|
||||
```http |
||||
GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100 HTTP/1.1 |
||||
Accept: application/json |
||||
Content-Type: application/json |
||||
Authorization: Basic YWRtaW46YWRtaW4= |
||||
``` |
||||
|
||||
|
||||
Query Parameters: |
||||
|
||||
- `from`: epoch datetime in milliseconds. Optional. |
||||
- `to`: epoch datetime in milliseconds. Optional. |
||||
- `limit`: number. Optional - default is 10. Max limit for results returned. |
||||
- `alertId`: number. Optional. Find annotations for a specified alert. |
||||
- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard |
||||
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel |
||||
- `tags`: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`. |
||||
|
||||
**Example Response**: |
||||
|
||||
```http |
||||
HTTP/1.1 200 |
||||
Content-Type: application/json |
||||
[ |
||||
{ |
||||
"id": 1124, |
||||
"alertId": 0, |
||||
"dashboardId": 468, |
||||
"panelId": 2, |
||||
"userId": 1, |
||||
"userName": "", |
||||
"newState": "", |
||||
"prevState": "", |
||||
"time": 1507266395000, |
||||
"text": "test", |
||||
"metric": "", |
||||
"regionId": 1123, |
||||
"type": "event", |
||||
"tags": [ |
||||
"tag1", |
||||
"tag2" |
||||
], |
||||
"data": {} |
||||
}, |
||||
{ |
||||
"id": 1123, |
||||
"alertId": 0, |
||||
"dashboardId": 468, |
||||
"panelId": 2, |
||||
"userId": 1, |
||||
"userName": "", |
||||
"newState": "", |
||||
"prevState": "", |
||||
"time": 1507265111000, |
||||
"text": "test", |
||||
"metric": "", |
||||
"regionId": 1123, |
||||
"type": "event", |
||||
"tags": [ |
||||
"tag1", |
||||
"tag2" |
||||
], |
||||
"data": {} |
||||
} |
||||
] |
||||
``` |
||||
|
||||
## Create Annotation |
||||
|
||||
Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional. If they are not specified then a global annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. |
||||
|
||||
`POST /api/annotations` |
||||
|
||||
**Example Request**: |
||||
|
||||
```json |
||||
POST /api/annotations HTTP/1.1 |
||||
Accept: application/json |
||||
Content-Type: application/json |
||||
|
||||
{ |
||||
"dashboardId":468, |
||||
"panelId":1, |
||||
"time":1507037197339, |
||||
"isRegion":true, |
||||
"timeEnd":1507180805056, |
||||
"tags":["tag1","tag2"], |
||||
"text":"Annotation Description" |
||||
} |
||||
``` |
||||
|
||||
**Example Response**: |
||||
|
||||
```json |
||||
HTTP/1.1 200 |
||||
Content-Type: application/json |
||||
|
||||
{"message":"Annotation added"} |
||||
``` |
||||
|
||||
## Update Annotation |
||||
|
||||
`PUT /api/annotations/:id` |
||||
|
||||
**Example Request**: |
||||
|
||||
```json |
||||
PUT /api/annotations/1141 HTTP/1.1 |
||||
Accept: application/json |
||||
Content-Type: application/json |
||||
|
||||
{ |
||||
"time":1507037197339, |
||||
"isRegion":true, |
||||
"timeEnd":1507180805056, |
||||
"text":"Annotation Description", |
||||
"tags":["tag3","tag4","tag5"] |
||||
} |
||||
``` |
||||
|
||||
## Delete Annotation By Id |
||||
|
||||
`DELETE /api/annotation/:id` |
||||
|
||||
Deletes the annotation that matches the specified id. |
||||
|
||||
**Example Request**: |
||||
|
||||
```http |
||||
DELETE /api/annotation/1 HTTP/1.1 |
||||
Accept: application/json |
||||
Content-Type: application/json |
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk |
||||
``` |
||||
|
||||
**Example Response**: |
||||
|
||||
```http |
||||
HTTP/1.1 200 |
||||
Content-Type: application/json |
||||
|
||||
{"message":"Annotation deleted"} |
||||
``` |
||||
|
||||
## Delete Annotation By RegionId |
||||
|
||||
`DELETE /api/annotation/region/:id` |
||||
|
||||
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id. |
||||
|
||||
**Example Request**: |
||||
|
||||
```http |
||||
DELETE /api/annotation/region/1 HTTP/1.1 |
||||
Accept: application/json |
||||
Content-Type: application/json |
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk |
||||
``` |
||||
|
||||
**Example Response**: |
||||
|
||||
```http |
||||
HTTP/1.1 200 |
||||
Content-Type: application/json |
||||
|
||||
{"message":"Annotation region deleted"} |
||||
``` |
@ -0,0 +1,60 @@ |
||||
package models |
||||
|
||||
import ( |
||||
"strings" |
||||
) |
||||
|
||||
type Tag struct { |
||||
Id int64 |
||||
Key string |
||||
Value string |
||||
} |
||||
|
||||
func ParseTagPairs(tagPairs []string) (tags []*Tag) { |
||||
if tagPairs == nil { |
||||
return []*Tag{} |
||||
} |
||||
|
||||
for _, tagPair := range tagPairs { |
||||
var tag Tag |
||||
|
||||
if strings.Contains(tagPair, ":") { |
||||
keyValue := strings.Split(tagPair, ":") |
||||
tag.Key = strings.Trim(keyValue[0], " ") |
||||
tag.Value = strings.Trim(keyValue[1], " ") |
||||
} else { |
||||
tag.Key = strings.Trim(tagPair, " ") |
||||
} |
||||
|
||||
if tag.Key == "" || ContainsTag(tags, &tag) { |
||||
continue |
||||
} |
||||
|
||||
tags = append(tags, &tag) |
||||
} |
||||
|
||||
return tags |
||||
} |
||||
|
||||
func ContainsTag(existingTags []*Tag, tag *Tag) bool { |
||||
for _, t := range existingTags { |
||||
if t.Key == tag.Key && t.Value == tag.Value { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func JoinTagPairs(tags []*Tag) []string { |
||||
tagPairs := []string{} |
||||
|
||||
for _, tag := range tags { |
||||
if tag.Value != "" { |
||||
tagPairs = append(tagPairs, tag.Key+":"+tag.Value) |
||||
} else { |
||||
tagPairs = append(tagPairs, tag.Key) |
||||
} |
||||
} |
||||
|
||||
return tagPairs |
||||
} |
@ -0,0 +1,95 @@ |
||||
package models |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
func TestParsingTags(t *testing.T) { |
||||
Convey("Testing parsing a tag pairs into tags", t, func() { |
||||
Convey("Can parse one empty tag", func() { |
||||
tags := ParseTagPairs([]string{""}) |
||||
So(len(tags), ShouldEqual, 0) |
||||
}) |
||||
|
||||
Convey("Can parse valid tags", func() { |
||||
tags := ParseTagPairs([]string{"outage", "type:outage", "error"}) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0].Key, ShouldEqual, "outage") |
||||
So(tags[0].Value, ShouldEqual, "") |
||||
So(tags[1].Key, ShouldEqual, "type") |
||||
So(tags[1].Value, ShouldEqual, "outage") |
||||
So(tags[2].Key, ShouldEqual, "error") |
||||
So(tags[2].Value, ShouldEqual, "") |
||||
}) |
||||
|
||||
Convey("Can parse tags with spaces", func() { |
||||
tags := ParseTagPairs([]string{" outage ", " type : outage ", "error "}) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0].Key, ShouldEqual, "outage") |
||||
So(tags[0].Value, ShouldEqual, "") |
||||
So(tags[1].Key, ShouldEqual, "type") |
||||
So(tags[1].Value, ShouldEqual, "outage") |
||||
So(tags[2].Key, ShouldEqual, "error") |
||||
So(tags[2].Value, ShouldEqual, "") |
||||
}) |
||||
|
||||
Convey("Can parse empty tags", func() { |
||||
tags := ParseTagPairs([]string{" outage ", "", "", ":", "type : outage ", "error ", "", ""}) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0].Key, ShouldEqual, "outage") |
||||
So(tags[0].Value, ShouldEqual, "") |
||||
So(tags[1].Key, ShouldEqual, "type") |
||||
So(tags[1].Value, ShouldEqual, "outage") |
||||
So(tags[2].Key, ShouldEqual, "error") |
||||
So(tags[2].Value, ShouldEqual, "") |
||||
}) |
||||
|
||||
Convey("Can parse tags with extra colons", func() { |
||||
tags := ParseTagPairs([]string{" outage", "type : outage:outage2 :outage3 ", "error :"}) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0].Key, ShouldEqual, "outage") |
||||
So(tags[0].Value, ShouldEqual, "") |
||||
So(tags[1].Key, ShouldEqual, "type") |
||||
So(tags[1].Value, ShouldEqual, "outage") |
||||
So(tags[2].Key, ShouldEqual, "error") |
||||
So(tags[2].Value, ShouldEqual, "") |
||||
}) |
||||
|
||||
Convey("Can parse tags that contains key and values with spaces", func() { |
||||
tags := ParseTagPairs([]string{" outage 1", "type 1: outage 1 ", "has error "}) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0].Key, ShouldEqual, "outage 1") |
||||
So(tags[0].Value, ShouldEqual, "") |
||||
So(tags[1].Key, ShouldEqual, "type 1") |
||||
So(tags[1].Value, ShouldEqual, "outage 1") |
||||
So(tags[2].Key, ShouldEqual, "has error") |
||||
So(tags[2].Value, ShouldEqual, "") |
||||
}) |
||||
|
||||
Convey("Can filter out duplicate tags", func() { |
||||
tags := ParseTagPairs([]string{"test", "test", "key:val1", "key:val2"}) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0].Key, ShouldEqual, "test") |
||||
So(tags[0].Value, ShouldEqual, "") |
||||
So(tags[1].Key, ShouldEqual, "key") |
||||
So(tags[1].Value, ShouldEqual, "val1") |
||||
So(tags[2].Key, ShouldEqual, "key") |
||||
So(tags[2].Value, ShouldEqual, "val2") |
||||
}) |
||||
|
||||
Convey("Can join tag pairs", func() { |
||||
tagPairs := []*Tag{ |
||||
{Key: "key1", Value: "val1"}, |
||||
{Key: "key2", Value: ""}, |
||||
{Key: "key3"}, |
||||
} |
||||
tags := JoinTagPairs(tagPairs) |
||||
So(len(tags), ShouldEqual, 3) |
||||
So(tags[0], ShouldEqual, "key1:val1") |
||||
So(tags[1], ShouldEqual, "key2") |
||||
So(tags[2], ShouldEqual, "key3") |
||||
}) |
||||
}) |
||||
} |
@ -0,0 +1,208 @@ |
||||
package sqlstore |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
. "github.com/smartystreets/goconvey/convey" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/annotations" |
||||
) |
||||
|
||||
func TestSavingTags(t *testing.T) { |
||||
Convey("Testing annotation saving/loading", t, func() { |
||||
InitTestDB(t) |
||||
|
||||
repo := SqlAnnotationRepo{} |
||||
|
||||
Convey("Can save tags", func() { |
||||
tagPairs := []*models.Tag{ |
||||
{Key: "outage"}, |
||||
{Key: "type", Value: "outage"}, |
||||
{Key: "server", Value: "server-1"}, |
||||
{Key: "error"}, |
||||
} |
||||
tags, err := repo.ensureTagsExist(newSession(), tagPairs) |
||||
|
||||
So(err, ShouldBeNil) |
||||
So(len(tags), ShouldEqual, 4) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func TestAnnotations(t *testing.T) { |
||||
Convey("Testing annotation saving/loading", t, func() { |
||||
InitTestDB(t) |
||||
|
||||
repo := SqlAnnotationRepo{} |
||||
|
||||
Convey("Can save annotation", func() { |
||||
err := repo.Save(&annotations.Item{ |
||||
OrgId: 1, |
||||
UserId: 1, |
||||
DashboardId: 1, |
||||
Text: "hello", |
||||
Epoch: 10, |
||||
Tags: []string{"outage", "error", "type:outage", "server:server-1"}, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
Convey("Can query for annotation", func() { |
||||
items, err := repo.Find(&annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 0, |
||||
To: 15, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
So(items, ShouldHaveLength, 1) |
||||
|
||||
Convey("Can read tags", func() { |
||||
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Should not find any when item is outside time range", func() { |
||||
items, err := repo.Find(&annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 12, |
||||
To: 15, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
So(items, ShouldHaveLength, 0) |
||||
}) |
||||
|
||||
Convey("Should not find one when tag filter does not match", func() { |
||||
items, err := repo.Find(&annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 1, |
||||
To: 15, |
||||
Tags: []string{"asd"}, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
So(items, ShouldHaveLength, 0) |
||||
}) |
||||
|
||||
Convey("Should find one when all tag filters does match", func() { |
||||
items, err := repo.Find(&annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 1, |
||||
To: 15, |
||||
Tags: []string{"outage", "error"}, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
So(items, ShouldHaveLength, 1) |
||||
}) |
||||
|
||||
Convey("Should find one when all key value tag filters does match", func() { |
||||
items, err := repo.Find(&annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 1, |
||||
To: 15, |
||||
Tags: []string{"type:outage", "server:server-1"}, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
So(items, ShouldHaveLength, 1) |
||||
}) |
||||
|
||||
Convey("Can update annotation and remove all tags", func() { |
||||
query := &annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 0, |
||||
To: 15, |
||||
} |
||||
items, err := repo.Find(query) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
annotationId := items[0].Id |
||||
|
||||
err = repo.Update(&annotations.Item{ |
||||
Id: annotationId, |
||||
OrgId: 1, |
||||
Text: "something new", |
||||
Tags: []string{}, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
items, err = repo.Find(query) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
Convey("Can read tags", func() { |
||||
So(items[0].Id, ShouldEqual, annotationId) |
||||
So(len(items[0].Tags), ShouldEqual, 0) |
||||
So(items[0].Text, ShouldEqual, "something new") |
||||
}) |
||||
}) |
||||
|
||||
Convey("Can update annotation with new tags", func() { |
||||
query := &annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 0, |
||||
To: 15, |
||||
} |
||||
items, err := repo.Find(query) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
annotationId := items[0].Id |
||||
|
||||
err = repo.Update(&annotations.Item{ |
||||
Id: annotationId, |
||||
OrgId: 1, |
||||
Text: "something new", |
||||
Tags: []string{"newtag1", "newtag2"}, |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
items, err = repo.Find(query) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
Convey("Can read tags", func() { |
||||
So(items[0].Id, ShouldEqual, annotationId) |
||||
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"}) |
||||
So(items[0].Text, ShouldEqual, "something new") |
||||
}) |
||||
}) |
||||
|
||||
Convey("Can delete annotation", func() { |
||||
query := &annotations.ItemQuery{ |
||||
OrgId: 1, |
||||
DashboardId: 1, |
||||
From: 0, |
||||
To: 15, |
||||
} |
||||
items, err := repo.Find(query) |
||||
So(err, ShouldBeNil) |
||||
|
||||
annotationId := items[0].Id |
||||
|
||||
err = repo.Delete(&annotations.DeleteParams{Id: annotationId}) |
||||
|
||||
items, err = repo.Find(query) |
||||
So(err, ShouldBeNil) |
||||
|
||||
Convey("Should be deleted", func() { |
||||
So(len(items), ShouldEqual, 0) |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
}) |
||||
} |
@ -0,0 +1,24 @@ |
||||
package migrations |
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" |
||||
|
||||
func addTagMigration(mg *Migrator) { |
||||
|
||||
tagTable := Table{ |
||||
Name: "tag", |
||||
Columns: []*Column{ |
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, |
||||
{Name: "key", Type: DB_NVarchar, Length: 100, Nullable: false}, |
||||
{Name: "value", Type: DB_NVarchar, Length: 100, Nullable: false}, |
||||
}, |
||||
Indices: []*Index{ |
||||
{Cols: []string{"key", "value"}, Type: UniqueIndex}, |
||||
}, |
||||
} |
||||
|
||||
// create table
|
||||
mg.AddMigration("create tag table", NewAddTableMigration(tagTable)) |
||||
|
||||
// create indices
|
||||
mg.AddMigration("add index tag.key_value", NewAddIndexMigration(tagTable, tagTable.Indices[0])) |
||||
} |
@ -1,38 +1,35 @@ |
||||
|
||||
<h5 class="section-heading text-center">Add annotation</h5> |
||||
<div class="graph-annotation"> |
||||
<div class="graph-annotation__header"> |
||||
<div class="graph-annotation__user" bs-tooltip="'Created by {{ctrl.login}}'"> |
||||
</div> |
||||
|
||||
<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 class="graph-annotation__title"> |
||||
<span ng-if="!ctrl.event.id">Add Annotation</span> |
||||
<span ng-if="ctrl.event.id">Edit Annotation</span> |
||||
</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> |
||||
<div class="graph-annotation__time">{{ctrl.timeFormated}}</div> |
||||
</div> |
||||
|
||||
<form name="ctrl.form" class="graph-annotation__body text-center"> |
||||
<div style="display: inline-block"> |
||||
<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="2" ng-model="ctrl.event.text" placeholder="Description"></textarea> |
||||
</div> |
||||
|
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-7">Tags</span> |
||||
<bootstrap-tagsinput ng-model="ctrl.event.tags" tagclass="label label-tag" placeholder="add tags"> |
||||
</bootstrap-tagsinput> |
||||
</div> |
||||
|
||||
<div class="gf-form-button-row"> |
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()">Save</button> |
||||
<button ng-if="ctrl.event.id" type="submit" class="btn btn-danger" ng-click="ctrl.delete()">Delete</button> |
||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
@ -0,0 +1,40 @@ |
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; |
||||
import '../annotations_srv'; |
||||
import helpers from 'test/specs/helpers'; |
||||
|
||||
describe('AnnotationsSrv', function() { |
||||
var ctx = new helpers.ServiceTestContext(); |
||||
|
||||
beforeEach(angularMocks.module('grafana.core')); |
||||
beforeEach(angularMocks.module('grafana.services')); |
||||
beforeEach(() => { |
||||
ctx.createService('annotationsSrv'); |
||||
}); |
||||
describe('When translating the query result', () => { |
||||
const annotationSource = { |
||||
datasource: '-- Grafana --', |
||||
enable: true, |
||||
hide: false, |
||||
limit: 200, |
||||
name: 'test', |
||||
scope: 'global', |
||||
tags: [ |
||||
'test' |
||||
], |
||||
type: 'event', |
||||
}; |
||||
|
||||
const time = 1507039543000; |
||||
const annotations = [{id: 1, panelId: 1, text: 'text', time: time}]; |
||||
let translatedAnnotations; |
||||
|
||||
beforeEach(() => { |
||||
translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations); |
||||
}); |
||||
|
||||
it('should set defaults', () => { |
||||
expect(translatedAnnotations[0].source).to.eql(annotationSource); |
||||
}); |
||||
}); |
||||
}); |
||||
|
@ -0,0 +1,83 @@ |
||||
System.config({ |
||||
defaultJSExtenions: true, |
||||
baseURL: 'public', |
||||
paths: { |
||||
'virtual-scroll': 'vendor/npm/virtual-scroll/src/index.js', |
||||
'mousetrap': 'vendor/npm/mousetrap/mousetrap.js', |
||||
'remarkable': 'vendor/npm/remarkable/dist/remarkable.js', |
||||
'tether': 'vendor/npm/tether/dist/js/tether.js', |
||||
'eventemitter3': 'vendor/npm/eventemitter3/index.js', |
||||
'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js', |
||||
'moment': 'vendor/moment.js', |
||||
"jquery": "vendor/jquery/dist/jquery.js", |
||||
'lodash-src': 'vendor/lodash/dist/lodash.js', |
||||
"lodash": 'app/core/lodash_extended.js', |
||||
"angular": "vendor/angular/angular.js", |
||||
"bootstrap": "vendor/bootstrap/bootstrap.js", |
||||
'angular-route': 'vendor/angular-route/angular-route.js', |
||||
'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js', |
||||
"angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js", |
||||
"angular-strap": "vendor/angular-other/angular-strap.js", |
||||
"angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js", |
||||
"angular-bindonce": "vendor/angular-bindonce/bindonce.js", |
||||
"spectrum": "vendor/spectrum.js", |
||||
"bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js", |
||||
"jquery.flot": "vendor/flot/jquery.flot", |
||||
"jquery.flot.pie": "vendor/flot/jquery.flot.pie", |
||||
"jquery.flot.selection": "vendor/flot/jquery.flot.selection", |
||||
"jquery.flot.stack": "vendor/flot/jquery.flot.stack", |
||||
"jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", |
||||
"jquery.flot.time": "vendor/flot/jquery.flot.time", |
||||
"jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", |
||||
"jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", |
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", |
||||
"d3": "vendor/d3/d3.js", |
||||
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", |
||||
"twemoji": "vendor/npm/twemoji/2/twemoji.amd.js", |
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace", |
||||
}, |
||||
|
||||
packages: { |
||||
app: { |
||||
defaultExtension: 'js', |
||||
}, |
||||
vendor: { |
||||
defaultExtension: 'js', |
||||
}, |
||||
plugins: { |
||||
defaultExtension: 'js', |
||||
}, |
||||
test: { |
||||
defaultExtension: 'js', |
||||
}, |
||||
}, |
||||
|
||||
map: { |
||||
text: 'vendor/plugin-text/text.js', |
||||
css: 'app/core/utils/css_loader.js' |
||||
}, |
||||
|
||||
meta: { |
||||
'vendor/npm/virtual-scroll/src/indx.js': { |
||||
format: 'cjs', |
||||
exports: 'VirtualScroll', |
||||
}, |
||||
'vendor/angular/angular.js': { |
||||
format: 'global', |
||||
deps: ['jquery'], |
||||
exports: 'angular', |
||||
}, |
||||
'vendor/npm/eventemitter3/index.js': { |
||||
format: 'cjs', |
||||
exports: 'EventEmitter' |
||||
}, |
||||
'vendor/npm/mousetrap/mousetrap.js': { |
||||
format: 'global', |
||||
exports: 'Mousetrap' |
||||
}, |
||||
'vendor/npm/ace-builds/src-noconflict/ace.js': { |
||||
format: 'global', |
||||
exports: 'ace' |
||||
} |
||||
} |
||||
}); |
@ -0,0 +1,26 @@ |
||||
.gf-icon-picker { |
||||
width: 400px; |
||||
height: 450px; |
||||
|
||||
.icon-filter { |
||||
padding-bottom: 10px; |
||||
margin: auto; |
||||
width: 50%; |
||||
} |
||||
|
||||
.icon-container { |
||||
max-height: 350px; |
||||
overflow: auto; |
||||
|
||||
.gf-event-icon { |
||||
margin: 0.4rem; |
||||
height: 1.5rem; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.gf-icon-picker-button { |
||||
.gf-event-icon { |
||||
height: 1.2rem; |
||||
} |
||||
} |
@ -0,0 +1,130 @@ |
||||
(function() { |
||||
"use strict"; |
||||
|
||||
// Tun on full stack traces in errors to help debugging
|
||||
Error.stackTraceLimit=Infinity; |
||||
|
||||
window.__karma__.loaded = function() {}; |
||||
|
||||
System.config({ |
||||
baseURL: '/base/', |
||||
defaultJSExtensions: true, |
||||
paths: { |
||||
'mousetrap': 'vendor/npm/mousetrap/mousetrap.js', |
||||
'eventemitter3': 'vendor/npm/eventemitter3/index.js', |
||||
'remarkable': 'vendor/npm/remarkable/dist/remarkable.js', |
||||
'tether': 'vendor/npm/tether/dist/js/tether.js', |
||||
'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js', |
||||
'moment': 'vendor/moment.js', |
||||
"jquery": "vendor/jquery/dist/jquery.js", |
||||
'lodash-src': 'vendor/lodash/dist/lodash.js', |
||||
"lodash": 'app/core/lodash_extended.js', |
||||
"angular": 'vendor/angular/angular.js', |
||||
'angular-mocks': 'vendor/angular-mocks/angular-mocks.js', |
||||
"bootstrap": "vendor/bootstrap/bootstrap.js", |
||||
'angular-route': 'vendor/angular-route/angular-route.js', |
||||
'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js', |
||||
"angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js", |
||||
"angular-strap": "vendor/angular-other/angular-strap.js", |
||||
"angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js", |
||||
"angular-bindonce": "vendor/angular-bindonce/bindonce.js", |
||||
"spectrum": "vendor/spectrum.js", |
||||
"bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js", |
||||
"jquery.flot": "vendor/flot/jquery.flot", |
||||
"jquery.flot.pie": "vendor/flot/jquery.flot.pie", |
||||
"jquery.flot.selection": "vendor/flot/jquery.flot.selection", |
||||
"jquery.flot.stack": "vendor/flot/jquery.flot.stack", |
||||
"jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent", |
||||
"jquery.flot.time": "vendor/flot/jquery.flot.time", |
||||
"jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", |
||||
"jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", |
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", |
||||
"d3": "vendor/d3/d3.js", |
||||
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes", |
||||
"twemoji": "vendor/npm/twemoji/2/twemoji.amd.js", |
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace", |
||||
}, |
||||
|
||||
packages: { |
||||
app: { |
||||
defaultExtension: 'js', |
||||
}, |
||||
vendor: { |
||||
defaultExtension: 'js', |
||||
}, |
||||
}, |
||||
|
||||
map: { |
||||
}, |
||||
|
||||
meta: { |
||||
'vendor/angular/angular.js': { |
||||
format: 'global', |
||||
deps: ['jquery'], |
||||
exports: 'angular', |
||||
}, |
||||
'vendor/angular-mocks/angular-mocks.js': { |
||||
format: 'global', |
||||
deps: ['angular'], |
||||
}, |
||||
'vendor/npm/eventemitter3/index.js': { |
||||
format: 'cjs', |
||||
exports: 'EventEmitter' |
||||
}, |
||||
'vendor/npm/mousetrap/mousetrap.js': { |
||||
format: 'global', |
||||
exports: 'Mousetrap' |
||||
}, |
||||
'vendor/npm/ace-builds/src-noconflict/ace.js': { |
||||
format: 'global', |
||||
exports: 'ace' |
||||
}, |
||||
} |
||||
}); |
||||
|
||||
function file2moduleName(filePath) { |
||||
return filePath.replace(/\\/g, '/') |
||||
.replace(/^\/base\//, '') |
||||
.replace(/\.\w*$/, ''); |
||||
} |
||||
|
||||
function onlySpecFiles(path) { |
||||
return /specs.*/.test(path); |
||||
} |
||||
|
||||
window.grafanaBootData = {settings: {}}; |
||||
|
||||
var modules = ['angular', 'angular-mocks', 'app/app']; |
||||
var promises = modules.map(function(name) { |
||||
return System.import(name); |
||||
}); |
||||
|
||||
Promise.all(promises).then(function(deps) { |
||||
var angular = deps[0]; |
||||
|
||||
angular.module('grafana', ['ngRoute']); |
||||
angular.module('grafana.services', ['ngRoute', '$strap.directives']); |
||||
angular.module('grafana.panels', []); |
||||
angular.module('grafana.controllers', []); |
||||
angular.module('grafana.directives', []); |
||||
angular.module('grafana.filters', []); |
||||
angular.module('grafana.routes', ['ngRoute']); |
||||
|
||||
// load specs
|
||||
return Promise.all( |
||||
Object.keys(window.__karma__.files) // All files served by Karma.
|
||||
.filter(onlySpecFiles) |
||||
.map(file2moduleName) |
||||
.map(function(path) { |
||||
// console.log(path);
|
||||
return System.import(path); |
||||
})); |
||||
}).then(function() { |
||||
window.__karma__.start(); |
||||
}, function(error) { |
||||
window.__karma__.error(error.stack || error); |
||||
}).catch(function(error) { |
||||
window.__karma__.error(error.stack || error); |
||||
}); |
||||
|
||||
})(); |
@ -0,0 +1,45 @@ |
||||
module.exports = function(config) { |
||||
return { |
||||
// copy source to temp, we will minify in place for the dist build
|
||||
everything_but_less_to_temp: { |
||||
cwd: '<%= srcDir %>', |
||||
expand: true, |
||||
src: ['**/*', '!**/*.less'], |
||||
dest: '<%= tempDir %>' |
||||
}, |
||||
|
||||
public_to_gen: { |
||||
cwd: '<%= srcDir %>', |
||||
expand: true, |
||||
src: ['**/*', '!**/*.less'], |
||||
dest: '<%= genDir %>' |
||||
}, |
||||
|
||||
node_modules: { |
||||
cwd: './node_modules', |
||||
expand: true, |
||||
src: [ |
||||
'ace-builds/src-noconflict/**/*', |
||||
'eventemitter3/*.js', |
||||
'systemjs/dist/*.js', |
||||
'es6-promise/**/*', |
||||
'es6-shim/*.js', |
||||
'reflect-metadata/*.js', |
||||
'reflect-metadata/*.ts', |
||||
'reflect-metadata/*.d.ts', |
||||
'rxjs/**/*', |
||||
'tether/**/*', |
||||
'tether-drop/**/*', |
||||
'tether-drop/**/*', |
||||
'remarkable/dist/*', |
||||
'remarkable/dist/*', |
||||
'virtual-scroll/**/*', |
||||
'mousetrap/**/*', |
||||
'twemoji/2/twemoji.amd*', |
||||
'twemoji/2/svg/*.svg', |
||||
], |
||||
dest: '<%= srcDir %>/vendor/npm' |
||||
} |
||||
|
||||
}; |
||||
}; |
Loading…
Reference in new issue