mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 issue
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,6 +41,7 @@ profile.cov
|
|||||||
.notouch
|
.notouch
|
||||||
/pkg/cmd/grafana-cli/grafana-cli
|
/pkg/cmd/grafana-cli/grafana-cli
|
||||||
/pkg/cmd/grafana-server/grafana-server
|
/pkg/cmd/grafana-server/grafana-server
|
||||||
|
/pkg/cmd/grafana-server/debug
|
||||||
/examples/*/dist
|
/examples/*/dist
|
||||||
/packaging/**/*.rpm
|
/packaging/**/*.rpm
|
||||||
/packaging/**/*.deb
|
/packaging/**/*.deb
|
||||||
|
189
docs/sources/http_api/annotations.md
Normal file
189
docs/sources/http_api/annotations.md
Normal file
@@ -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
|
||||||
|
[
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
```
|
||||||
|
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"}
|
||||||
|
```
|
@@ -87,8 +87,8 @@
|
|||||||
"tslint-loader": "^3.5.3",
|
"tslint-loader": "^3.5.3",
|
||||||
"typescript": "^2.5.2",
|
"typescript": "^2.5.2",
|
||||||
"webpack": "^3.6.0",
|
"webpack": "^3.6.0",
|
||||||
"webpack-bundle-analyzer": "^2.9.0",
|
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
"webpack-cleanup-plugin": "^0.5.1",
|
||||||
|
"webpack-bundle-analyzer": "^2.9.0",
|
||||||
"webpack-merge": "^4.1.0",
|
"webpack-merge": "^4.1.0",
|
||||||
"zone.js": "^0.7.2"
|
"zone.js": "^0.7.2"
|
||||||
},
|
},
|
||||||
@@ -97,13 +97,14 @@
|
|||||||
"watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
|
"watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
|
||||||
"build": "./node_modules/.bin/grunt build",
|
"build": "./node_modules/.bin/grunt build",
|
||||||
"test": "./node_modules/.bin/grunt test",
|
"test": "./node_modules/.bin/grunt test",
|
||||||
"lint" : "./node_modules/.bin/tslint -c tslint.json --project ./tsconfig.json --type-check",
|
"lint" : "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check",
|
||||||
"watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev"
|
"watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev"
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"angular": "^1.6.6",
|
"angular": "^1.6.6",
|
||||||
"angular-bindonce": "^0.3.1",
|
"angular-bindonce": "^0.3.1",
|
||||||
|
"angular-mocks": "^1.6.6",
|
||||||
"angular-native-dragdrop": "^1.2.2",
|
"angular-native-dragdrop": "^1.2.2",
|
||||||
"angular-route": "^1.6.6",
|
"angular-route": "^1.6.6",
|
||||||
"angular-sanitize": "^1.6.6",
|
"angular-sanitize": "^1.6.6",
|
||||||
|
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
)
|
)
|
||||||
@@ -11,13 +12,12 @@ func GetAnnotations(c *middleware.Context) Response {
|
|||||||
query := &annotations.ItemQuery{
|
query := &annotations.ItemQuery{
|
||||||
From: c.QueryInt64("from") / 1000,
|
From: c.QueryInt64("from") / 1000,
|
||||||
To: c.QueryInt64("to") / 1000,
|
To: c.QueryInt64("to") / 1000,
|
||||||
Type: annotations.ItemType(c.Query("type")),
|
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
AlertId: c.QueryInt64("alertId"),
|
AlertId: c.QueryInt64("alertId"),
|
||||||
DashboardId: c.QueryInt64("dashboardId"),
|
DashboardId: c.QueryInt64("dashboardId"),
|
||||||
PanelId: c.QueryInt64("panelId"),
|
PanelId: c.QueryInt64("panelId"),
|
||||||
Limit: c.QueryInt64("limit"),
|
Limit: c.QueryInt64("limit"),
|
||||||
NewState: c.QueryStrings("newState"),
|
Tags: c.QueryStrings("tags"),
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := annotations.GetRepository()
|
repo := annotations.GetRepository()
|
||||||
@@ -27,25 +27,14 @@ func GetAnnotations(c *middleware.Context) Response {
|
|||||||
return ApiError(500, "Failed to get annotations", err)
|
return ApiError(500, "Failed to get annotations", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]dtos.Annotation, 0)
|
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
result = append(result, dtos.Annotation{
|
if item.Email != "" {
|
||||||
AlertId: item.AlertId,
|
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
|
||||||
Time: item.Epoch * 1000,
|
}
|
||||||
Data: item.Data,
|
item.Time = item.Time * 1000
|
||||||
NewState: item.NewState,
|
|
||||||
PrevState: item.PrevState,
|
|
||||||
Text: item.Text,
|
|
||||||
Metric: item.Metric,
|
|
||||||
Title: item.Title,
|
|
||||||
PanelId: item.PanelId,
|
|
||||||
RegionId: item.RegionId,
|
|
||||||
Type: string(item.Type),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Json(200, result)
|
return Json(200, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
|
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
|
||||||
@@ -53,14 +42,13 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
|||||||
|
|
||||||
item := annotations.Item{
|
item := annotations.Item{
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
|
UserId: c.UserId,
|
||||||
DashboardId: cmd.DashboardId,
|
DashboardId: cmd.DashboardId,
|
||||||
PanelId: cmd.PanelId,
|
PanelId: cmd.PanelId,
|
||||||
Epoch: cmd.Time / 1000,
|
Epoch: cmd.Time / 1000,
|
||||||
Title: cmd.Title,
|
|
||||||
Text: cmd.Text,
|
Text: cmd.Text,
|
||||||
CategoryId: cmd.CategoryId,
|
Data: cmd.Data,
|
||||||
NewState: cmd.FillColor,
|
Tags: cmd.Tags,
|
||||||
Type: annotations.EventType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo.Save(&item); err != nil {
|
if err := repo.Save(&item); err != nil {
|
||||||
@@ -71,12 +59,16 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
|||||||
if cmd.IsRegion {
|
if cmd.IsRegion {
|
||||||
item.RegionId = item.Id
|
item.RegionId = item.Id
|
||||||
|
|
||||||
|
if item.Data == nil {
|
||||||
|
item.Data = simplejson.New()
|
||||||
|
}
|
||||||
|
|
||||||
if err := repo.Update(&item); err != nil {
|
if err := repo.Update(&item); err != nil {
|
||||||
return ApiError(500, "Failed set regionId on annotation", err)
|
return ApiError(500, "Failed set regionId on annotation", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Id = 0
|
item.Id = 0
|
||||||
item.Epoch = cmd.TimeEnd
|
item.Epoch = cmd.TimeEnd / 1000
|
||||||
|
|
||||||
if err := repo.Save(&item); err != nil {
|
if err := repo.Save(&item); err != nil {
|
||||||
return ApiError(500, "Failed save annotation for region end time", err)
|
return ApiError(500, "Failed save annotation for region end time", err)
|
||||||
@@ -86,6 +78,41 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
|||||||
return ApiSuccess("Annotation added")
|
return ApiSuccess("Annotation added")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||||
|
annotationId := c.ParamsInt64(":annotationId")
|
||||||
|
|
||||||
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
|
item := annotations.Item{
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
UserId: c.UserId,
|
||||||
|
Id: annotationId,
|
||||||
|
Epoch: cmd.Time / 1000,
|
||||||
|
Text: cmd.Text,
|
||||||
|
Tags: cmd.Tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.Update(&item); err != nil {
|
||||||
|
return ApiError(500, "Failed to update annotation", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.IsRegion {
|
||||||
|
itemRight := item
|
||||||
|
itemRight.RegionId = item.Id
|
||||||
|
itemRight.Epoch = cmd.TimeEnd / 1000
|
||||||
|
|
||||||
|
// We don't know id of region right event, so set it to 0 and find then using query like
|
||||||
|
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
|
||||||
|
itemRight.Id = 0
|
||||||
|
|
||||||
|
if err := repo.Update(&itemRight); err != nil {
|
||||||
|
return ApiError(500, "Failed to update annotation for region end time", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Annotation updated")
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
|
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
|
||||||
repo := annotations.GetRepository()
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
@@ -101,3 +128,33 @@ func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Res
|
|||||||
|
|
||||||
return ApiSuccess("Annotations deleted")
|
return ApiSuccess("Annotations deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteAnnotationById(c *middleware.Context) Response {
|
||||||
|
repo := annotations.GetRepository()
|
||||||
|
annotationId := c.ParamsInt64(":annotationId")
|
||||||
|
|
||||||
|
err := repo.Delete(&annotations.DeleteParams{
|
||||||
|
Id: annotationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Failed to delete annotation", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Annotation deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteAnnotationRegion(c *middleware.Context) Response {
|
||||||
|
repo := annotations.GetRepository()
|
||||||
|
regionId := c.ParamsInt64(":regionId")
|
||||||
|
|
||||||
|
err := repo.Delete(&annotations.DeleteParams{
|
||||||
|
RegionId: regionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Failed to delete annotation region", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Annotation region deleted")
|
||||||
|
}
|
||||||
|
@@ -289,6 +289,9 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
|
|
||||||
apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
|
apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
|
||||||
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
|
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
|
||||||
|
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
|
||||||
|
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
|
||||||
|
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
|
||||||
}, reqEditorRole)
|
}, reqEditorRole)
|
||||||
|
|
||||||
// error test
|
// error test
|
||||||
|
@@ -65,7 +65,7 @@ func New(hash string) *Avatar {
|
|||||||
return &Avatar{
|
return &Avatar{
|
||||||
hash: hash,
|
hash: hash,
|
||||||
reqParams: url.Values{
|
reqParams: url.Values{
|
||||||
"d": {"404"},
|
"d": {"retro"},
|
||||||
"size": {"200"},
|
"size": {"200"},
|
||||||
"r": {"pg"}}.Encode(),
|
"r": {"pg"}}.Encode(),
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ func CacheServer() http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newNotFound() *Avatar {
|
func newNotFound() *Avatar {
|
||||||
avatar := &Avatar{}
|
avatar := &Avatar{notFound: true}
|
||||||
|
|
||||||
// load transparent png into buffer
|
// load transparent png into buffer
|
||||||
path := filepath.Join(setting.StaticRootPath, "img", "transparent.png")
|
path := filepath.Join(setting.StaticRootPath, "img", "transparent.png")
|
||||||
|
@@ -2,37 +2,30 @@ package dtos
|
|||||||
|
|
||||||
import "github.com/grafana/grafana/pkg/components/simplejson"
|
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
|
||||||
type Annotation struct {
|
type PostAnnotationsCmd struct {
|
||||||
AlertId int64 `json:"alertId"`
|
DashboardId int64 `json:"dashboardId"`
|
||||||
DashboardId int64 `json:"dashboardId"`
|
PanelId int64 `json:"panelId"`
|
||||||
PanelId int64 `json:"panelId"`
|
Time int64 `json:"time"`
|
||||||
NewState string `json:"newState"`
|
Text string `json:"text"`
|
||||||
PrevState string `json:"prevState"`
|
Tags []string `json:"tags"`
|
||||||
Time int64 `json:"time"`
|
Data *simplejson.Json `json:"data"`
|
||||||
Title string `json:"title"`
|
IsRegion bool `json:"isRegion"`
|
||||||
Text string `json:"text"`
|
TimeEnd int64 `json:"timeEnd"`
|
||||||
Metric string `json:"metric"`
|
|
||||||
RegionId int64 `json:"regionId"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
|
|
||||||
Data *simplejson.Json `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostAnnotationsCmd struct {
|
type UpdateAnnotationsCmd struct {
|
||||||
DashboardId int64 `json:"dashboardId"`
|
Id int64 `json:"id"`
|
||||||
PanelId int64 `json:"panelId"`
|
Time int64 `json:"time"`
|
||||||
CategoryId int64 `json:"categoryId"`
|
Text string `json:"text"`
|
||||||
Time int64 `json:"time"`
|
Tags []string `json:"tags"`
|
||||||
Title string `json:"title"`
|
IsRegion bool `json:"isRegion"`
|
||||||
Text string `json:"text"`
|
TimeEnd int64 `json:"timeEnd"`
|
||||||
|
|
||||||
FillColor string `json:"fillColor"`
|
|
||||||
IsRegion bool `json:"isRegion"`
|
|
||||||
TimeEnd int64 `json:"timeEnd"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteAnnotationsCmd struct {
|
type DeleteAnnotationsCmd struct {
|
||||||
AlertId int64 `json:"alertId"`
|
AlertId int64 `json:"alertId"`
|
||||||
DashboardId int64 `json:"dashboardId"`
|
DashboardId int64 `json:"dashboardId"`
|
||||||
PanelId int64 `json:"panelId"`
|
PanelId int64 `json:"panelId"`
|
||||||
|
AnnotationId int64 `json:"annotationId"`
|
||||||
|
RegionId int64 `json:"regionId"`
|
||||||
}
|
}
|
||||||
|
60
pkg/models/tags.go
Normal file
60
pkg/models/tags.go
Normal file
@@ -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
|
||||||
|
}
|
95
pkg/models/tags_test.go
Normal file
95
pkg/models/tags_test.go
Normal file
@@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@@ -73,10 +73,8 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
|||||||
OrgId: evalContext.Rule.OrgId,
|
OrgId: evalContext.Rule.OrgId,
|
||||||
DashboardId: evalContext.Rule.DashboardId,
|
DashboardId: evalContext.Rule.DashboardId,
|
||||||
PanelId: evalContext.Rule.PanelId,
|
PanelId: evalContext.Rule.PanelId,
|
||||||
Type: annotations.AlertType,
|
|
||||||
AlertId: evalContext.Rule.Id,
|
AlertId: evalContext.Rule.Id,
|
||||||
Title: evalContext.Rule.Name,
|
Text: "",
|
||||||
Text: evalContext.GetStateModel().Text,
|
|
||||||
NewState: string(evalContext.Rule.State),
|
NewState: string(evalContext.Rule.State),
|
||||||
PrevState: string(evalContext.PrevAlertState),
|
PrevState: string(evalContext.PrevAlertState),
|
||||||
Epoch: time.Now().Unix(),
|
Epoch: time.Now().Unix(),
|
||||||
|
@@ -5,7 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
|
|||||||
type Repository interface {
|
type Repository interface {
|
||||||
Save(item *Item) error
|
Save(item *Item) error
|
||||||
Update(item *Item) error
|
Update(item *Item) error
|
||||||
Find(query *ItemQuery) ([]*Item, error)
|
Find(query *ItemQuery) ([]*ItemDTO, error)
|
||||||
Delete(params *DeleteParams) error
|
Delete(params *DeleteParams) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,11 +13,10 @@ type ItemQuery struct {
|
|||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
From int64 `json:"from"`
|
From int64 `json:"from"`
|
||||||
To int64 `json:"to"`
|
To int64 `json:"to"`
|
||||||
Type ItemType `json:"type"`
|
|
||||||
AlertId int64 `json:"alertId"`
|
AlertId int64 `json:"alertId"`
|
||||||
DashboardId int64 `json:"dashboardId"`
|
DashboardId int64 `json:"dashboardId"`
|
||||||
PanelId int64 `json:"panelId"`
|
PanelId int64 `json:"panelId"`
|
||||||
NewState []string `json:"newState"`
|
Tags []string `json:"tags"`
|
||||||
|
|
||||||
Limit int64 `json:"limit"`
|
Limit int64 `json:"limit"`
|
||||||
}
|
}
|
||||||
@@ -28,12 +27,15 @@ type PostParams struct {
|
|||||||
Epoch int64 `json:"epoch"`
|
Epoch int64 `json:"epoch"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteParams struct {
|
type DeleteParams struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
AlertId int64 `json:"alertId"`
|
AlertId int64 `json:"alertId"`
|
||||||
DashboardId int64 `json:"dashboardId"`
|
DashboardId int64 `json:"dashboardId"`
|
||||||
PanelId int64 `json:"panelId"`
|
PanelId int64 `json:"panelId"`
|
||||||
|
RegionId int64 `json:"regionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var repositoryInstance Repository
|
var repositoryInstance Repository
|
||||||
@@ -46,29 +48,41 @@ func SetRepository(rep Repository) {
|
|||||||
repositoryInstance = rep
|
repositoryInstance = rep
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
AlertType ItemType = "alert"
|
|
||||||
EventType ItemType = "event"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
DashboardId int64 `json:"dashboardId"`
|
UserId int64 `json:"userId"`
|
||||||
PanelId int64 `json:"panelId"`
|
DashboardId int64 `json:"dashboardId"`
|
||||||
CategoryId int64 `json:"categoryId"`
|
PanelId int64 `json:"panelId"`
|
||||||
RegionId int64 `json:"regionId"`
|
RegionId int64 `json:"regionId"`
|
||||||
Type ItemType `json:"type"`
|
Text string `json:"text"`
|
||||||
Title string `json:"title"`
|
AlertId int64 `json:"alertId"`
|
||||||
Text string `json:"text"`
|
PrevState string `json:"prevState"`
|
||||||
Metric string `json:"metric"`
|
NewState string `json:"newState"`
|
||||||
AlertId int64 `json:"alertId"`
|
Epoch int64 `json:"epoch"`
|
||||||
UserId int64 `json:"userId"`
|
Tags []string `json:"tags"`
|
||||||
PrevState string `json:"prevState"`
|
Data *simplejson.Json `json:"data"`
|
||||||
NewState string `json:"newState"`
|
|
||||||
Epoch int64 `json:"epoch"`
|
|
||||||
|
|
||||||
Data *simplejson.Json `json:"data"`
|
// needed until we remove it from db
|
||||||
|
Type string
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemDTO struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
AlertId int64 `json:"alertId"`
|
||||||
|
AlertName string `json:"alertName"`
|
||||||
|
DashboardId int64 `json:"dashboardId"`
|
||||||
|
PanelId int64 `json:"panelId"`
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
NewState string `json:"newState"`
|
||||||
|
PrevState string `json:"prevState"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
RegionId int64 `json:"regionId"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
|
Data *simplejson.Json `json:"data"`
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,11 @@ package sqlstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,19 +15,94 @@ type SqlAnnotationRepo struct {
|
|||||||
|
|
||||||
func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
|
tags := models.ParseTagPairs(item.Tags)
|
||||||
|
item.Tags = models.JoinTagPairs(tags)
|
||||||
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.Tags != nil {
|
||||||
|
if tags, err := r.ensureTagsExist(sess, tags); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Will insert if needed any new key/value pars and return ids
|
||||||
|
func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag) ([]*models.Tag, error) {
|
||||||
|
for _, tag := range tags {
|
||||||
|
var existingTag models.Tag
|
||||||
|
|
||||||
|
// check if it exists
|
||||||
|
if exists, err := sess.Table("tag").Where("key=? AND value=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
tag.Id = existingTag.Id
|
||||||
|
} else {
|
||||||
|
if _, err := sess.Table("tag").Insert(tag); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
|
var (
|
||||||
|
isExist bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
existing := new(annotations.Item)
|
||||||
|
|
||||||
if _, err := sess.Table("annotation").Id(item.Id).Update(item); err != nil {
|
if item.Id == 0 && item.RegionId != 0 {
|
||||||
|
// Update region end time
|
||||||
|
isExist, err = sess.Table("annotation").Where("region_id=? AND id!=? AND org_id=?", item.RegionId, item.RegionId, item.OrgId).Get(existing)
|
||||||
|
} else {
|
||||||
|
isExist, err = sess.Table("annotation").Where("id=? AND org_id=?", item.Id, item.OrgId).Get(existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isExist {
|
||||||
|
return errors.New("Annotation not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.Epoch = item.Epoch
|
||||||
|
existing.Text = item.Text
|
||||||
|
if item.RegionId != 0 {
|
||||||
|
existing.RegionId = item.RegionId
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Tags != nil {
|
||||||
|
if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, tag := range tags {
|
||||||
|
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.Tags = item.Tags
|
||||||
|
|
||||||
|
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,51 +110,79 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
|
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||||
var sql bytes.Buffer
|
var sql bytes.Buffer
|
||||||
params := make([]interface{}, 0)
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
sql.WriteString(`SELECT *
|
sql.WriteString(`
|
||||||
from annotation
|
SELECT
|
||||||
`)
|
annotation.id,
|
||||||
|
annotation.epoch as time,
|
||||||
|
annotation.dashboard_id,
|
||||||
|
annotation.panel_id,
|
||||||
|
annotation.new_state,
|
||||||
|
annotation.prev_state,
|
||||||
|
annotation.alert_id,
|
||||||
|
annotation.region_id,
|
||||||
|
annotation.text,
|
||||||
|
annotation.tags,
|
||||||
|
annotation.data,
|
||||||
|
usr.email,
|
||||||
|
usr.login,
|
||||||
|
alert.name as alert_name
|
||||||
|
FROM annotation
|
||||||
|
LEFT OUTER JOIN ` + dialect.Quote("user") + ` as usr on usr.id = annotation.user_id
|
||||||
|
LEFT OUTER JOIN alert on alert.id = annotation.alert_id
|
||||||
|
`)
|
||||||
|
|
||||||
sql.WriteString(`WHERE org_id = ?`)
|
sql.WriteString(`WHERE annotation.org_id = ?`)
|
||||||
params = append(params, query.OrgId)
|
params = append(params, query.OrgId)
|
||||||
|
|
||||||
if query.AlertId != 0 {
|
if query.AlertId != 0 {
|
||||||
sql.WriteString(` AND alert_id = ?`)
|
sql.WriteString(` AND annotation.alert_id = ?`)
|
||||||
params = append(params, query.AlertId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.AlertId != 0 {
|
|
||||||
sql.WriteString(` AND alert_id = ?`)
|
|
||||||
params = append(params, query.AlertId)
|
params = append(params, query.AlertId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.DashboardId != 0 {
|
if query.DashboardId != 0 {
|
||||||
sql.WriteString(` AND dashboard_id = ?`)
|
sql.WriteString(` AND annotation.dashboard_id = ?`)
|
||||||
params = append(params, query.DashboardId)
|
params = append(params, query.DashboardId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.PanelId != 0 {
|
if query.PanelId != 0 {
|
||||||
sql.WriteString(` AND panel_id = ?`)
|
sql.WriteString(` AND annotation.panel_id = ?`)
|
||||||
params = append(params, query.PanelId)
|
params = append(params, query.PanelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.From > 0 && query.To > 0 {
|
if query.From > 0 && query.To > 0 {
|
||||||
sql.WriteString(` AND epoch BETWEEN ? AND ?`)
|
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
|
||||||
params = append(params, query.From, query.To)
|
params = append(params, query.From, query.To)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Type != "" {
|
if len(query.Tags) > 0 {
|
||||||
sql.WriteString(` AND type = ?`)
|
keyValueFilters := []string{}
|
||||||
params = append(params, string(query.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(query.NewState) > 0 {
|
tags := models.ParseTagPairs(query.Tags)
|
||||||
sql.WriteString(` AND new_state IN (?` + strings.Repeat(",?", len(query.NewState)-1) + ")")
|
for _, tag := range tags {
|
||||||
for _, v := range query.NewState {
|
if tag.Value == "" {
|
||||||
params = append(params, v)
|
keyValueFilters = append(keyValueFilters, "(tag.key = ?)")
|
||||||
|
params = append(params, tag.Key)
|
||||||
|
} else {
|
||||||
|
keyValueFilters = append(keyValueFilters, "(tag.key = ? AND tag.value = ?)")
|
||||||
|
params = append(params, tag.Key, tag.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) > 0 {
|
||||||
|
tagsSubQuery := fmt.Sprintf(`
|
||||||
|
SELECT SUM(1) FROM annotation_tag at
|
||||||
|
INNER JOIN tag on tag.id = at.tag_id
|
||||||
|
WHERE at.annotation_id = annotation.id
|
||||||
|
AND (
|
||||||
|
%s
|
||||||
|
)
|
||||||
|
`, strings.Join(keyValueFilters, " OR "))
|
||||||
|
|
||||||
|
sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +192,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
|
|
||||||
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||||
|
|
||||||
items := make([]*annotations.Item, 0)
|
items := make([]*annotations.ItemDTO, 0)
|
||||||
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -97,11 +202,31 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
|
|
||||||
func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
|
func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
|
var (
|
||||||
|
sql string
|
||||||
|
annoTagSql string
|
||||||
|
queryParams []interface{}
|
||||||
|
)
|
||||||
|
|
||||||
sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
|
if params.RegionId != 0 {
|
||||||
|
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE region_id = ?)"
|
||||||
|
sql = "DELETE FROM annotation WHERE region_id = ?"
|
||||||
|
queryParams = []interface{}{params.RegionId}
|
||||||
|
} else if params.Id != 0 {
|
||||||
|
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ?)"
|
||||||
|
sql = "DELETE FROM annotation WHERE id = ?"
|
||||||
|
queryParams = []interface{}{params.Id}
|
||||||
|
} else {
|
||||||
|
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ?)"
|
||||||
|
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
|
||||||
|
queryParams = []interface{}{params.DashboardId, params.PanelId}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := sess.Exec(sql, params.DashboardId, params.PanelId)
|
if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sess.Exec(sql, queryParams...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
208
pkg/services/sqlstore/annotation_test.go
Normal file
208
pkg/services/sqlstore/annotation_test.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@@ -261,6 +261,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
|||||||
"DELETE FROM dashboard WHERE id = ?",
|
"DELETE FROM dashboard WHERE id = ?",
|
||||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||||
|
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sql := range deletes {
|
for _, sql := range deletes {
|
||||||
|
@@ -57,4 +57,37 @@ func addAnnotationMig(mg *Migrator) {
|
|||||||
mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{
|
mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{
|
||||||
Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0",
|
Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
categoryIdIndex := &Index{Cols: []string{"org_id", "category_id"}, Type: IndexType}
|
||||||
|
mg.AddMigration("Drop category_id index", NewDropIndexMigration(table, categoryIdIndex))
|
||||||
|
|
||||||
|
mg.AddMigration("Add column tags to annotation table", NewAddColumnMigration(table, &Column{
|
||||||
|
Name: "tags", Type: DB_NVarchar, Nullable: true, Length: 500,
|
||||||
|
}))
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Annotation tag
|
||||||
|
///
|
||||||
|
annotationTagTable := Table{
|
||||||
|
Name: "annotation_tag",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "annotation_id", Type: DB_BigInt, Nullable: false},
|
||||||
|
{Name: "tag_id", Type: DB_BigInt, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*Index{
|
||||||
|
{Cols: []string{"annotation_id", "tag_id"}, Type: UniqueIndex},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("Create annotation_tag table v2", NewAddTableMigration(annotationTagTable))
|
||||||
|
mg.AddMigration("Add unique index annotation_tag.annotation_id_tag_id", NewAddIndexMigration(annotationTagTable, annotationTagTable.Indices[0]))
|
||||||
|
|
||||||
|
//
|
||||||
|
// clear alert text
|
||||||
|
//
|
||||||
|
updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0"
|
||||||
|
mg.AddMigration("Update alert annotations and set TEXT to empty", new(RawSqlMigration).
|
||||||
|
Sqlite(updateTextFieldSql).
|
||||||
|
Postgres(updateTextFieldSql).
|
||||||
|
Mysql(updateTextFieldSql))
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,7 @@ func AddMigrations(mg *Migrator) {
|
|||||||
addAnnotationMig(mg)
|
addAnnotationMig(mg)
|
||||||
addTestDataMigrations(mg)
|
addTestDataMigrations(mg)
|
||||||
addDashboardVersionMigration(mg)
|
addDashboardVersionMigration(mg)
|
||||||
|
addTagMigration(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
24
pkg/services/sqlstore/migrations/tag_mig.go
Normal file
24
pkg/services/sqlstore/migrations/tag_mig.go
Normal file
@@ -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]))
|
||||||
|
}
|
@@ -4,9 +4,6 @@ import coreModule from 'app/core/core_module';
|
|||||||
|
|
||||||
var template = `
|
var template = `
|
||||||
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
|
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
|
||||||
<info-popover mode="right-absolute">
|
|
||||||
Not finding dashboard you want? Star it first, then it should appear in this select box.
|
|
||||||
</info-popover>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export class DashboardSelectorCtrl {
|
export class DashboardSelectorCtrl {
|
||||||
|
@@ -7,6 +7,7 @@ import $ from 'jquery';
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import {profiler} from 'app/core/profiler';
|
import {profiler} from 'app/core/profiler';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import Drop from 'tether-drop';
|
||||||
|
|
||||||
export class GrafanaCtrl {
|
export class GrafanaCtrl {
|
||||||
|
|
||||||
@@ -117,6 +118,11 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
|
|||||||
if (data.params.kiosk) {
|
if (data.params.kiosk) {
|
||||||
appEvents.emit('toggle-kiosk-mode');
|
appEvents.emit('toggle-kiosk-mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// close all drops
|
||||||
|
for (let drop of Drop.drops) {
|
||||||
|
drop.destroy();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle kiosk mode
|
// handle kiosk mode
|
||||||
|
@@ -27,6 +27,8 @@ export function infoPopover() {
|
|||||||
|
|
||||||
transclude(function(clone, newScope) {
|
transclude(function(clone, newScope) {
|
||||||
var content = document.createElement("div");
|
var content = document.createElement("div");
|
||||||
|
content.className = 'markdown-html';
|
||||||
|
|
||||||
_.each(clone, (node) => {
|
_.each(clone, (node) => {
|
||||||
content.appendChild(node);
|
content.appendChild(node);
|
||||||
});
|
});
|
||||||
|
@@ -88,6 +88,7 @@ function (angular, $, coreModule) {
|
|||||||
typeahead: {
|
typeahead: {
|
||||||
source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
|
source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
|
||||||
},
|
},
|
||||||
|
widthClass: attrs.widthClass,
|
||||||
itemValue: getItemProperty(scope, attrs.itemvalue),
|
itemValue: getItemProperty(scope, attrs.itemvalue),
|
||||||
itemText : getItemProperty(scope, attrs.itemtext),
|
itemText : getItemProperty(scope, attrs.itemtext),
|
||||||
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
|
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
|
||||||
|
@@ -163,7 +163,7 @@ export class NavModelSrv {
|
|||||||
|
|
||||||
menu.push({
|
menu.push({
|
||||||
title: 'Annotations',
|
title: 'Annotations',
|
||||||
icon: 'fa fa-fw fa-bolt',
|
icon: 'fa fa-fw fa-comment',
|
||||||
clickHandler: () => dashNavCtrl.openEditView('annotations')
|
clickHandler: () => dashNavCtrl.openEditView('annotations')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -128,7 +128,6 @@ function joinEvalMatches(matches, separator: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAlertAnnotationInfo(ah) {
|
function getAlertAnnotationInfo(ah) {
|
||||||
|
|
||||||
// backward compatability, can be removed in grafana 5.x
|
// backward compatability, can be removed in grafana 5.x
|
||||||
// old way stored evalMatches in data property directly,
|
// old way stored evalMatches in data property directly,
|
||||||
// new way stores it in evalMatches property on new data object
|
// new way stores it in evalMatches property on new data object
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import alertDef from '../alerting/alert_def';
|
import alertDef from '../alerting/alert_def';
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, popoverSrv, $compile) {
|
||||||
|
|
||||||
function sanitizeString(str) {
|
function sanitizeString(str) {
|
||||||
try {
|
try {
|
||||||
@@ -21,6 +19,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: {
|
scope: {
|
||||||
"event": "=",
|
"event": "=",
|
||||||
|
"onEdit": "&"
|
||||||
},
|
},
|
||||||
link: function(scope, element) {
|
link: function(scope, element) {
|
||||||
var event = scope.event;
|
var event = scope.event;
|
||||||
@@ -31,33 +30,46 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
|||||||
var tooltip = '<div class="graph-annotation">';
|
var tooltip = '<div class="graph-annotation">';
|
||||||
var titleStateClass = '';
|
var titleStateClass = '';
|
||||||
|
|
||||||
if (event.source.name === 'panel-alert') {
|
if (event.alertId) {
|
||||||
var stateModel = alertDef.getStateDisplayModel(event.newState);
|
var stateModel = alertDef.getStateDisplayModel(event.newState);
|
||||||
titleStateClass = stateModel.stateClass;
|
titleStateClass = stateModel.stateClass;
|
||||||
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
|
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||||
text = alertDef.getAlertAnnotationInfo(event);
|
text = alertDef.getAlertAnnotationInfo(event);
|
||||||
|
if (event.text) {
|
||||||
|
text = text + '<br />' + event.text;
|
||||||
|
}
|
||||||
|
} else if (title) {
|
||||||
|
text = title + '<br />' + text;
|
||||||
|
title = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += `
|
var header = `<div class="graph-annotation__header">`;
|
||||||
<div class="graph-annotation-header">
|
if (event.login) {
|
||||||
<span class="graph-annotation-title ${titleStateClass}">${sanitizeString(title)}</span>
|
header += `<div class="graph-annotation__user" bs-tooltip="'Created by ${event.login}'"><img src="${event.avatarUrl}" /></div>`;
|
||||||
<span class="graph-annotation-time">${dashboard.formatDate(event.min)}</span>
|
}
|
||||||
</div>
|
header += `
|
||||||
|
<span class="graph-annotation__title ${titleStateClass}">${sanitizeString(title)}</span>
|
||||||
|
<span class="graph-annotation__time">${dashboard.formatDate(event.min)}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tooltip += '<div class="graph-annotation-body">';
|
// Show edit icon only for users with at least Editor role
|
||||||
|
if (event.id && contextSrv.isEditor) {
|
||||||
|
header += `
|
||||||
|
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
|
||||||
|
<i class="fa fa-pencil-square"></i>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
header += `</div>`;
|
||||||
|
tooltip += header;
|
||||||
|
tooltip += '<div class="graph-annotation__body">';
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
tooltip += sanitizeString(text).replace(/\n/g, '<br>') + '<br>';
|
tooltip += '<div>' + sanitizeString(text).replace(/\n/g, '<br>') + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var tags = event.tags;
|
var tags = event.tags;
|
||||||
if (_.isString(event.tags)) {
|
|
||||||
tags = event.tags.split(',');
|
|
||||||
if (tags.length === 1) {
|
|
||||||
tags = event.tags.split(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags && tags.length) {
|
if (tags && tags.length) {
|
||||||
scope.tags = tags;
|
scope.tags = tags;
|
||||||
@@ -65,6 +77,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tooltip += "</div>";
|
tooltip += "</div>";
|
||||||
|
tooltip += '</div>';
|
||||||
|
|
||||||
var $tooltip = $(tooltip);
|
var $tooltip = $(tooltip);
|
||||||
$tooltip.appendTo(element);
|
$tooltip.appendTo(element);
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import './editor_ctrl';
|
import './editor_ctrl';
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
@@ -11,11 +9,7 @@ export class AnnotationsSrv {
|
|||||||
alertStatesPromise: any;
|
alertStatesPromise: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope,
|
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
|
||||||
private $q,
|
|
||||||
private datasourceSrv,
|
|
||||||
private backendSrv,
|
|
||||||
private timeSrv) {
|
|
||||||
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
|
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
|
||||||
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
|
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
|
||||||
}
|
}
|
||||||
@@ -26,64 +20,40 @@ export class AnnotationsSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAnnotations(options) {
|
getAnnotations(options) {
|
||||||
return this.$q.all([
|
return this.$q
|
||||||
this.getGlobalAnnotations(options),
|
.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
||||||
this.getPanelAnnotations(options),
|
.then(results => {
|
||||||
this.getAlertStates(options)
|
// combine the annotations and flatten results
|
||||||
]).then(results => {
|
var annotations = _.flattenDeep(results[0]);
|
||||||
|
|
||||||
// combine the annotations and flatten results
|
// filter out annotations that do not belong to requesting panel
|
||||||
var annotations = _.flattenDeep([results[0], results[1]]);
|
annotations = _.filter(annotations, item => {
|
||||||
|
// if event has panel id and query is of type dashboard then panel and requesting panel id must match
|
||||||
// filter out annotations that do not belong to requesting panel
|
if (item.panelId && item.source.type === 'dashboard') {
|
||||||
annotations = _.filter(annotations, item => {
|
return item.panelId === options.panel.id;
|
||||||
// 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
annotations = dedupAnnotations(annotations);
|
||||||
|
annotations = makeRegions(annotations, options);
|
||||||
|
|
||||||
|
// look for alert state for this panel
|
||||||
|
var alertState = _.find(results[1], {panelId: options.panel.id});
|
||||||
|
|
||||||
|
return {
|
||||||
|
annotations: annotations,
|
||||||
|
alertState: alertState,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (!err.message && err.data && err.data.message) {
|
||||||
|
err.message = err.data.message;
|
||||||
}
|
}
|
||||||
return true;
|
console.log('AnnotationSrv.query error', err);
|
||||||
|
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', err.message || err]);
|
||||||
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// look for alert state for this panel
|
|
||||||
var alertState = _.find(results[2], {panelId: options.panel.id});
|
|
||||||
|
|
||||||
return {
|
|
||||||
annotations: annotations,
|
|
||||||
alertState: alertState,
|
|
||||||
};
|
|
||||||
|
|
||||||
}).catch(err => {
|
|
||||||
if (!err.message && err.data && err.data.message) {
|
|
||||||
err.message = err.data.message;
|
|
||||||
}
|
|
||||||
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', (err.message || err)]);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelAnnotations(options) {
|
|
||||||
var panel = options.panel;
|
|
||||||
var dashboard = options.dashboard;
|
|
||||||
|
|
||||||
if (dashboard.id && panel && panel.alert) {
|
|
||||||
return this.backendSrv.get('/api/annotations', {
|
|
||||||
from: options.range.from.valueOf(),
|
|
||||||
to: options.range.to.valueOf(),
|
|
||||||
limit: 100,
|
|
||||||
panelId: panel.id,
|
|
||||||
dashboardId: dashboard.id,
|
|
||||||
}).then(results => {
|
|
||||||
// this built in annotation source name `panel-alert` is used in annotation tooltip
|
|
||||||
// to know that this annotation is from panel alert
|
|
||||||
return this.translateQueryResult({iconColor: '#AA0000', name: 'panel-alert'}, results);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$q.when([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAlertStates(options) {
|
getAlertStates(options) {
|
||||||
@@ -104,43 +74,55 @@ export class AnnotationsSrv {
|
|||||||
return this.alertStatesPromise;
|
return this.alertStatesPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
|
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {
|
||||||
|
dashboardId: options.dashboard.id,
|
||||||
|
});
|
||||||
return this.alertStatesPromise;
|
return this.alertStatesPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGlobalAnnotations(options) {
|
getGlobalAnnotations(options) {
|
||||||
var dashboard = options.dashboard;
|
var dashboard = options.dashboard;
|
||||||
|
|
||||||
if (dashboard.annotations.list.length === 0) {
|
|
||||||
return this.$q.when([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.globalAnnotationsPromise) {
|
if (this.globalAnnotationsPromise) {
|
||||||
return this.globalAnnotationsPromise;
|
return this.globalAnnotationsPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
var annotations = _.filter(dashboard.annotations.list, {enable: true});
|
|
||||||
var range = this.timeSrv.timeRange();
|
var range = this.timeSrv.timeRange();
|
||||||
|
var promises = [];
|
||||||
|
|
||||||
|
for (let annotation of dashboard.annotations.list) {
|
||||||
|
if (!annotation.enable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => {
|
|
||||||
if (annotation.snapshotData) {
|
if (annotation.snapshotData) {
|
||||||
return this.translateQueryResult(annotation, annotation.snapshotData);
|
return this.translateQueryResult(annotation, annotation.snapshotData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.datasourceSrv.get(annotation.datasource).then(datasource => {
|
promises.push(
|
||||||
// issue query against data source
|
this.datasourceSrv
|
||||||
return datasource.annotationQuery({range: range, rangeRaw: range.raw, annotation: annotation});
|
.get(annotation.datasource)
|
||||||
})
|
.then(datasource => {
|
||||||
.then(results => {
|
// issue query against data source
|
||||||
// store response in annotation object if this is a snapshot call
|
return datasource.annotationQuery({
|
||||||
if (dashboard.snapshot) {
|
range: range,
|
||||||
annotation.snapshotData = angular.copy(results);
|
rangeRaw: range.raw,
|
||||||
}
|
annotation: annotation,
|
||||||
// translate result
|
dashboard: dashboard,
|
||||||
return this.translateQueryResult(annotation, results);
|
});
|
||||||
});
|
})
|
||||||
}));
|
.then(results => {
|
||||||
|
// store response in annotation object if this is a snapshot call
|
||||||
|
if (dashboard.snapshot) {
|
||||||
|
annotation.snapshotData = angular.copy(results);
|
||||||
|
}
|
||||||
|
// translate result
|
||||||
|
return this.translateQueryResult(annotation, results);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.globalAnnotationsPromise = this.$q.all(promises);
|
||||||
return this.globalAnnotationsPromise;
|
return this.globalAnnotationsPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +131,21 @@ export class AnnotationsSrv {
|
|||||||
return this.backendSrv.post('/api/annotations', annotation);
|
return this.backendSrv.post('/api/annotations', annotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAnnotationEvent(annotation) {
|
||||||
|
this.globalAnnotationsPromise = null;
|
||||||
|
return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAnnotationEvent(annotation) {
|
||||||
|
this.globalAnnotationsPromise = null;
|
||||||
|
let deleteUrl = `/api/annotations/${annotation.id}`;
|
||||||
|
if (annotation.isRegion) {
|
||||||
|
deleteUrl = `/api/annotations/region/${annotation.regionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.backendSrv.delete(deleteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
translateQueryResult(annotation, results) {
|
translateQueryResult(annotation, results) {
|
||||||
// if annotation has snapshotData
|
// if annotation has snapshotData
|
||||||
// make clone and remove it
|
// make clone and remove it
|
||||||
@@ -159,13 +156,88 @@ export class AnnotationsSrv {
|
|||||||
|
|
||||||
for (var item of results) {
|
for (var item of results) {
|
||||||
item.source = annotation;
|
item.source = annotation;
|
||||||
item.min = item.time;
|
|
||||||
item.max = item.time;
|
|
||||||
item.scope = 1;
|
|
||||||
item.eventType = annotation.name;
|
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function converts annotation events into set
|
||||||
|
* of single events and regions (event consist of two)
|
||||||
|
* @param annotations
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
function makeRegions(annotations, options) {
|
||||||
|
let [regionEvents, singleEvents] = _.partition(annotations, 'regionId');
|
||||||
|
let regions = getRegions(regionEvents, options.range);
|
||||||
|
annotations = _.concat(regions, singleEvents);
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegions(events, range) {
|
||||||
|
let region_events = _.filter(events, event => {
|
||||||
|
return event.regionId;
|
||||||
|
});
|
||||||
|
let regions = _.groupBy(region_events, 'regionId');
|
||||||
|
regions = _.compact(
|
||||||
|
_.map(regions, region_events => {
|
||||||
|
let region_obj = _.head(region_events);
|
||||||
|
if (region_events && region_events.length > 1) {
|
||||||
|
region_obj.timeEnd = region_events[1].time;
|
||||||
|
region_obj.isRegion = true;
|
||||||
|
return region_obj;
|
||||||
|
} else {
|
||||||
|
if (region_events && region_events.length) {
|
||||||
|
// Don't change proper region object
|
||||||
|
if (!region_obj.time || !region_obj.timeEnd) {
|
||||||
|
// This is cut region
|
||||||
|
if (isStartOfRegion(region_obj)) {
|
||||||
|
region_obj.timeEnd = range.to.valueOf() - 1;
|
||||||
|
} else {
|
||||||
|
// Start time = null
|
||||||
|
region_obj.timeEnd = region_obj.time;
|
||||||
|
region_obj.time = range.from.valueOf() + 1;
|
||||||
|
}
|
||||||
|
region_obj.isRegion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return region_obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStartOfRegion(event): boolean {
|
||||||
|
return event.id && event.id === event.regionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupAnnotations(annotations) {
|
||||||
|
let dedup = [];
|
||||||
|
|
||||||
|
// Split events by annotationId property existance
|
||||||
|
let events = _.partition(annotations, 'id');
|
||||||
|
|
||||||
|
let eventsById = _.groupBy(events[0], 'id');
|
||||||
|
dedup = _.map(eventsById, eventGroup => {
|
||||||
|
if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) {
|
||||||
|
// Get first non-panel alert
|
||||||
|
return _.find(eventGroup, event => {
|
||||||
|
return event.eventType !== 'panel-alert';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _.head(eventGroup);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dedup = _.concat(dedup, events[1]);
|
||||||
|
return dedup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPanelAlert(event) {
|
||||||
|
return event.eventType === 'panel-alert';
|
||||||
|
}
|
||||||
|
|
||||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
coreModule.service('annotationsSrv', AnnotationsSrv);
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
@@ -35,12 +33,6 @@ export class AnnotationsEditorCtrl {
|
|||||||
this.datasources = datasourceSrv.getAnnotationSources();
|
this.datasources = datasourceSrv.getAnnotationSources();
|
||||||
this.annotations = $scope.dashboard.annotations.list;
|
this.annotations = $scope.dashboard.annotations.list;
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
$scope.$watch('mode', newVal => {
|
|
||||||
if (newVal === 'new') {
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasourceChanged() {
|
datasourceChanged() {
|
||||||
@@ -71,6 +63,11 @@ export class AnnotationsEditorCtrl {
|
|||||||
this.$scope.broadcastRefresh();
|
this.$scope.broadcastRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupNew() {
|
||||||
|
this.mode = 'new';
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.annotations.push(this.currentAnnotation);
|
this.annotations.push(this.currentAnnotation);
|
||||||
this.reset();
|
this.reset();
|
||||||
@@ -85,6 +82,14 @@ export class AnnotationsEditorCtrl {
|
|||||||
this.$scope.dashboard.updateSubmenuVisibility();
|
this.$scope.dashboard.updateSubmenuVisibility();
|
||||||
this.$scope.broadcastRefresh();
|
this.$scope.broadcastRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
annotationEnabledChange() {
|
||||||
|
this.$scope.broadcastRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationHiddenChanged() {
|
||||||
|
this.$scope.dashboard.updateSubmenuVisibility();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl);
|
coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl);
|
||||||
|
@@ -2,9 +2,11 @@
|
|||||||
export class AnnotationEvent {
|
export class AnnotationEvent {
|
||||||
dashboardId: number;
|
dashboardId: number;
|
||||||
panelId: number;
|
panelId: number;
|
||||||
|
userId: number;
|
||||||
time: any;
|
time: any;
|
||||||
timeEnd: any;
|
timeEnd: any;
|
||||||
isRegion: boolean;
|
isRegion: boolean;
|
||||||
title: string;
|
|
||||||
text: string;
|
text: string;
|
||||||
|
type: string;
|
||||||
|
tags: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import {coreModule} from 'app/core/core';
|
import {coreModule} from 'app/core/core';
|
||||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||||
import {AnnotationEvent} from './event';
|
import {AnnotationEvent} from './event';
|
||||||
@@ -11,11 +10,20 @@ export class EventEditorCtrl {
|
|||||||
timeRange: {from: number, to: number};
|
timeRange: {from: number, to: number};
|
||||||
form: any;
|
form: any;
|
||||||
close: any;
|
close: any;
|
||||||
|
timeFormated: string;
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
constructor(private annotationsSrv) {
|
constructor(private annotationsSrv) {
|
||||||
this.event.panelId = this.panelCtrl.panel.id;
|
this.event.panelId = this.panelCtrl.panel.id;
|
||||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||||
|
|
||||||
|
// Annotations query returns time as Unix timestamp in milliseconds
|
||||||
|
this.event.time = tryEpochToMoment(this.event.time);
|
||||||
|
if (this.event.isRegion) {
|
||||||
|
this.event.timeEnd = tryEpochToMoment(this.event.timeEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
@@ -28,7 +36,7 @@ export class EventEditorCtrl {
|
|||||||
saveModel.timeEnd = 0;
|
saveModel.timeEnd = 0;
|
||||||
|
|
||||||
if (saveModel.isRegion) {
|
if (saveModel.isRegion) {
|
||||||
saveModel.timeEnd = saveModel.timeEnd.valueOf();
|
saveModel.timeEnd = this.event.timeEnd.valueOf();
|
||||||
|
|
||||||
if (saveModel.timeEnd < saveModel.time) {
|
if (saveModel.timeEnd < saveModel.time) {
|
||||||
console.log('invalid time');
|
console.log('invalid time');
|
||||||
@@ -36,14 +44,48 @@ export class EventEditorCtrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.annotationsSrv.saveAnnotationEvent(saveModel).then(() => {
|
if (saveModel.id) {
|
||||||
|
this.annotationsSrv.updateAnnotationEvent(saveModel)
|
||||||
|
.then(() => {
|
||||||
|
this.panelCtrl.refresh();
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.panelCtrl.refresh();
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.annotationsSrv.saveAnnotationEvent(saveModel)
|
||||||
|
.then(() => {
|
||||||
|
this.panelCtrl.refresh();
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.panelCtrl.refresh();
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
return this.annotationsSrv.deleteAnnotationEvent(this.event)
|
||||||
|
.then(() => {
|
||||||
|
this.panelCtrl.refresh();
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
this.panelCtrl.refresh();
|
this.panelCtrl.refresh();
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timeChanged() {
|
function tryEpochToMoment(timestamp) {
|
||||||
this.panelCtrl.render();
|
if (timestamp && _.isNumber(timestamp)) {
|
||||||
|
let epoch = Number(timestamp);
|
||||||
|
return moment(epoch);
|
||||||
|
} else {
|
||||||
|
return timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,25 +3,30 @@ import moment from 'moment';
|
|||||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||||
import {AnnotationEvent} from './event';
|
import {AnnotationEvent} from './event';
|
||||||
|
|
||||||
|
const OK_COLOR = "rgba(11, 237, 50, 1)",
|
||||||
|
ALERTING_COLOR = "rgba(237, 46, 24, 1)",
|
||||||
|
NO_DATA_COLOR = "rgba(150, 150, 150, 1)";
|
||||||
|
|
||||||
|
|
||||||
export class EventManager {
|
export class EventManager {
|
||||||
event: AnnotationEvent;
|
event: AnnotationEvent;
|
||||||
|
editorOpen: boolean;
|
||||||
|
|
||||||
constructor(private panelCtrl: MetricsPanelCtrl, private elem, private popoverSrv) {
|
constructor(private panelCtrl: MetricsPanelCtrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
editorClosed() {
|
editorClosed() {
|
||||||
console.log('editorClosed');
|
|
||||||
this.event = null;
|
this.event = null;
|
||||||
|
this.editorOpen = false;
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTime(range) {
|
editorOpened() {
|
||||||
let newEvent = true;
|
this.editorOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.event) {
|
updateTime(range) {
|
||||||
newEvent = false;
|
if (!this.event) {
|
||||||
} else {
|
|
||||||
// init new event
|
|
||||||
this.event = new AnnotationEvent();
|
this.event = new AnnotationEvent();
|
||||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||||
this.event.panelId = this.panelCtrl.panel.id;
|
this.event.panelId = this.panelCtrl.panel.id;
|
||||||
@@ -35,25 +40,11 @@ export class EventManager {
|
|||||||
this.event.isRegion = true;
|
this.event.isRegion = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// newEvent means the editor is not visible
|
this.panelCtrl.render();
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
editEvent(event, elem?) {
|
||||||
|
this.event = event;
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,35 +55,54 @@ export class EventManager {
|
|||||||
|
|
||||||
var types = {
|
var types = {
|
||||||
'$__alerting': {
|
'$__alerting': {
|
||||||
color: 'rgba(237, 46, 24, 1)',
|
color: ALERTING_COLOR,
|
||||||
position: 'BOTTOM',
|
position: 'BOTTOM',
|
||||||
markerSize: 5,
|
markerSize: 5,
|
||||||
},
|
},
|
||||||
'$__ok': {
|
'$__ok': {
|
||||||
color: 'rgba(11, 237, 50, 1)',
|
color: OK_COLOR,
|
||||||
position: 'BOTTOM',
|
position: 'BOTTOM',
|
||||||
markerSize: 5,
|
markerSize: 5,
|
||||||
},
|
},
|
||||||
'$__no_data': {
|
'$__no_data': {
|
||||||
color: 'rgba(150, 150, 150, 1)',
|
color: NO_DATA_COLOR,
|
||||||
position: 'BOTTOM',
|
position: 'BOTTOM',
|
||||||
markerSize: 5,
|
markerSize: 5,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.event) {
|
if (this.event) {
|
||||||
annotations = [
|
if (this.event.isRegion) {
|
||||||
{
|
annotations = [
|
||||||
min: this.event.time.valueOf(),
|
{
|
||||||
title: this.event.title,
|
isRegion: true,
|
||||||
text: this.event.text,
|
min: this.event.time.valueOf(),
|
||||||
eventType: '$__alerting',
|
timeEnd: this.event.timeEnd.valueOf(),
|
||||||
}
|
text: this.event.text,
|
||||||
];
|
eventType: '$__alerting',
|
||||||
|
editModel: this.event,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
annotations = [
|
||||||
|
{
|
||||||
|
min: this.event.time.valueOf(),
|
||||||
|
text: this.event.text,
|
||||||
|
editModel: this.event,
|
||||||
|
eventType: '$__alerting',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// annotations from query
|
// annotations from query
|
||||||
for (var i = 0; i < annotations.length; i++) {
|
for (var i = 0; i < annotations.length; i++) {
|
||||||
var item = annotations[i];
|
var item = annotations[i];
|
||||||
|
|
||||||
|
// add properties used by jquery flot events
|
||||||
|
item.min = item.time;
|
||||||
|
item.max = item.time;
|
||||||
|
item.eventType = item.source.name;
|
||||||
|
|
||||||
if (item.newState) {
|
if (item.newState) {
|
||||||
item.eventType = '$__' + item.newState;
|
item.eventType = '$__' + item.newState;
|
||||||
continue;
|
continue;
|
||||||
@@ -108,10 +118,69 @@ export class EventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let regions = getRegions(annotations);
|
||||||
|
addRegionMarking(regions, flotOptions);
|
||||||
|
|
||||||
|
let eventSectionHeight = 20;
|
||||||
|
let eventSectionMargin = 7;
|
||||||
|
flotOptions.grid.eventSectionHeight = eventSectionMargin;
|
||||||
|
flotOptions.xaxis.eventSectionHeight = eventSectionHeight;
|
||||||
|
|
||||||
flotOptions.events = {
|
flotOptions.events = {
|
||||||
levels: _.keys(types).length + 1,
|
levels: _.keys(types).length + 1,
|
||||||
data: annotations,
|
data: annotations,
|
||||||
types: types,
|
types: types,
|
||||||
|
manager: this
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRegions(events) {
|
||||||
|
return _.filter(events, 'isRegion');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRegionMarking(regions, flotOptions) {
|
||||||
|
let markings = flotOptions.grid.markings;
|
||||||
|
let defaultColor = 'rgb(237, 46, 24)';
|
||||||
|
let fillColor;
|
||||||
|
|
||||||
|
_.each(regions, region => {
|
||||||
|
if (region.source) {
|
||||||
|
fillColor = region.source.iconColor || defaultColor;
|
||||||
|
} else {
|
||||||
|
fillColor = defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert #FFFFFF to rgb(255, 255, 255)
|
||||||
|
// because panels with alerting use this format
|
||||||
|
let hexPattern = /^#[\da-fA-f]{3,6}/;
|
||||||
|
if (hexPattern.test(fillColor)) {
|
||||||
|
fillColor = convertToRGB(fillColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillColor = addAlphaToRGB(fillColor, 0.090);
|
||||||
|
markings.push({ xaxis: { from: region.min, to: region.timeEnd }, color: fillColor });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAlphaToRGB(rgb: string, alpha: number): string {
|
||||||
|
let rgbPattern = /^rgb\(/;
|
||||||
|
if (rgbPattern.test(rgb)) {
|
||||||
|
return rgb.replace(')', `, ${alpha})`).replace('rgb', 'rgba');
|
||||||
|
} else {
|
||||||
|
return rgb.replace(/[\d\.]+\)/, `${alpha})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToRGB(hex: string): string {
|
||||||
|
let hexPattern = /#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/g;
|
||||||
|
let match = hexPattern.exec(hex);
|
||||||
|
if (match) {
|
||||||
|
let rgb = _.map(match.slice(1), hex_val => {
|
||||||
|
return parseInt(hex_val, 16);
|
||||||
|
});
|
||||||
|
return 'rgb(' + rgb.join(',') + ')';
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -40,10 +40,11 @@
|
|||||||
Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons
|
Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons
|
||||||
on all graph panels. When you hover over an annotation icon you can get title, tags, and text information for the event.
|
on all graph panels. When you hover over an annotation icon you can get title, tags, and text information for the event.
|
||||||
In the <i>Queries</i> tab you can add queries that return annotation events.
|
In the <i>Queries</i> tab you can add queries that return annotation events.
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
You can add annotations directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.
|
||||||
|
</p>
|
||||||
|
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,13 +54,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="grafana-options-table">
|
<table class="grafana-options-table">
|
||||||
<tr ng-repeat="annotation in ctrl.annotations">
|
<tr ng-repeat="annotation in ctrl.annotations">
|
||||||
<td style="width:90%">
|
<td style="width:90%" ng-hide="annotation.builtIn">
|
||||||
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
|
<i class="fa fa-comment" style="color:{{annotation.iconColor}}"></i>
|
||||||
{{annotation.name}}
|
{{annotation.name}}
|
||||||
</td>
|
</td>
|
||||||
|
<td style="width:90%" ng-show="annotation.builtIn">
|
||||||
|
<i class="fa fa-comment"></i>
|
||||||
|
<em class="muted">{{annotation.name}} (Built-in)</em>
|
||||||
|
</td>
|
||||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||||
|
|
||||||
<td style="width: 1%">
|
<td style="width: 1%">
|
||||||
<a ng-click="ctrl.edit(annotation)" class="btn btn-inverse btn-mini">
|
<a ng-click="ctrl.edit(annotation)" class="btn btn-inverse btn-mini">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
@@ -67,7 +71,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 1%">
|
<td style="width: 1%">
|
||||||
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini">
|
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini" ng-hide="annotation.builtIn">
|
||||||
<i class="fa fa-remove"></i>
|
<i class="fa fa-remove"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -77,60 +81,63 @@
|
|||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.mode === 'list'">
|
<div class="gf-form" ng-show="ctrl.mode === 'list'">
|
||||||
<div class="gf-form-button-row">
|
<div class="gf-form-button-row">
|
||||||
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.mode = 'new';"><i class="fa fa-plus" ></i> New</a>
|
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.setupNew()"><i class="fa fa-plus" ></i> New</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||||
<div class="gf-form-group">
|
<div>
|
||||||
<h5 class="section-heading">Options</h5>
|
<div class="gf-form-group">
|
||||||
|
<h5 class="section-heading">General</h5>
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-9">Name</span>
|
<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>
|
<input type="text" class="gf-form-input width-20" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-9">Data source</span>
|
<span class="gf-form-label width-7">Data source</span>
|
||||||
<div class="gf-form-select-wrapper width-12">
|
<div class="gf-form-select-wrapper">
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form-group">
|
</div>
|
||||||
<div class="gf-form-inline">
|
|
||||||
<!-- <div class="gf-form"> -->
|
<div class="gf-form-group">
|
||||||
<!-- <span class="gf-form-label width-7">Show in</span> -->
|
<div class="gf-form-inline">
|
||||||
<!-- <div class="gf-form-select-wrapper width-12"> -->
|
<gf-form-switch class="gf-form"
|
||||||
<!-- <select class="gf-form-input" ng-model="ctrl.currentAnnotation.showIn" ng-options="f.value as f.text for f in ctrl.showOptions"></select> -->
|
label="Enabled"
|
||||||
<!-- </div> -->
|
checked="ctrl.currentAnnotation.enable"
|
||||||
<!-- </div> -->
|
on-change="ctrl.annotationEnabledChange()"
|
||||||
<gf-form-switch class="gf-form"
|
label-class="width-7">
|
||||||
label="Hide toggle"
|
</gf-form-switch>
|
||||||
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
<gf-form-switch class="gf-form"
|
||||||
checked="ctrl.currentAnnotation.hide"
|
label="Hidden"
|
||||||
label-class="width-9">
|
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
||||||
</gf-form-switch>
|
checked="ctrl.currentAnnotation.hide"
|
||||||
</div>
|
on-change="ctrl.annotationHiddenChanged()"
|
||||||
|
label-class="width-7">
|
||||||
|
</gf-form-switch>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Color</label>
|
<label class="gf-form-label">Color</label>
|
||||||
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="section-heading">Query</h5>
|
<h5 class="section-heading">Query</h5>
|
||||||
<rebuild-on-change property="ctrl.currentDatasource">
|
<rebuild-on-change property="ctrl.currentDatasource">
|
||||||
<plugin-component type="annotations-query-ctrl">
|
<plugin-component type="annotations-query-ctrl">
|
||||||
</plugin-component>
|
</plugin-component>
|
||||||
</rebuild-on-change>
|
</rebuild-on-change>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<div class="gf-form-button-row p-y-0">
|
<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 === '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>
|
<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>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,38 +1,35 @@
|
|||||||
|
|
||||||
<h5 class="section-heading text-center">Add annotation</h5>
|
<div class="graph-annotation">
|
||||||
|
<div class="graph-annotation__header">
|
||||||
<form name="ctrl.form" class="text-center">
|
<div class="graph-annotation__user" bs-tooltip="'Created by {{ctrl.login}}'">
|
||||||
<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>
|
</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">
|
<div class="graph-annotation__title">
|
||||||
<button type="submit" class="btn gf-form-btn btn-success" ng-click="ctrl.save()">Save</button>
|
<span ng-if="!ctrl.event.id">Add Annotation</span>
|
||||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
<span ng-if="ctrl.event.id">Edit Annotation</span>
|
||||||
</div>
|
</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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@@ -71,10 +71,35 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addBuiltInAnnotationQuery();
|
||||||
this.updateSchema(data);
|
this.updateSchema(data);
|
||||||
this.initMeta(meta);
|
this.initMeta(meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addBuiltInAnnotationQuery() {
|
||||||
|
let found = false;
|
||||||
|
for (let item of this.annotations.list) {
|
||||||
|
if (item.builtIn === 1) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.annotations.list.unshift({
|
||||||
|
datasource: '-- Grafana --',
|
||||||
|
name: 'Annotations & Alerts',
|
||||||
|
type: 'dashboard',
|
||||||
|
iconColor: 'rgb(0, 211, 255)',
|
||||||
|
enable: true,
|
||||||
|
hide: true,
|
||||||
|
builtIn: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private initMeta(meta) {
|
private initMeta(meta) {
|
||||||
meta = meta || {};
|
meta = meta || {};
|
||||||
|
|
||||||
|
@@ -46,8 +46,8 @@ describe('DashboardModel', function() {
|
|||||||
var saveModel = model.getSaveModelClone();
|
var saveModel = model.getSaveModelClone();
|
||||||
var keys = _.keys(saveModel);
|
var keys = _.keys(saveModel);
|
||||||
|
|
||||||
expect(keys[0]).to.be('addEmptyRow');
|
expect(keys[0]).to.be('addBuiltInAnnotationQuery');
|
||||||
expect(keys[1]).to.be('addPanel');
|
expect(keys[1]).to.be('addEmptyRow');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,26 +220,6 @@ describe('DashboardModel', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
|
||||||
var model;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
model = new DashboardModel({
|
|
||||||
annotations: {
|
|
||||||
enable: true,
|
|
||||||
},
|
|
||||||
templating: {
|
|
||||||
enable: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add empty list', function() {
|
|
||||||
expect(model.annotations.list.length).to.be(0);
|
|
||||||
expect(model.templating.list.length).to.be(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Given editable false dashboard', function() {
|
describe('Given editable false dashboard', function() {
|
||||||
var model;
|
var model;
|
||||||
|
|
||||||
@@ -339,7 +319,12 @@ describe('DashboardModel', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add empty list', function() {
|
it('should add empty list', function() {
|
||||||
expect(model.annotations.list.length).to.be(0);
|
expect(model.annotations.list.length).to.be(1);
|
||||||
|
expect(model.templating.list.length).to.be(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add builtin annotation query', function() {
|
||||||
|
expect(model.annotations.list[0].builtIn).to.be(1);
|
||||||
expect(model.templating.list.length).to.be(0);
|
expect(model.templating.list.length).to.be(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -80,6 +80,10 @@ describe('given dashboard with repeated panels', function() {
|
|||||||
name: 'mixed',
|
name: 'mixed',
|
||||||
meta: {id: "mixed", info: {version: "1.2.1"}, name: "Mixed", builtIn: true}
|
meta: {id: "mixed", info: {version: "1.2.1"}, name: "Mixed", builtIn: true}
|
||||||
}));
|
}));
|
||||||
|
datasourceSrvStub.get.withArgs('-- Grafana --').returns(Promise.resolve({
|
||||||
|
name: '-- Grafana --',
|
||||||
|
meta: {id: "grafana", info: {version: "1.2.1"}, name: "grafana", builtIn: true}
|
||||||
|
}));
|
||||||
|
|
||||||
config.panels['graph'] = {
|
config.panels['graph'] = {
|
||||||
id: "graph",
|
id: "graph",
|
||||||
@@ -116,7 +120,7 @@ describe('given dashboard with repeated panels', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should replace datasource in annotation query', function() {
|
it('should replace datasource in annotation query', function() {
|
||||||
expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}");
|
expect(exported.annotations.list[1].datasource).to.be("${DS_GFDB}");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add datasource as input', function() {
|
it('should add datasource as input', function() {
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
|
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
|
||||||
<div ng-repeat="annotation in ctrl.dashboard.annotations.list" ng-hide="annotation.hide" 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>
|
<gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -59,9 +59,13 @@ var template = `
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Home Dashboard</span>
|
<span class="gf-form-label width-10">
|
||||||
<dashboard-selector class="gf-form-select-wrapper max-width-20 gf-form-select-wrapper--has-help-icon"
|
Home Dashboard
|
||||||
model="ctrl.prefs.homeDashboardId">
|
<info-popover mode="right-normal">
|
||||||
|
Not finding dashboard you want? Star it first, then it should appear in this select box.
|
||||||
|
</info-popover>
|
||||||
|
</span>
|
||||||
|
<dashboard-selector class="gf-form-select-wrapper max-width-20" model="ctrl.prefs.homeDashboardId">
|
||||||
</dashboard-selector>
|
</dashboard-selector>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
6
public/app/headers/common.d.ts
vendored
6
public/app/headers/common.d.ts
vendored
@@ -4,9 +4,3 @@ declare module 'eventemitter3' {
|
|||||||
var config: any;
|
var config: any;
|
||||||
export default config;
|
export default config;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'd3' {
|
|
||||||
var d3: any;
|
|
||||||
export default d3;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@@ -83,7 +83,6 @@ export class ElasticDatasource {
|
|||||||
var timeField = annotation.timeField || '@timestamp';
|
var timeField = annotation.timeField || '@timestamp';
|
||||||
var queryString = annotation.query || '*';
|
var queryString = annotation.query || '*';
|
||||||
var tagsField = annotation.tagsField || 'tags';
|
var tagsField = annotation.tagsField || 'tags';
|
||||||
var titleField = annotation.titleField || 'desc';
|
|
||||||
var textField = annotation.textField || null;
|
var textField = annotation.textField || null;
|
||||||
|
|
||||||
var range = {};
|
var range = {};
|
||||||
@@ -146,9 +145,6 @@ export class ElasticDatasource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.isArray(fieldValue)) {
|
|
||||||
fieldValue = fieldValue.join(', ');
|
|
||||||
}
|
|
||||||
return fieldValue;
|
return fieldValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,16 +161,27 @@ export class ElasticDatasource {
|
|||||||
var event = {
|
var event = {
|
||||||
annotation: annotation,
|
annotation: annotation,
|
||||||
time: moment.utc(time).valueOf(),
|
time: moment.utc(time).valueOf(),
|
||||||
title: getFieldFromSource(source, titleField),
|
text: getFieldFromSource(source, textField),
|
||||||
tags: getFieldFromSource(source, tagsField),
|
tags: getFieldFromSource(source, tagsField),
|
||||||
text: getFieldFromSource(source, textField)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// legacy support for title tield
|
||||||
|
if (annotation.titleField) {
|
||||||
|
const title = getFieldFromSource(source, annotation.titleField);
|
||||||
|
if (title) {
|
||||||
|
event.text = title + '\n' + event.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof event.tags === 'string') {
|
||||||
|
event.tags = event.tags.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
list.push(event);
|
list.push(event);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource() {
|
||||||
this.timeSrv.setTime({ from: 'now-1m', to: 'now' }, true);
|
this.timeSrv.setTime({ from: 'now-1m', to: 'now' }, true);
|
||||||
@@ -242,7 +249,7 @@ export class ElasticDatasource {
|
|||||||
return this.post('_msearch', payload).then(function(res) {
|
return this.post('_msearch', payload).then(function(res) {
|
||||||
return new ElasticResponse(sentTargets, res).getTimeSeries();
|
return new ElasticResponse(sentTargets, res).getTimeSeries();
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
getFields(query) {
|
getFields(query) {
|
||||||
return this.get('/_mapping').then(function(result) {
|
return this.get('/_mapping').then(function(result) {
|
||||||
|
@@ -18,7 +18,7 @@ export class IndexPattern {
|
|||||||
} else {
|
} else {
|
||||||
return this.pattern;
|
return this.pattern;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
getIndexList(from, to) {
|
getIndexList(from, to) {
|
||||||
if (!this.interval) {
|
if (!this.interval) {
|
||||||
|
@@ -15,24 +15,20 @@
|
|||||||
<h6>Field mappings</h6>
|
<h6>Field mappings</h6>
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Time</span>
|
<span class="gf-form-label">Time</span>
|
||||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
|
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Title</span>
|
<span class="gf-form-label">Text</span>
|
||||||
|
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.textField' placeholder=""></input>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label">Tags</span>
|
||||||
|
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form" ng-show="ctrl.annotation.titleField">
|
||||||
|
<span class="gf-form-label">Title <em class="muted">(depricated)</em></span>
|
||||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
|
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<span class="gf-form-label width-10">Tags</span>
|
|
||||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
|
||||||
<span class="gf-form-label width-10">Text</span>
|
|
||||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.textField' placeholder=""></input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -167,7 +167,7 @@ export class ElasticQueryBuilder {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
build(target, adhocFilters?, queryString?) {
|
build(target, adhocFilters?, queryString?) {
|
||||||
// make sure query has defaults;
|
// make sure query has defaults;
|
||||||
|
@@ -188,4 +188,4 @@ export function describeOrderBy(orderBy, target) {
|
|||||||
} else {
|
} else {
|
||||||
return "metric not found";
|
return "metric not found";
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
class GrafanaDatasource {
|
class GrafanaDatasource {
|
||||||
@@ -8,42 +6,62 @@ class GrafanaDatasource {
|
|||||||
constructor(private backendSrv, private $q) {}
|
constructor(private backendSrv, private $q) {}
|
||||||
|
|
||||||
query(options) {
|
query(options) {
|
||||||
return this.backendSrv.get('/api/tsdb/testdata/random-walk', {
|
return this.backendSrv
|
||||||
from: options.range.from.valueOf(),
|
.get('/api/tsdb/testdata/random-walk', {
|
||||||
to: options.range.to.valueOf(),
|
from: options.range.from.valueOf(),
|
||||||
intervalMs: options.intervalMs,
|
to: options.range.to.valueOf(),
|
||||||
maxDataPoints: options.maxDataPoints,
|
intervalMs: options.intervalMs,
|
||||||
}).then(res => {
|
maxDataPoints: options.maxDataPoints,
|
||||||
var data = [];
|
})
|
||||||
|
.then(res => {
|
||||||
|
var data = [];
|
||||||
|
|
||||||
if (res.results) {
|
if (res.results) {
|
||||||
_.forEach(res.results, queryRes => {
|
_.forEach(res.results, queryRes => {
|
||||||
for (let series of queryRes.series) {
|
for (let series of queryRes.series) {
|
||||||
data.push({
|
data.push({
|
||||||
target: series.name,
|
target: series.name,
|
||||||
datapoints: series.points
|
datapoints: series.points,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {data: data};
|
return {data: data};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
metricFindQuery(options) {
|
metricFindQuery(options) {
|
||||||
return this.$q.when({data: []});
|
return this.$q.when({data: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
annotationQuery(options) {
|
annotationQuery(options) {
|
||||||
return this.backendSrv.get('/api/annotations', {
|
const params: any = {
|
||||||
from: options.range.from.valueOf(),
|
from: options.range.from.valueOf(),
|
||||||
to: options.range.to.valueOf(),
|
to: options.range.to.valueOf(),
|
||||||
limit: options.limit,
|
limit: options.annotation.limit,
|
||||||
type: options.type,
|
tags: options.annotation.tags,
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
if (options.annotation.type === 'dashboard') {
|
||||||
|
// if no dashboard id yet return
|
||||||
|
if (!options.dashboard.id) {
|
||||||
|
return this.$q.when([]);
|
||||||
|
}
|
||||||
|
// filter by dashboard id
|
||||||
|
params.dashboardId = options.dashboard.id;
|
||||||
|
// remove tags filter if any
|
||||||
|
delete params.tags;
|
||||||
|
} else {
|
||||||
|
// require at least one tag
|
||||||
|
if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) {
|
||||||
|
return this.$q.when([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.backendSrv.get('/api/annotations', params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {GrafanaDatasource};
|
export {GrafanaDatasource};
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import {GrafanaDatasource} from './datasource';
|
import {GrafanaDatasource} from './datasource';
|
||||||
import {QueryCtrl} from 'app/plugins/sdk';
|
import {QueryCtrl} from 'app/plugins/sdk';
|
||||||
|
|
||||||
@@ -10,19 +8,22 @@ class GrafanaQueryCtrl extends QueryCtrl {
|
|||||||
class GrafanaAnnotationsQueryCtrl {
|
class GrafanaAnnotationsQueryCtrl {
|
||||||
annotation: any;
|
annotation: any;
|
||||||
|
|
||||||
|
types = [
|
||||||
|
{text: 'Dashboard', value: 'dashboard'},
|
||||||
|
{text: 'Tags', value: 'tags'}
|
||||||
|
];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.annotation.type = this.annotation.type || 'alert';
|
this.annotation.type = this.annotation.type || 'tags';
|
||||||
this.annotation.limit = this.annotation.limit || 100;
|
this.annotation.limit = this.annotation.limit || 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
static templateUrl = 'partials/annotations.editor.html';
|
static templateUrl = 'partials/annotations.editor.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
GrafanaDatasource,
|
GrafanaDatasource,
|
||||||
GrafanaDatasource as Datasource,
|
GrafanaDatasource as Datasource,
|
||||||
GrafanaQueryCtrl as QueryCtrl,
|
GrafanaQueryCtrl as QueryCtrl,
|
||||||
GrafanaAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
GrafanaAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,14 +2,29 @@
|
|||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-7">Type</span>
|
<span class="gf-form-label width-8">
|
||||||
<div class="gf-form-select-wrapper">
|
Filter by
|
||||||
<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'}]">
|
<info-popover mode="right-normal">
|
||||||
|
<ul>
|
||||||
|
<li>Dashboard: This will fetch annotation and alert state changes for whole dashboard and show them only on the event's originating panel.</li>
|
||||||
|
<li>All: This will fetch any annotation events that match the tags filter.</li>
|
||||||
|
</ul>
|
||||||
|
</info-popover>
|
||||||
|
</span>
|
||||||
|
<div class="gf-form-select-wrapper width-8">
|
||||||
|
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in ctrl.types">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
|
||||||
|
<span class="gf-form-label">Tags</span>
|
||||||
|
<bootstrap-tagsinput ng-model="ctrl.annotation.tags" tagclass="label label-tag" placeholder="add tags">
|
||||||
|
</bootstrap-tagsinput>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-7">Max limit</span>
|
<span class="gf-form-label">Max limit</span>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]">
|
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]">
|
||||||
</select>
|
</select>
|
||||||
@@ -17,3 +32,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -68,6 +68,18 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.parseTags = function(tagString) {
|
||||||
|
let tags = [];
|
||||||
|
tags = tagString.split(',');
|
||||||
|
if (tags.length === 1) {
|
||||||
|
tags = tagString.split(' ');
|
||||||
|
if (tags[0] === '') {
|
||||||
|
tags = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
|
};
|
||||||
|
|
||||||
this.annotationQuery = function(options) {
|
this.annotationQuery = function(options) {
|
||||||
// Graphite metric as annotation
|
// Graphite metric as annotation
|
||||||
if (options.annotation.target) {
|
if (options.annotation.target) {
|
||||||
@@ -102,19 +114,25 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
|||||||
} else {
|
} else {
|
||||||
// Graphite event as annotation
|
// Graphite event as annotation
|
||||||
var tags = templateSrv.replace(options.annotation.tags);
|
var tags = templateSrv.replace(options.annotation.tags);
|
||||||
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
|
return this.events({range: options.rangeRaw, tags: tags}).then(results => {
|
||||||
var list = [];
|
var list = [];
|
||||||
for (var i = 0; i < results.data.length; i++) {
|
for (var i = 0; i < results.data.length; i++) {
|
||||||
var e = results.data[i];
|
var e = results.data[i];
|
||||||
|
|
||||||
|
var tags = e.tags;
|
||||||
|
if (_.isString(e.tags)) {
|
||||||
|
tags = this.parseTags(e.tags);
|
||||||
|
}
|
||||||
|
|
||||||
list.push({
|
list.push({
|
||||||
annotation: options.annotation,
|
annotation: options.annotation,
|
||||||
time: e.when * 1000,
|
time: e.when * 1000,
|
||||||
title: e.what,
|
title: e.what,
|
||||||
tags: e.tags,
|
tags: tags,
|
||||||
text: e.data
|
text: e.data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -126,7 +144,6 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
|||||||
if (options.tags) {
|
if (options.tags) {
|
||||||
tags = '&tags=' + options.tags;
|
tags = '&tags=' + options.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.doGraphiteRequest({
|
return this.doGraphiteRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
|
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
|
||||||
|
@@ -2,10 +2,12 @@
|
|||||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||||
import helpers from 'test/specs/helpers';
|
import helpers from 'test/specs/helpers';
|
||||||
import {GraphiteDatasource} from "../datasource";
|
import {GraphiteDatasource} from "../datasource";
|
||||||
|
import moment from 'moment';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
describe('graphiteDatasource', function() {
|
describe('graphiteDatasource', function() {
|
||||||
var ctx = new helpers.ServiceTestContext();
|
let ctx = new helpers.ServiceTestContext();
|
||||||
var instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}};
|
let instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}};
|
||||||
|
|
||||||
beforeEach(angularMocks.module('grafana.core'));
|
beforeEach(angularMocks.module('grafana.core'));
|
||||||
beforeEach(angularMocks.module('grafana.services'));
|
beforeEach(angularMocks.module('grafana.services'));
|
||||||
@@ -22,16 +24,16 @@ describe('graphiteDatasource', function() {
|
|||||||
ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
|
ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When querying influxdb with one target using query editor target spec', function() {
|
describe('When querying graphite with one target using query editor target spec', function() {
|
||||||
var query = {
|
let query = {
|
||||||
panelId: 3,
|
panelId: 3,
|
||||||
rangeRaw: { from: 'now-1h', to: 'now' },
|
rangeRaw: { from: 'now-1h', to: 'now' },
|
||||||
targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}],
|
targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}],
|
||||||
maxDataPoints: 500,
|
maxDataPoints: 500,
|
||||||
};
|
};
|
||||||
|
|
||||||
var results;
|
let results;
|
||||||
var requestOptions;
|
let requestOptions;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.backendSrv.datasourceRequest = function(options) {
|
ctx.backendSrv.datasourceRequest = function(options) {
|
||||||
@@ -52,7 +54,7 @@ describe('graphiteDatasource', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should query correctly', function() {
|
it('should query correctly', function() {
|
||||||
var params = requestOptions.data.split('&');
|
let params = requestOptions.data.split('&');
|
||||||
expect(params).to.contain('target=prod1.count');
|
expect(params).to.contain('target=prod1.count');
|
||||||
expect(params).to.contain('target=prod2.count');
|
expect(params).to.contain('target=prod2.count');
|
||||||
expect(params).to.contain('from=-1h');
|
expect(params).to.contain('from=-1h');
|
||||||
@@ -60,7 +62,7 @@ describe('graphiteDatasource', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should exclude undefined params', function() {
|
it('should exclude undefined params', function() {
|
||||||
var params = requestOptions.data.split('&');
|
let params = requestOptions.data.split('&');
|
||||||
expect(params).to.not.contain('cacheTimeout=undefined');
|
expect(params).to.not.contain('cacheTimeout=undefined');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,58 +77,130 @@ describe('graphiteDatasource', function() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when fetching Graphite Events as annotations', () => {
|
||||||
|
let results;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
annotation: {
|
||||||
|
tags: 'tag1'
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
from: moment(1432288354),
|
||||||
|
to: moment(1432288401)
|
||||||
|
},
|
||||||
|
rangeRaw: {from: "now-24h", to: "now"}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('and tags are returned as string', () => {
|
||||||
|
const response = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
when: 1507222850,
|
||||||
|
tags: 'tag1 tag2',
|
||||||
|
data: 'some text',
|
||||||
|
id: 2,
|
||||||
|
what: 'Event - deploy'
|
||||||
|
}
|
||||||
|
]};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx.backendSrv.datasourceRequest = function(options) {
|
||||||
|
return ctx.$q.when(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds.annotationQuery(options).then(function(data) { results = data; });
|
||||||
|
ctx.$rootScope.$apply();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse the tags string into an array', () => {
|
||||||
|
expect(_.isArray(results[0].tags)).to.eql(true);
|
||||||
|
expect(results[0].tags.length).to.eql(2);
|
||||||
|
expect(results[0].tags[0]).to.eql('tag1');
|
||||||
|
expect(results[0].tags[1]).to.eql('tag2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and tags are returned as an array', () => {
|
||||||
|
const response = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
when: 1507222850,
|
||||||
|
tags: ['tag1', 'tag2'],
|
||||||
|
data: 'some text',
|
||||||
|
id: 2,
|
||||||
|
what: 'Event - deploy'
|
||||||
|
}
|
||||||
|
]};
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx.backendSrv.datasourceRequest = function(options) {
|
||||||
|
return ctx.$q.when(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds.annotationQuery(options).then(function(data) { results = data; });
|
||||||
|
ctx.$rootScope.$apply();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse the tags string into an array', () => {
|
||||||
|
expect(_.isArray(results[0].tags)).to.eql(true);
|
||||||
|
expect(results[0].tags.length).to.eql(2);
|
||||||
|
expect(results[0].tags[0]).to.eql('tag1');
|
||||||
|
expect(results[0].tags[1]).to.eql('tag2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('building graphite params', function() {
|
describe('building graphite params', function() {
|
||||||
it('should return empty array if no targets', function() {
|
it('should return empty array if no targets', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{}]
|
targets: [{}]
|
||||||
});
|
});
|
||||||
expect(results.length).to.be(0);
|
expect(results.length).to.be(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should uri escape targets', function() {
|
it('should uri escape targets', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}]
|
targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}]
|
||||||
});
|
});
|
||||||
expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D');
|
expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should replace target placeholder', function() {
|
it('should replace target placeholder', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}]
|
targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}]
|
||||||
});
|
});
|
||||||
expect(results[2]).to.be('target=asPercent(series1%2Cseries2)');
|
expect(results[2]).to.be('target=asPercent(series1%2Cseries2)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should replace target placeholder for hidden series', function() {
|
it('should replace target placeholder for hidden series', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: 'series1', hide: true}, {target: 'sumSeries(#A)', hide: true}, {target: 'asPercent(#A,#B)'}]
|
targets: [{target: 'series1', hide: true}, {target: 'sumSeries(#A)', hide: true}, {target: 'asPercent(#A,#B)'}]
|
||||||
});
|
});
|
||||||
expect(results[0]).to.be('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
|
expect(results[0]).to.be('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should replace target placeholder when nesting query references', function() {
|
it('should replace target placeholder when nesting query references', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: 'series1'}, {target: 'sumSeries(#A)'}, {target: 'asPercent(#A,#B)'}]
|
targets: [{target: 'series1'}, {target: 'sumSeries(#A)'}, {target: 'asPercent(#A,#B)'}]
|
||||||
});
|
});
|
||||||
expect(results[2]).to.be('target=' + encodeURIComponent("asPercent(series1,sumSeries(series1))"));
|
expect(results[2]).to.be('target=' + encodeURIComponent("asPercent(series1,sumSeries(series1))"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fix wrong minute interval parameters', function() {
|
it('should fix wrong minute interval parameters', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: "summarize(prod.25m.count, '25m', 'sum')" }]
|
targets: [{target: "summarize(prod.25m.count, '25m', 'sum')" }]
|
||||||
});
|
});
|
||||||
expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')"));
|
expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fix wrong month interval parameters', function() {
|
it('should fix wrong month interval parameters', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: "summarize(prod.5M.count, '5M', 'sum')" }]
|
targets: [{target: "summarize(prod.5M.count, '5M', 'sum')" }]
|
||||||
});
|
});
|
||||||
expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')"));
|
expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore empty targets', function() {
|
it('should ignore empty targets', function() {
|
||||||
var results = ctx.ds.buildGraphiteParams({
|
let results = ctx.ds.buildGraphiteParams({
|
||||||
targets: [{target: 'series1'}, {target: ''}]
|
targets: [{target: 'series1'}, {target: ''}]
|
||||||
});
|
});
|
||||||
expect(results.length).to.be(2);
|
expect(results.length).to.be(2);
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
|
@@ -9,18 +9,16 @@
|
|||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-4">Title</span>
|
<span class="gf-form-label width-4">Text</span>
|
||||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
|
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.textColumn' placeholder=""></input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-4">Tags</span>
|
<span class="gf-form-label width-4">Tags</span>
|
||||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsColumn' placeholder=""></input>
|
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsColumn' placeholder=""></input>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form" ng-show="ctrl.annotation.titleColumn">
|
||||||
<div class="gf-form">
|
<span class="gf-form-label width-4">Title <em class="muted">(depricated)</em></span>
|
||||||
<span class="gf-form-label width-4">Text</span>
|
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
|
||||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.textColumn' placeholder=""></input>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -9,7 +9,6 @@ class MysqlConfigCtrl {
|
|||||||
|
|
||||||
const defaultQuery = `SELECT
|
const defaultQuery = `SELECT
|
||||||
UNIX_TIMESTAMP(<time_column>) as time_sec,
|
UNIX_TIMESTAMP(<time_column>) as time_sec,
|
||||||
<title_column> as title,
|
|
||||||
<text_column> as text,
|
<text_column> as text,
|
||||||
<tags_column> as tags
|
<tags_column> as tags
|
||||||
FROM <table name>
|
FROM <table name>
|
||||||
|
@@ -106,7 +106,6 @@ export default class ResponseParser {
|
|||||||
const table = data.data.results[options.annotation.name].tables[0];
|
const table = data.data.results[options.annotation.name].tables[0];
|
||||||
|
|
||||||
let timeColumnIndex = -1;
|
let timeColumnIndex = -1;
|
||||||
let titleColumnIndex = -1;
|
|
||||||
let textColumnIndex = -1;
|
let textColumnIndex = -1;
|
||||||
let tagsColumnIndex = -1;
|
let tagsColumnIndex = -1;
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ export default class ResponseParser {
|
|||||||
if (table.columns[i].text === 'time_sec') {
|
if (table.columns[i].text === 'time_sec') {
|
||||||
timeColumnIndex = i;
|
timeColumnIndex = i;
|
||||||
} else if (table.columns[i].text === 'title') {
|
} else if (table.columns[i].text === 'title') {
|
||||||
titleColumnIndex = i;
|
return this.$q.reject({message: 'Title return column on annotations are depricated, return only a column named text'});
|
||||||
} else if (table.columns[i].text === 'text') {
|
} else if (table.columns[i].text === 'text') {
|
||||||
textColumnIndex = i;
|
textColumnIndex = i;
|
||||||
} else if (table.columns[i].text === 'tags') {
|
} else if (table.columns[i].text === 'tags') {
|
||||||
@@ -132,7 +131,6 @@ export default class ResponseParser {
|
|||||||
list.push({
|
list.push({
|
||||||
annotation: options.annotation,
|
annotation: options.annotation,
|
||||||
time: Math.floor(row[timeColumnIndex]) * 1000,
|
time: Math.floor(row[timeColumnIndex]) * 1000,
|
||||||
title: row[titleColumnIndex],
|
|
||||||
text: row[textColumnIndex],
|
text: row[textColumnIndex],
|
||||||
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
|
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
|
||||||
});
|
});
|
||||||
|
@@ -27,7 +27,7 @@ describe('MySQLDatasource', function() {
|
|||||||
const options = {
|
const options = {
|
||||||
annotation: {
|
annotation: {
|
||||||
name: annotationName,
|
name: annotationName,
|
||||||
rawQuery: 'select time_sec, title, text, tags from table;'
|
rawQuery: 'select time_sec, text, tags from table;'
|
||||||
},
|
},
|
||||||
range: {
|
range: {
|
||||||
from: moment(1432288354),
|
from: moment(1432288354),
|
||||||
@@ -41,11 +41,11 @@ describe('MySQLDatasource', function() {
|
|||||||
refId: annotationName,
|
refId: annotationName,
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}, {text: 'tags'}],
|
columns: [{text: 'time_sec'}, {text: 'text'}, {text: 'tags'}],
|
||||||
rows: [
|
rows: [
|
||||||
[1432288355, 'aTitle', 'some text', 'TagA,TagB'],
|
[1432288355, 'some text', 'TagA,TagB'],
|
||||||
[1432288390, 'aTitle2', 'some text2', ' TagB , TagC'],
|
[1432288390, 'some text2', ' TagB , TagC'],
|
||||||
[1432288400, 'aTitle3', 'some text3']
|
[1432288400, 'some text3']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -64,7 +64,6 @@ describe('MySQLDatasource', function() {
|
|||||||
it('should return annotation list', function() {
|
it('should return annotation list', function() {
|
||||||
expect(results.length).to.be(3);
|
expect(results.length).to.be(3);
|
||||||
|
|
||||||
expect(results[0].title).to.be('aTitle');
|
|
||||||
expect(results[0].text).to.be('some text');
|
expect(results[0].text).to.be('some text');
|
||||||
expect(results[0].tags[0]).to.be('TagA');
|
expect(results[0].tags[0]).to.be('TagA');
|
||||||
expect(results[0].tags[1]).to.be('TagB');
|
expect(results[0].tags[1]).to.be('TagB');
|
||||||
|
@@ -91,9 +91,8 @@ function (angular, _, dateMath) {
|
|||||||
if(annotationObject) {
|
if(annotationObject) {
|
||||||
_.each(annotationObject, function(annotation) {
|
_.each(annotationObject, function(annotation) {
|
||||||
var event = {
|
var event = {
|
||||||
title: annotation.description,
|
text: annotation.description,
|
||||||
time: Math.floor(annotation.startTime) * 1000,
|
time: Math.floor(annotation.startTime) * 1000,
|
||||||
text: annotation.notes,
|
|
||||||
annotation: options.annotation
|
annotation: options.annotation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
<i class="{{al.stateModel.iconClass}}"></i>
|
<i class="{{al.stateModel.iconClass}}"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert-list-main">
|
<div class="alert-list-main">
|
||||||
<p class="alert-list-title">{{al.title}}</p>
|
<p class="alert-list-title">{{al.alertName}}</p>
|
||||||
<div class="alert-list-text">
|
<div class="alert-list-text">
|
||||||
<span class="alert-list-state {{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
<span class="alert-list-state {{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
||||||
<span class="alert-list-info alert-list-info-left">{{al.info}}</span>
|
<span class="alert-list-info alert-list-info-left">{{al.info}}</span>
|
||||||
|
@@ -22,7 +22,7 @@ import {EventManager} from 'app/features/annotations/all';
|
|||||||
import {convertValuesToHistogram, getSeriesValues} from './histogram';
|
import {convertValuesToHistogram, getSeriesValues} from './histogram';
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
function graphDirective($rootScope, timeSrv, popoverSrv) {
|
function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
template: '',
|
template: '',
|
||||||
@@ -37,7 +37,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
|||||||
var legendSideLastValue = null;
|
var legendSideLastValue = null;
|
||||||
var rootScope = scope.$root;
|
var rootScope = scope.$root;
|
||||||
var panelWidth = 0;
|
var panelWidth = 0;
|
||||||
var eventManager = new EventManager(ctrl, elem, popoverSrv);
|
var eventManager = new EventManager(ctrl);
|
||||||
var thresholdManager = new ThresholdManager(ctrl);
|
var thresholdManager = new ThresholdManager(ctrl);
|
||||||
var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
|
var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
|
||||||
return sortedSeries;
|
return sortedSeries;
|
||||||
@@ -268,6 +268,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
|||||||
clickable: true,
|
clickable: true,
|
||||||
color: '#c8c8c8',
|
color: '#c8c8c8',
|
||||||
margin: { left: 0, right: 0 },
|
margin: { left: 0, right: 0 },
|
||||||
|
labelMarginX: 0,
|
||||||
},
|
},
|
||||||
selection: {
|
selection: {
|
||||||
mode: "x",
|
mode: "x",
|
||||||
@@ -651,10 +652,10 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elem.bind("plotselected", function (event, ranges) {
|
elem.bind("plotselected", function (event, ranges) {
|
||||||
if (ranges.ctrlKey || ranges.metaKey) {
|
if ((ranges.ctrlKey || ranges.metaKey) && contextSrv.isEditor) {
|
||||||
// scope.$apply(() => {
|
setTimeout(() => {
|
||||||
// eventManager.updateTime(ranges.xaxis);
|
eventManager.updateTime(ranges.xaxis);
|
||||||
// });
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
scope.$apply(function() {
|
scope.$apply(function() {
|
||||||
timeSrv.setTime({
|
timeSrv.setTime({
|
||||||
@@ -666,13 +667,13 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
elem.bind("plotclick", function (event, pos, item) {
|
elem.bind("plotclick", function (event, pos, item) {
|
||||||
if (pos.ctrlKey || pos.metaKey || eventManager.event) {
|
if ((pos.ctrlKey || pos.metaKey) && contextSrv.isEditor) {
|
||||||
// Skip if range selected (added in "plotselected" event handler)
|
// Skip if range selected (added in "plotselected" event handler)
|
||||||
let isRangeSelection = pos.x !== pos.x1;
|
let isRangeSelection = pos.x !== pos.x1;
|
||||||
if (!isRangeSelection) {
|
if (!isRangeSelection) {
|
||||||
// scope.$apply(() => {
|
setTimeout(() => {
|
||||||
// eventManager.updateTime({from: pos.x, to: null});
|
eventManager.updateTime({from: pos.x, to: null});
|
||||||
// });
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -7,14 +7,18 @@ define([
|
|||||||
function ($, _, angular, Drop) {
|
function ($, _, angular, Drop) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createAnnotationToolip(element, event) {
|
function createAnnotationToolip(element, event, plot) {
|
||||||
var injector = angular.element(document).injector();
|
var injector = angular.element(document).injector();
|
||||||
var content = document.createElement('div');
|
var content = document.createElement('div');
|
||||||
content.innerHTML = '<annotation-tooltip event="event"></annotation-tooltip>';
|
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
|
||||||
|
|
||||||
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
||||||
|
var eventManager = plot.getOptions().events.manager;
|
||||||
var tmpScope = $rootScope.$new(true);
|
var tmpScope = $rootScope.$new(true);
|
||||||
tmpScope.event = event;
|
tmpScope.event = event;
|
||||||
|
tmpScope.onEdit = function() {
|
||||||
|
eventManager.editEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
$compile(content)(tmpScope);
|
$compile(content)(tmpScope);
|
||||||
tmpScope.$digest();
|
tmpScope.$digest();
|
||||||
@@ -42,6 +46,69 @@ function ($, _, angular, Drop) {
|
|||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var markerElementToAttachTo = null;
|
||||||
|
|
||||||
|
function createEditPopover(element, event, plot) {
|
||||||
|
var eventManager = plot.getOptions().events.manager;
|
||||||
|
if (eventManager.editorOpen) {
|
||||||
|
// update marker element to attach to (needed in case of legend on the right
|
||||||
|
// when there is a double render pass and the inital marker element is removed)
|
||||||
|
markerElementToAttachTo = element;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark as openend
|
||||||
|
eventManager.editorOpened();
|
||||||
|
// set marker elment to attache to
|
||||||
|
markerElementToAttachTo = element;
|
||||||
|
|
||||||
|
// wait for element to be attached and positioned
|
||||||
|
setTimeout(function() {
|
||||||
|
|
||||||
|
var injector = angular.element(document).injector();
|
||||||
|
var content = document.createElement('div');
|
||||||
|
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
|
||||||
|
|
||||||
|
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
||||||
|
var scope = $rootScope.$new(true);
|
||||||
|
var drop;
|
||||||
|
|
||||||
|
scope.event = event;
|
||||||
|
scope.panelCtrl = eventManager.panelCtrl;
|
||||||
|
scope.close = function() {
|
||||||
|
drop.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
$compile(content)(scope);
|
||||||
|
scope.$digest();
|
||||||
|
|
||||||
|
drop = new Drop({
|
||||||
|
target: markerElementToAttachTo[0],
|
||||||
|
content: content,
|
||||||
|
position: "bottom center",
|
||||||
|
classes: 'drop-popover drop-popover--form',
|
||||||
|
openOn: 'click',
|
||||||
|
tetherOptions: {
|
||||||
|
constraints: [{to: 'window', pin: true, attachment: "both"}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
drop.open();
|
||||||
|
eventManager.editorOpened();
|
||||||
|
|
||||||
|
drop.on('close', function() {
|
||||||
|
// need timeout here in order call drop.destroy
|
||||||
|
setTimeout(function() {
|
||||||
|
eventManager.editorClosed();
|
||||||
|
scope.$destroy();
|
||||||
|
drop.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}]);
|
||||||
|
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* jquery.flot.events
|
* jquery.flot.events
|
||||||
*
|
*
|
||||||
@@ -121,11 +188,20 @@ function ($, _, angular, Drop) {
|
|||||||
*/
|
*/
|
||||||
this.setupEvents = function(events) {
|
this.setupEvents = function(events) {
|
||||||
var that = this;
|
var that = this;
|
||||||
|
var parts = _.partition(events, 'isRegion');
|
||||||
|
var regions = parts[0];
|
||||||
|
events = parts[1];
|
||||||
|
|
||||||
$.each(events, function(index, event) {
|
$.each(events, function(index, event) {
|
||||||
var ve = new VisualEvent(event, that._buildDiv(event));
|
var ve = new VisualEvent(event, that._buildDiv(event));
|
||||||
_events.push(ve);
|
_events.push(ve);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$.each(regions, function (index, event) {
|
||||||
|
var vre = new VisualEvent(event, that._buildRegDiv(event));
|
||||||
|
_events.push(vre);
|
||||||
|
});
|
||||||
|
|
||||||
_events.sort(function(a, b) {
|
_events.sort(function(a, b) {
|
||||||
var ao = a.getOptions(), bo = b.getOptions();
|
var ao = a.getOptions(), bo = b.getOptions();
|
||||||
if (ao.min > bo.min) { return 1; }
|
if (ao.min > bo.min) { return 1; }
|
||||||
@@ -232,7 +308,10 @@ function ($, _, angular, Drop) {
|
|||||||
lineWidth = this._types[eventTypeId].lineWidth;
|
lineWidth = this._types[eventTypeId].lineWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
top = o.top + this._plot.height();
|
var topOffset = xaxis.options.eventSectionHeight || 0;
|
||||||
|
topOffset = topOffset / 3;
|
||||||
|
|
||||||
|
top = o.top + this._plot.height() + topOffset;
|
||||||
left = xaxis.p2c(event.min) + o.left;
|
left = xaxis.p2c(event.min) + o.left;
|
||||||
|
|
||||||
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||||
@@ -241,25 +320,27 @@ function ($, _, angular, Drop) {
|
|||||||
"left": left + 'px',
|
"left": left + 'px',
|
||||||
"top": 8,
|
"top": 8,
|
||||||
"width": lineWidth + "px",
|
"width": lineWidth + "px",
|
||||||
"height": this._plot.height(),
|
"height": this._plot.height() + topOffset * 0.8,
|
||||||
"border-left-width": lineWidth + "px",
|
"border-left-width": lineWidth + "px",
|
||||||
"border-left-style": lineStyle,
|
"border-left-style": lineStyle,
|
||||||
"border-left-color": color
|
"border-left-color": color,
|
||||||
|
"color": color
|
||||||
})
|
})
|
||||||
.appendTo(container);
|
.appendTo(container);
|
||||||
|
|
||||||
if (markerShow) {
|
if (markerShow) {
|
||||||
var marker = $('<div class="events_marker"></div>').css({
|
var marker = $('<div class="events_marker"></div>').css({
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"left": (-markerSize-Math.round(lineWidth/2)) + "px",
|
"left": (-markerSize - Math.round(lineWidth / 2)) + "px",
|
||||||
"font-size": 0,
|
"font-size": 0,
|
||||||
"line-height": 0,
|
"line-height": 0,
|
||||||
"width": 0,
|
"width": 0,
|
||||||
"height": 0,
|
"height": 0,
|
||||||
"border-left": markerSize+"px solid transparent",
|
"border-left": markerSize+"px solid transparent",
|
||||||
"border-right": markerSize+"px solid transparent"
|
"border-right": markerSize+"px solid transparent"
|
||||||
})
|
});
|
||||||
.appendTo(line);
|
|
||||||
|
marker.appendTo(line);
|
||||||
|
|
||||||
if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
|
if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
|
||||||
marker.css({
|
marker.css({
|
||||||
@@ -280,9 +361,13 @@ function ($, _, angular, Drop) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var mouseenter = function() {
|
var mouseenter = function() {
|
||||||
createAnnotationToolip(marker, $(this).data("event"));
|
createAnnotationToolip(marker, $(this).data("event"), that._plot);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (event.editModel) {
|
||||||
|
createEditPopover(marker, event.editModel, that._plot);
|
||||||
|
}
|
||||||
|
|
||||||
var mouseleave = function() {
|
var mouseleave = function() {
|
||||||
that._plot.clearSelection();
|
that._plot.clearSelection();
|
||||||
};
|
};
|
||||||
@@ -312,6 +397,127 @@ function ($, _, angular, Drop) {
|
|||||||
return drawableEvent;
|
return drawableEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a DOM element for the given region
|
||||||
|
*/
|
||||||
|
this._buildRegDiv = function (event) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var container = this._plot.getPlaceholder();
|
||||||
|
var o = this._plot.getPlotOffset();
|
||||||
|
var axes = this._plot.getAxes();
|
||||||
|
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||||
|
var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
|
||||||
|
|
||||||
|
// determine the y axis used
|
||||||
|
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
|
||||||
|
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
|
||||||
|
|
||||||
|
// map the eventType to a types object
|
||||||
|
var eventTypeId = event.eventType;
|
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
|
||||||
|
color = '#666';
|
||||||
|
} else {
|
||||||
|
color = this._types[eventTypeId].color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
|
||||||
|
markerTooltip = true;
|
||||||
|
} else {
|
||||||
|
markerTooltip = this._types[eventTypeId].markerTooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
|
||||||
|
lineWidth = 1; //default line width
|
||||||
|
} else {
|
||||||
|
lineWidth = this._types[eventTypeId].lineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
|
||||||
|
lineStyle = 'dashed'; //default line style
|
||||||
|
} else {
|
||||||
|
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
var topOffset = 2;
|
||||||
|
top = o.top + this._plot.height() + topOffset;
|
||||||
|
|
||||||
|
var timeFrom = Math.min(event.min, event.timeEnd);
|
||||||
|
var timeTo = Math.max(event.min, event.timeEnd);
|
||||||
|
left = xaxis.p2c(timeFrom) + o.left;
|
||||||
|
var right = xaxis.p2c(timeTo) + o.left;
|
||||||
|
regionWidth = right - left;
|
||||||
|
|
||||||
|
_.each([left, right], function(position) {
|
||||||
|
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||||
|
"position": "absolute",
|
||||||
|
"opacity": 0.8,
|
||||||
|
"left": position + 'px',
|
||||||
|
"top": 8,
|
||||||
|
"width": lineWidth + "px",
|
||||||
|
"height": that._plot.height() + topOffset,
|
||||||
|
"border-left-width": lineWidth + "px",
|
||||||
|
"border-left-style": lineStyle,
|
||||||
|
"border-left-color": color,
|
||||||
|
"color": color
|
||||||
|
});
|
||||||
|
line.appendTo(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
|
||||||
|
"position": "absolute",
|
||||||
|
"opacity": 0.5,
|
||||||
|
"left": left + 'px',
|
||||||
|
"top": top,
|
||||||
|
"width": Math.round(regionWidth + lineWidth) + "px",
|
||||||
|
"height": "0.5rem",
|
||||||
|
"border-left-color": color,
|
||||||
|
"color": color,
|
||||||
|
"background-color": color
|
||||||
|
});
|
||||||
|
region.appendTo(container);
|
||||||
|
|
||||||
|
region.data({
|
||||||
|
"event": event
|
||||||
|
});
|
||||||
|
|
||||||
|
var mouseenter = function () {
|
||||||
|
createAnnotationToolip(region, $(this).data("event"), that._plot);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.editModel) {
|
||||||
|
createEditPopover(region, event.editModel, that._plot);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouseleave = function () {
|
||||||
|
that._plot.clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (markerTooltip) {
|
||||||
|
region.css({ "cursor": "help" });
|
||||||
|
region.hover(mouseenter, mouseleave);
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawableEvent = new DrawableEvent(
|
||||||
|
region,
|
||||||
|
function drawFunc(obj) { obj.show(); },
|
||||||
|
function (obj) { obj.remove(); },
|
||||||
|
function (obj, position) {
|
||||||
|
obj.css({
|
||||||
|
top: position.top,
|
||||||
|
left: position.left
|
||||||
|
});
|
||||||
|
},
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
region.width(),
|
||||||
|
region.height()
|
||||||
|
);
|
||||||
|
|
||||||
|
return drawableEvent;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the event is inside visible range
|
* check if the event is inside visible range
|
||||||
*/
|
*/
|
||||||
@@ -395,5 +601,4 @@ function ($, _, angular, Drop) {
|
|||||||
name: "events",
|
name: "events",
|
||||||
version: "0.2.5"
|
version: "0.2.5"
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
83
public/app/system.conf.js
Normal file
83
public/app/system.conf.js
Normal file
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@@ -78,6 +78,7 @@
|
|||||||
@import "components/jsontree";
|
@import "components/jsontree";
|
||||||
@import "components/edit_sidemenu.scss";
|
@import "components/edit_sidemenu.scss";
|
||||||
@import "components/row.scss";
|
@import "components/row.scss";
|
||||||
|
@import "components/icon-picker.scss";
|
||||||
@import "components/json_explorer.scss";
|
@import "components/json_explorer.scss";
|
||||||
@import "components/code_editor.scss";
|
@import "components/code_editor.scss";
|
||||||
|
|
||||||
|
@@ -251,7 +251,8 @@ $alert-info-bg: linear-gradient(100deg, #1a4552, #00374a);
|
|||||||
// popover
|
// popover
|
||||||
$popover-bg: $panel-bg;
|
$popover-bg: $panel-bg;
|
||||||
$popover-color: $text-color;
|
$popover-color: $text-color;
|
||||||
$popover-border-color: $gray-1;
|
$popover-border-color: $dark-4;
|
||||||
|
$popover-shadow: 0 0 20px black;
|
||||||
|
|
||||||
$popover-help-bg: $btn-secondary-bg;
|
$popover-help-bg: $btn-secondary-bg;
|
||||||
$popover-help-color: $text-color;
|
$popover-help-color: $text-color;
|
||||||
|
@@ -270,9 +270,11 @@ $alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d);
|
|||||||
$alert-info-bg: $blue-dark;
|
$alert-info-bg: $blue-dark;
|
||||||
|
|
||||||
// popover
|
// popover
|
||||||
$popover-bg: $gray-5;
|
$popover-bg: $panel-bg;
|
||||||
$popover-color: $text-color;
|
$popover-color: $text-color;
|
||||||
$popover-border-color: $gray-3;
|
$popover-border-color: $gray-5;
|
||||||
|
$popover-shadow: 0 0 20px $white;
|
||||||
|
|
||||||
$popover-help-bg: $blue-dark;
|
$popover-help-bg: $blue-dark;
|
||||||
$popover-help-color: $gray-6;
|
$popover-help-color: $gray-6;
|
||||||
$popover-error-bg: $btn-danger-bg;
|
$popover-error-bg: $btn-danger-bg;
|
||||||
|
@@ -51,9 +51,16 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop-element.drop-popover {
|
||||||
|
.drop-content {
|
||||||
|
box-shadow: $popover-shadow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.drop-element.drop-popover--form {
|
.drop-element.drop-popover--form {
|
||||||
.drop-content {
|
.drop-content {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
public/sass/components/_icon-picker.scss
Normal file
26
public/sass/components/_icon-picker.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -287,19 +287,27 @@
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-annotation-header {
|
.graph-annotation__header {
|
||||||
background-color: $input-label-bg;
|
background-color: $popover-border-color;
|
||||||
padding: 0.40rem 0.65rem;
|
padding: 0.40rem 0.65rem;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-annotation-title {
|
.graph-annotation__title {
|
||||||
font-weight: $font-weight-semi-bold;
|
font-weight: $font-weight-semi-bold;
|
||||||
padding-right: $spacer;
|
padding-right: $spacer;
|
||||||
position: relative;
|
overflow: hidden;
|
||||||
top: 2px;
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-annotation-time {
|
.graph-annotation__edit-icon {
|
||||||
|
padding-left: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-annotation__time {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -308,15 +316,22 @@
|
|||||||
top: 1px;
|
top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-annotation-body {
|
.graph-annotation__body {
|
||||||
padding: 0.65rem;
|
padding: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.graph-annotation__user {
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href] {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-yaxis-label {
|
.left-yaxis-label {
|
||||||
|
@@ -16,10 +16,6 @@
|
|||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
border: 1px solid $border-color;
|
border: 1px solid $border-color;
|
||||||
|
|
||||||
@if $theme-bg != $border-color {
|
|
||||||
box-shadow: 0 0 15px $border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
130
public/test/test-main.js
Normal file
130
public/test/test-main.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
7
public/vendor/flot/jquery.flot.js
vendored
7
public/vendor/flot/jquery.flot.js
vendored
@@ -602,6 +602,7 @@ Licensed under the MIT license.
|
|||||||
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
|
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
|
||||||
margin: 0, // distance from the canvas edge to the grid
|
margin: 0, // distance from the canvas edge to the grid
|
||||||
labelMargin: 5, // in pixels
|
labelMargin: 5, // in pixels
|
||||||
|
eventSectionHeight: 0, // space for event section
|
||||||
axisMargin: 8, // in pixels
|
axisMargin: 8, // in pixels
|
||||||
borderWidth: 2, // in pixels
|
borderWidth: 2, // in pixels
|
||||||
minBorderMargin: null, // in pixels, null means taken from points radius
|
minBorderMargin: null, // in pixels, null means taken from points radius
|
||||||
@@ -1450,6 +1451,7 @@ Licensed under the MIT license.
|
|||||||
tickLength = axis.options.tickLength,
|
tickLength = axis.options.tickLength,
|
||||||
axisMargin = options.grid.axisMargin,
|
axisMargin = options.grid.axisMargin,
|
||||||
padding = options.grid.labelMargin,
|
padding = options.grid.labelMargin,
|
||||||
|
eventSectionPadding = options.grid.eventSectionHeight,
|
||||||
innermost = true,
|
innermost = true,
|
||||||
outermost = true,
|
outermost = true,
|
||||||
first = true,
|
first = true,
|
||||||
@@ -1490,7 +1492,9 @@ Licensed under the MIT license.
|
|||||||
padding += +tickLength;
|
padding += +tickLength;
|
||||||
|
|
||||||
if (isXAxis) {
|
if (isXAxis) {
|
||||||
|
// Add space for event section
|
||||||
lh += padding;
|
lh += padding;
|
||||||
|
lh += eventSectionPadding;
|
||||||
|
|
||||||
if (pos == "bottom") {
|
if (pos == "bottom") {
|
||||||
plotOffset.bottom += lh + axisMargin;
|
plotOffset.bottom += lh + axisMargin;
|
||||||
@@ -1518,6 +1522,7 @@ Licensed under the MIT license.
|
|||||||
axis.position = pos;
|
axis.position = pos;
|
||||||
axis.tickLength = tickLength;
|
axis.tickLength = tickLength;
|
||||||
axis.box.padding = padding;
|
axis.box.padding = padding;
|
||||||
|
axis.box.eventSectionPadding = eventSectionPadding;
|
||||||
axis.innermost = innermost;
|
axis.innermost = innermost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2225,7 +2230,7 @@ Licensed under the MIT license.
|
|||||||
halign = "center";
|
halign = "center";
|
||||||
x = plotOffset.left + axis.p2c(tick.v);
|
x = plotOffset.left + axis.p2c(tick.v);
|
||||||
if (axis.position == "bottom") {
|
if (axis.position == "bottom") {
|
||||||
y = box.top + box.padding;
|
y = box.top + box.padding + box.eventSectionPadding;
|
||||||
} else {
|
} else {
|
||||||
y = box.top + box.height - box.padding;
|
y = box.top + box.height - box.padding;
|
||||||
valign = "bottom";
|
valign = "bottom";
|
||||||
|
12
public/vendor/tagsinput/bootstrap-tagsinput.js
vendored
12
public/vendor/tagsinput/bootstrap-tagsinput.js
vendored
@@ -28,15 +28,14 @@
|
|||||||
this.$element = $(element);
|
this.$element = $(element);
|
||||||
this.$element.hide();
|
this.$element.hide();
|
||||||
|
|
||||||
|
this.widthClass = options.widthClass || 'width-9';
|
||||||
this.isSelect = (element.tagName === 'SELECT');
|
this.isSelect = (element.tagName === 'SELECT');
|
||||||
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
||||||
this.objectItems = options && options.itemValue;
|
this.objectItems = options && options.itemValue;
|
||||||
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
|
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
|
||||||
this.inputSize = Math.max(1, this.placeholderText.length);
|
|
||||||
|
|
||||||
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
||||||
this.$input = $('<input class="gf-form-input" size="' +
|
this.$input = $('<input class="gf-form-input ' + this.widthClass + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
||||||
this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
|
||||||
|
|
||||||
this.$element.after(this.$container);
|
this.$element.after(this.$container);
|
||||||
|
|
||||||
@@ -292,6 +291,13 @@
|
|||||||
self.$input.focus();
|
self.$input.focus();
|
||||||
}, self));
|
}, self));
|
||||||
|
|
||||||
|
self.$container.on('blur', 'input', $.proxy(function(event) {
|
||||||
|
var $input = $(event.target);
|
||||||
|
self.add($input.val());
|
||||||
|
$input.val('');
|
||||||
|
event.preventDefault();
|
||||||
|
}, self));
|
||||||
|
|
||||||
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
||||||
var $input = $(event.target),
|
var $input = $(event.target),
|
||||||
$inputWrapper = self.findInputWrapper();
|
$inputWrapper = self.findInputWrapper();
|
||||||
|
@@ -29,13 +29,14 @@ module.exports = {
|
|||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx)$/,
|
test: /\.tsx?$/,
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: {
|
use: {
|
||||||
loader: 'tslint-loader',
|
loader: 'tslint-loader',
|
||||||
options: {
|
options: {
|
||||||
emitErrors: true
|
emitErrors: true,
|
||||||
|
typeCheck: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,10 +60,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
|
||||||
// loader : 'file-loader',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
test: /\.html$/,
|
test: /\.html$/,
|
||||||
exclude: /index\.template.html/,
|
exclude: /index\.template.html/,
|
||||||
|
45
tasks/options/copy.js
Normal file
45
tasks/options/copy.js
Normal file
@@ -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'
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
@@ -9,8 +9,8 @@
|
|||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": false,
|
||||||
"sourceMap": false,
|
"sourceMap": true,
|
||||||
"noEmitOnError": false,
|
"noEmitOnError": false,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"experimentalDecorators": false,
|
"experimentalDecorators": false,
|
||||||
@@ -28,7 +28,5 @@
|
|||||||
"public/app/**/*.ts",
|
"public/app/**/*.ts",
|
||||||
"public/app/**/*.tsx",
|
"public/app/**/*.tsx",
|
||||||
"public/test/**/*.ts"
|
"public/test/**/*.ts"
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"no-unused-variable": true,
|
"no-unused-variable": true,
|
||||||
"curly": true,
|
"curly": true,
|
||||||
"class-name": true,
|
"class-name": true,
|
||||||
"semicolon": ["always"],
|
"semicolon": [true, "always", "ignore-bound-class-methods"],
|
||||||
"triple-equals": [true, "allow-null-check"],
|
"triple-equals": [true, "allow-null-check"],
|
||||||
"comment-format": [false, "check-space"],
|
"comment-format": [false, "check-space"],
|
||||||
"eofline": true,
|
"eofline": true,
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
],
|
],
|
||||||
"no-construct": true,
|
"no-construct": true,
|
||||||
"no-debugger": true,
|
"no-debugger": true,
|
||||||
"no-duplicate-variable": true,
|
|
||||||
"no-empty": false,
|
"no-empty": false,
|
||||||
"no-eval": true,
|
"no-eval": true,
|
||||||
"no-inferrable-types": true,
|
"no-inferrable-types": true,
|
||||||
|
Reference in New Issue
Block a user