mirror of
https://github.com/grafana/grafana.git
synced 2026-02-19 02:30:53 -05:00
Some checks failed
Actionlint / Lint GitHub Actions files (push) Has been cancelled
Backend Code Checks / Detect whether code changed (push) Has been cancelled
Backend Unit Tests / Detect whether code changed (push) Has been cancelled
CodeQL checks / Detect whether code changed (push) Has been cancelled
Deploy Storybook / Detect whether code changed (push) Has been cancelled
Lint Frontend / Detect whether code changed (push) Has been cancelled
Lint Frontend / Verify API clients (push) Has been cancelled
Lint Frontend / Verify API clients (enterprise) (push) Has been cancelled
golangci-lint / Detect whether code changed (push) Has been cancelled
Verify i18n / verify-i18n (push) Has been cancelled
Documentation / Build & Verify Docs (push) Has been cancelled
End-to-end tests / Detect whether code changed (push) Has been cancelled
Frontend tests / Detect whether code changed (push) Has been cancelled
Integration Tests / Detect whether code changed (push) Has been cancelled
publish-technical-documentation-next / sync (push) Has been cancelled
Reject GitHub secrets / reject-gh-secrets (push) Has been cancelled
Build Release Packages / setup (push) Has been cancelled
Run dashboard schema v2 e2e / dashboard-schema-v2-e2e (push) Has been cancelled
Shellcheck / Shellcheck scripts (push) Has been cancelled
Run Storybook a11y tests / Detect whether code changed (push) Has been cancelled
Swagger generated code / Detect whether code changed (push) Has been cancelled
Dispatch sync to mirror / dispatch-job (push) Has been cancelled
Backend Code Checks / Validate Backend Configs (push) Has been cancelled
Backend Unit Tests / Grafana (1/8) (push) Has been cancelled
Backend Unit Tests / Grafana (2/8) (push) Has been cancelled
Backend Unit Tests / Grafana (3/8) (push) Has been cancelled
Backend Unit Tests / Grafana (4/8) (push) Has been cancelled
Backend Unit Tests / Grafana (5/8) (push) Has been cancelled
Backend Unit Tests / Grafana (6/8) (push) Has been cancelled
Backend Unit Tests / Grafana (7/8) (push) Has been cancelled
Backend Unit Tests / Grafana (8/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (1/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (2/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (3/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (4/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (5/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (6/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (7/8) (push) Has been cancelled
Backend Unit Tests / Grafana Enterprise (8/8) (push) Has been cancelled
Backend Unit Tests / All backend unit tests complete (push) Has been cancelled
CodeQL checks / Analyze (push) Has been cancelled
Deploy Storybook / Deploy Storybook (push) Has been cancelled
Lint Frontend / Lint (push) Has been cancelled
Lint Frontend / Typecheck (push) Has been cancelled
Lint Frontend / Verify packed frontend packages (push) Has been cancelled
golangci-lint / go-fmt (push) Has been cancelled
golangci-lint / lint-go (push) Has been cancelled
End-to-end tests / Build & Package Grafana (push) Has been cancelled
End-to-end tests / Build E2E test runner (push) Has been cancelled
End-to-end tests / push-docker-image (push) Has been cancelled
End-to-end tests / dashboards-suite (old arch) (push) Has been cancelled
End-to-end tests / panels-suite (old arch) (push) Has been cancelled
End-to-end tests / smoke-tests-suite (old arch) (push) Has been cancelled
End-to-end tests / various-suite (old arch) (push) Has been cancelled
End-to-end tests / Verify Storybook (Playwright) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (1/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (2/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (3/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (4/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (5/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (6/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (7/8) (push) Has been cancelled
End-to-end tests / Playwright E2E tests (8/8) (push) Has been cancelled
End-to-end tests / run-azure-monitor-e2e (push) Has been cancelled
End-to-end tests / All Playwright tests complete (push) Has been cancelled
End-to-end tests / A11y test (push) Has been cancelled
End-to-end tests / Publish metrics (push) Has been cancelled
End-to-end tests / All E2E tests complete (push) Has been cancelled
Frontend tests / Unit tests (1 / 16) (push) Has been cancelled
Frontend tests / Unit tests (10 / 16) (push) Has been cancelled
Frontend tests / Unit tests (11 / 16) (push) Has been cancelled
Frontend tests / Unit tests (12 / 16) (push) Has been cancelled
Frontend tests / Unit tests (13 / 16) (push) Has been cancelled
Frontend tests / Unit tests (14 / 16) (push) Has been cancelled
Frontend tests / Unit tests (15 / 16) (push) Has been cancelled
Frontend tests / Unit tests (16 / 16) (push) Has been cancelled
Frontend tests / Unit tests (2 / 16) (push) Has been cancelled
Frontend tests / Unit tests (3 / 16) (push) Has been cancelled
Frontend tests / Unit tests (4 / 16) (push) Has been cancelled
Frontend tests / Unit tests (5 / 16) (push) Has been cancelled
Frontend tests / Unit tests (6 / 16) (push) Has been cancelled
Frontend tests / Unit tests (7 / 16) (push) Has been cancelled
Frontend tests / Unit tests (8 / 16) (push) Has been cancelled
Frontend tests / Unit tests (9 / 16) (push) Has been cancelled
Frontend tests / Decoupled plugin tests (push) Has been cancelled
Frontend tests / Packages unit tests (push) Has been cancelled
Frontend tests / All frontend unit tests complete (push) Has been cancelled
Frontend tests / Devenv frontend-service build (push) Has been cancelled
Integration Tests / Sqlite (1/4) (push) Has been cancelled
Integration Tests / Sqlite (2/4) (push) Has been cancelled
Integration Tests / Sqlite (3/4) (push) Has been cancelled
Integration Tests / Sqlite (4/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo (1/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo (2/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo (3/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo (4/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo (profiled) (push) Has been cancelled
Integration Tests / MySQL (1/16) (push) Has been cancelled
Integration Tests / MySQL (10/16) (push) Has been cancelled
Integration Tests / MySQL (11/16) (push) Has been cancelled
Integration Tests / MySQL (12/16) (push) Has been cancelled
Integration Tests / MySQL (13/16) (push) Has been cancelled
Integration Tests / MySQL (14/16) (push) Has been cancelled
Integration Tests / MySQL (15/16) (push) Has been cancelled
Integration Tests / MySQL (16/16) (push) Has been cancelled
Integration Tests / MySQL (2/16) (push) Has been cancelled
Integration Tests / MySQL (3/16) (push) Has been cancelled
Integration Tests / MySQL (4/16) (push) Has been cancelled
Integration Tests / MySQL (5/16) (push) Has been cancelled
Integration Tests / MySQL (6/16) (push) Has been cancelled
Integration Tests / MySQL (7/16) (push) Has been cancelled
Integration Tests / MySQL (8/16) (push) Has been cancelled
Integration Tests / MySQL (9/16) (push) Has been cancelled
Integration Tests / Postgres (1/16) (push) Has been cancelled
Integration Tests / Postgres (10/16) (push) Has been cancelled
Integration Tests / Postgres (11/16) (push) Has been cancelled
Integration Tests / Postgres (12/16) (push) Has been cancelled
Integration Tests / Postgres (13/16) (push) Has been cancelled
Integration Tests / Postgres (14/16) (push) Has been cancelled
Integration Tests / Postgres (15/16) (push) Has been cancelled
Integration Tests / Postgres (16/16) (push) Has been cancelled
Integration Tests / Postgres (2/16) (push) Has been cancelled
Integration Tests / Postgres (3/16) (push) Has been cancelled
Integration Tests / Postgres (4/16) (push) Has been cancelled
Integration Tests / Postgres (5/16) (push) Has been cancelled
Integration Tests / Postgres (6/16) (push) Has been cancelled
Integration Tests / Postgres (7/16) (push) Has been cancelled
Integration Tests / Postgres (8/16) (push) Has been cancelled
Integration Tests / Postgres (9/16) (push) Has been cancelled
Integration Tests / Sqlite Enterprise (1/4) (push) Has been cancelled
Integration Tests / Sqlite Enterprise (2/4) (push) Has been cancelled
Integration Tests / Sqlite Enterprise (3/4) (push) Has been cancelled
Integration Tests / Sqlite Enterprise (4/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo Enterprise (1/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo Enterprise (2/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo Enterprise (3/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo Enterprise (4/4) (push) Has been cancelled
Integration Tests / Sqlite Without CGo Enterprise (profiled) (push) Has been cancelled
Integration Tests / MySQL Enterprise (1/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (10/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (11/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (12/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (13/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (14/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (15/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (16/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (2/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (3/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (4/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (5/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (6/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (7/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (8/16) (push) Has been cancelled
Integration Tests / MySQL Enterprise (9/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (1/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (10/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (11/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (12/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (13/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (14/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (15/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (16/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (2/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (3/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (4/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (5/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (6/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (7/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (8/16) (push) Has been cancelled
Integration Tests / Postgres Enterprise (9/16) (push) Has been cancelled
Integration Tests / All backend integration tests complete (push) Has been cancelled
Build Release Packages / Dispatch grafana-enterprise build (push) Has been cancelled
Build Release Packages / / darwin-amd64 (push) Has been cancelled
Build Release Packages / / darwin-arm64 (push) Has been cancelled
Build Release Packages / / linux-amd64 (push) Has been cancelled
Build Release Packages / / linux-armv6 (push) Has been cancelled
Build Release Packages / / linux-armv7 (push) Has been cancelled
Build Release Packages / / linux-arm64 (push) Has been cancelled
Build Release Packages / / linux-s390x (push) Has been cancelled
Build Release Packages / / windows-amd64 (push) Has been cancelled
Build Release Packages / / windows-arm64 (push) Has been cancelled
Build Release Packages / Upload artifacts (push) Has been cancelled
Build Release Packages / publish-dockerhub (push) Has been cancelled
Build Release Packages / Dispatch publish NPM canaries (push) Has been cancelled
Build Release Packages / notify-pr (push) Has been cancelled
Run Storybook a11y tests / Run Storybook a11y tests (light theme) (push) Has been cancelled
Run Storybook a11y tests / Run Storybook a11y tests (dark theme) (push) Has been cancelled
Swagger generated code / Verify committed API specs match (push) Has been cancelled
* Remove fully rolled out ft annotationPermissionUpdate * fix annot test and lint * fix frontend tests * fix integration test * fix flaky test
785 lines
25 KiB
Go
785 lines
25 KiB
Go
package api
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/grafana/grafana/pkg/api/dtos"
|
||
"github.com/grafana/grafana/pkg/api/response"
|
||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||
"github.com/grafana/grafana/pkg/services/annotations"
|
||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||
"github.com/grafana/grafana/pkg/services/folder"
|
||
"github.com/grafana/grafana/pkg/services/user"
|
||
"github.com/grafana/grafana/pkg/util"
|
||
"github.com/grafana/grafana/pkg/web"
|
||
)
|
||
|
||
const defaultAnnotationsLimit = 100
|
||
|
||
// swagger:route GET /annotations annotations getAnnotations
|
||
//
|
||
// Find Annotations.
|
||
//
|
||
// Starting in Grafana v6.4 regions annotations are now returned in one entity that now includes the timeEnd property.
|
||
//
|
||
// Responses:
|
||
// 200: getAnnotationsResponse
|
||
// 401: unauthorisedError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) GetAnnotations(c *contextmodel.ReqContext) response.Response {
|
||
query := &annotations.ItemQuery{
|
||
From: c.QueryInt64("from"),
|
||
To: c.QueryInt64("to"),
|
||
OrgID: c.GetOrgID(),
|
||
UserID: c.QueryInt64("userId"),
|
||
AlertID: c.QueryInt64("alertId"),
|
||
AlertUID: c.Query("alertUID"),
|
||
DashboardID: c.QueryInt64("dashboardId"),
|
||
DashboardUID: c.Query("dashboardUID"),
|
||
PanelID: c.QueryInt64("panelId"),
|
||
Limit: c.QueryInt64("limit"),
|
||
Tags: c.QueryStrings("tags"),
|
||
Type: c.Query("type"),
|
||
MatchAny: c.QueryBool("matchAny"),
|
||
SignedInUser: c.SignedInUser,
|
||
}
|
||
if query.Limit == 0 {
|
||
query.Limit = defaultAnnotationsLimit
|
||
}
|
||
|
||
// When dashboard ID exists without UID, find the UID from dashboards api
|
||
if query.DashboardID != 0 && query.DashboardUID == "" { // nolint:staticcheck
|
||
dq := dashboards.GetDashboardQuery{ID: query.DashboardID, OrgID: c.GetOrgID()} // nolint:staticcheck
|
||
dqResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &dq)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "Invalid dashboard ID in annotation request", err)
|
||
}
|
||
query.DashboardUID = dqResult.UID
|
||
}
|
||
|
||
items, err := hs.annotationsRepo.Find(c.Req.Context(), query)
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Failed to get annotations", err)
|
||
}
|
||
|
||
for _, item := range items {
|
||
if item.Email != "" {
|
||
item.AvatarURL = dtos.GetGravatarUrl(hs.Cfg, item.Email)
|
||
}
|
||
}
|
||
|
||
return response.JSON(http.StatusOK, items)
|
||
}
|
||
|
||
type AnnotationError struct {
|
||
message string
|
||
}
|
||
|
||
func (e *AnnotationError) Error() string {
|
||
return e.message
|
||
}
|
||
|
||
// swagger:route POST /annotations annotations postAnnotation
|
||
//
|
||
// Create Annotation.
|
||
//
|
||
// Creates an annotation in the Grafana database. The dashboardId and panelId fields are optional. If they are not specified then an organization annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. When creating a region annotation include the timeEnd property.
|
||
// The format for `time` and `timeEnd` should be epoch numbers in millisecond resolution.
|
||
// The response for this HTTP request is slightly different in versions prior to v6.4. In prior versions you would also get an endId if you where creating a region. But in 6.4 regions are represented using a single event with time and timeEnd properties.
|
||
//
|
||
// Responses:
|
||
// 200: postAnnotationResponse
|
||
// 400: badRequestError
|
||
// 401: unauthorisedError
|
||
// 403: forbiddenError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) PostAnnotation(c *contextmodel.ReqContext) response.Response {
|
||
cmd := dtos.PostAnnotationsCmd{}
|
||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||
}
|
||
|
||
// overwrite dashboardId when dashboardUID is not empty
|
||
if cmd.DashboardUID != "" {
|
||
query := dashboards.GetDashboardQuery{OrgID: c.GetOrgID(), UID: cmd.DashboardUID}
|
||
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
|
||
if err == nil {
|
||
cmd.DashboardId = queryResult.ID
|
||
}
|
||
}
|
||
|
||
// get dashboard uid if not provided
|
||
if cmd.DashboardId != 0 && cmd.DashboardUID == "" {
|
||
query := dashboards.GetDashboardQuery{OrgID: c.GetOrgID(), ID: cmd.DashboardId}
|
||
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "Invalid dashboard ID in annotation request", err)
|
||
}
|
||
cmd.DashboardUID = queryResult.UID
|
||
}
|
||
|
||
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardUID); err != nil || !canSave {
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Error while checking annotation permissions", err)
|
||
} else {
|
||
return response.Error(http.StatusForbidden, "Access denied to save the annotation", nil)
|
||
}
|
||
}
|
||
|
||
if cmd.Text == "" {
|
||
err := &AnnotationError{"text field should not be empty"}
|
||
return response.Error(http.StatusBadRequest, "Failed to save annotation", err)
|
||
}
|
||
|
||
userID, _ := identity.UserIdentifier(c.GetID())
|
||
item := annotations.Item{
|
||
OrgID: c.GetOrgID(),
|
||
UserID: userID,
|
||
DashboardID: cmd.DashboardId,
|
||
DashboardUID: cmd.DashboardUID,
|
||
PanelID: cmd.PanelId,
|
||
Epoch: cmd.Time,
|
||
EpochEnd: cmd.TimeEnd,
|
||
Text: cmd.Text,
|
||
Data: cmd.Data,
|
||
Tags: cmd.Tags,
|
||
}
|
||
|
||
if err := hs.annotationsRepo.Save(c.Req.Context(), &item); err != nil {
|
||
if errors.Is(err, annotations.ErrTimerangeMissing) {
|
||
return response.Error(http.StatusBadRequest, "Failed to save annotation", err)
|
||
}
|
||
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save annotation", err)
|
||
}
|
||
|
||
startID := item.ID
|
||
|
||
return response.JSON(http.StatusOK, util.DynMap{
|
||
"message": "Annotation added",
|
||
"id": startID,
|
||
})
|
||
}
|
||
|
||
func formatGraphiteAnnotation(what string, data string) string {
|
||
text := what
|
||
if data != "" {
|
||
text = text + "\n" + data
|
||
}
|
||
return text
|
||
}
|
||
|
||
// swagger:route POST /annotations/graphite annotations postGraphiteAnnotation
|
||
//
|
||
// Create Annotation in Graphite format.
|
||
//
|
||
// Creates an annotation by using Graphite-compatible event format. The `when` and `data` fields are optional. If `when` is not specified then the current time will be used as annotation’s timestamp. The `tags` field can also be in prior to Graphite `0.10.0` format (string with multiple tags being separated by a space).
|
||
//
|
||
// Responses:
|
||
// 200: postAnnotationResponse
|
||
// 400: badRequestError
|
||
// 401: unauthorisedError
|
||
// 403: forbiddenError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) PostGraphiteAnnotation(c *contextmodel.ReqContext) response.Response {
|
||
cmd := dtos.PostGraphiteAnnotationsCmd{}
|
||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||
}
|
||
if cmd.What == "" {
|
||
err := &AnnotationError{"what field should not be empty"}
|
||
return response.Error(http.StatusBadRequest, "Failed to save Graphite annotation", err)
|
||
}
|
||
|
||
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
|
||
|
||
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
|
||
var tagsArray []string
|
||
switch tags := cmd.Tags.(type) {
|
||
case string:
|
||
if tags != "" {
|
||
tagsArray = strings.Split(tags, " ")
|
||
} else {
|
||
tagsArray = []string{}
|
||
}
|
||
case []any:
|
||
for _, t := range tags {
|
||
if tagStr, ok := t.(string); ok {
|
||
tagsArray = append(tagsArray, tagStr)
|
||
} else {
|
||
err := &AnnotationError{"tag should be a string"}
|
||
return response.Error(http.StatusBadRequest, "Failed to save Graphite annotation", err)
|
||
}
|
||
}
|
||
default:
|
||
err := &AnnotationError{"unsupported tags format"}
|
||
return response.Error(http.StatusBadRequest, "Failed to save Graphite annotation", err)
|
||
}
|
||
|
||
userID, _ := identity.UserIdentifier(c.GetID())
|
||
item := annotations.Item{
|
||
OrgID: c.GetOrgID(),
|
||
UserID: userID,
|
||
Epoch: cmd.When * 1000,
|
||
Text: text,
|
||
Tags: tagsArray,
|
||
}
|
||
|
||
if err := hs.annotationsRepo.Save(c.Req.Context(), &item); err != nil {
|
||
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save Graphite annotation", err)
|
||
}
|
||
|
||
return response.JSON(http.StatusOK, util.DynMap{
|
||
"message": "Graphite annotation added",
|
||
"id": item.ID,
|
||
})
|
||
}
|
||
|
||
// swagger:route PUT /annotations/{annotation_id} annotations updateAnnotation
|
||
//
|
||
// Update Annotation.
|
||
//
|
||
// Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the Patch Annotation operation.
|
||
//
|
||
// Responses:
|
||
// 200: okResponse
|
||
// 400: badRequestError
|
||
// 401: unauthorisedError
|
||
// 403: forbiddenError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Response {
|
||
cmd := dtos.UpdateAnnotationsCmd{}
|
||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||
}
|
||
|
||
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
|
||
}
|
||
|
||
annotation, resp := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, annotationID, c.SignedInUser)
|
||
if resp != nil {
|
||
return resp
|
||
}
|
||
|
||
userID, _ := identity.UserIdentifier(c.GetID())
|
||
item := annotations.Item{
|
||
OrgID: c.GetOrgID(),
|
||
UserID: userID,
|
||
ID: annotationID,
|
||
Epoch: cmd.Time,
|
||
EpochEnd: cmd.TimeEnd,
|
||
Text: cmd.Text,
|
||
Tags: cmd.Tags,
|
||
Data: annotation.Data,
|
||
}
|
||
|
||
if cmd.Data != nil {
|
||
item.Data = cmd.Data
|
||
}
|
||
|
||
if err := hs.annotationsRepo.Update(c.Req.Context(), &item); err != nil {
|
||
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update annotation", err)
|
||
}
|
||
|
||
return response.Success("Annotation updated")
|
||
}
|
||
|
||
// swagger:route PATCH /annotations/{annotation_id} annotations patchAnnotation
|
||
//
|
||
// Patch Annotation.
|
||
//
|
||
// Updates one or more properties of an annotation that matches the specified ID.
|
||
// This operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties.
|
||
// This is available in Grafana 6.0.0-beta2 and above.
|
||
//
|
||
// Responses:
|
||
// 200: okResponse
|
||
// 401: unauthorisedError
|
||
// 403: forbiddenError
|
||
// 404: notFoundError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Response {
|
||
cmd := dtos.PatchAnnotationsCmd{}
|
||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||
}
|
||
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
|
||
}
|
||
|
||
annotation, resp := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, annotationID, c.SignedInUser)
|
||
if resp != nil {
|
||
return resp
|
||
}
|
||
|
||
userID, _ := identity.UserIdentifier(c.GetID())
|
||
existing := annotations.Item{
|
||
OrgID: c.GetOrgID(),
|
||
UserID: userID,
|
||
ID: annotationID,
|
||
Epoch: annotation.Time,
|
||
EpochEnd: annotation.TimeEnd,
|
||
Text: annotation.Text,
|
||
Tags: annotation.Tags,
|
||
Data: annotation.Data,
|
||
}
|
||
|
||
if cmd.Tags != nil {
|
||
existing.Tags = cmd.Tags
|
||
}
|
||
|
||
if cmd.Text != "" && cmd.Text != existing.Text {
|
||
existing.Text = cmd.Text
|
||
}
|
||
|
||
if cmd.Time > 0 && cmd.Time != existing.Epoch {
|
||
existing.Epoch = cmd.Time
|
||
}
|
||
|
||
if cmd.TimeEnd > 0 && cmd.TimeEnd != existing.EpochEnd {
|
||
existing.EpochEnd = cmd.TimeEnd
|
||
}
|
||
|
||
if cmd.Data != nil {
|
||
existing.Data = cmd.Data
|
||
}
|
||
|
||
if err := hs.annotationsRepo.Update(c.Req.Context(), &existing); err != nil {
|
||
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update annotation", err)
|
||
}
|
||
|
||
return response.Success("Annotation patched")
|
||
}
|
||
|
||
// swagger:route POST /annotations/mass-delete annotations massDeleteAnnotations
|
||
//
|
||
// Delete multiple annotations.
|
||
//
|
||
// Responses:
|
||
// 200: okResponse
|
||
// 401: unauthorisedError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) MassDeleteAnnotations(c *contextmodel.ReqContext) response.Response {
|
||
cmd := dtos.MassDeleteAnnotationsCmd{}
|
||
err := web.Bind(c.Req, &cmd)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||
}
|
||
|
||
if cmd.DashboardUID != "" {
|
||
query := dashboards.GetDashboardQuery{OrgID: c.GetOrgID(), UID: cmd.DashboardUID}
|
||
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
|
||
if err == nil {
|
||
cmd.DashboardId = queryResult.ID
|
||
}
|
||
}
|
||
|
||
if cmd.DashboardId != 0 && cmd.DashboardUID == "" {
|
||
query := dashboards.GetDashboardQuery{OrgID: c.GetOrgID(), ID: cmd.DashboardId}
|
||
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "Invalid dashboard ID in annotation request", err)
|
||
}
|
||
cmd.DashboardUID = queryResult.UID
|
||
}
|
||
|
||
if (cmd.DashboardId != 0 && cmd.PanelId == 0) || (cmd.PanelId != 0 && cmd.DashboardId == 0) {
|
||
err := &AnnotationError{message: "DashboardId and PanelId are both required for mass delete"}
|
||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||
}
|
||
|
||
var deleteParams *annotations.DeleteParams
|
||
|
||
// validations only for RBAC. A user can mass delete all annotations in a (dashboard + panel) or a specific annotation
|
||
// if has access to that dashboard.
|
||
var dashboardUID string
|
||
|
||
if cmd.AnnotationId != 0 {
|
||
annotation, respErr := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, cmd.AnnotationId, c.SignedInUser)
|
||
if respErr != nil {
|
||
return respErr
|
||
}
|
||
dashboardUID = *annotation.DashboardUID
|
||
deleteParams = &annotations.DeleteParams{
|
||
OrgID: c.GetOrgID(),
|
||
ID: cmd.AnnotationId,
|
||
}
|
||
} else {
|
||
dashboardUID = cmd.DashboardUID
|
||
deleteParams = &annotations.DeleteParams{
|
||
OrgID: c.GetOrgID(),
|
||
DashboardID: cmd.DashboardId,
|
||
DashboardUID: cmd.DashboardUID,
|
||
PanelID: cmd.PanelId,
|
||
}
|
||
}
|
||
|
||
canSave, err := hs.canMassDeleteAnnotations(c, dashboardUID)
|
||
if err != nil || !canSave {
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Error while checking annotation permissions", err)
|
||
} else {
|
||
return response.Error(http.StatusForbidden, "Access denied to mass delete annotations", nil)
|
||
}
|
||
}
|
||
|
||
err = hs.annotationsRepo.Delete(c.Req.Context(), deleteParams)
|
||
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Failed to delete annotations", err)
|
||
}
|
||
|
||
return response.Success("Annotations deleted")
|
||
}
|
||
|
||
// swagger:route GET /annotations/{annotation_id} annotations getAnnotationByID
|
||
//
|
||
// Get Annotation by ID.
|
||
//
|
||
// Responses:
|
||
// 200: getAnnotationByIDResponse
|
||
// 401: unauthorisedError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) GetAnnotationByID(c *contextmodel.ReqContext) response.Response {
|
||
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
|
||
}
|
||
|
||
annotation, resp := findAnnotationByID(c.Req.Context(), hs.annotationsRepo, annotationID, c.SignedInUser)
|
||
if resp != nil {
|
||
return resp
|
||
}
|
||
|
||
if annotation.Email != "" {
|
||
annotation.AvatarURL = dtos.GetGravatarUrl(hs.Cfg, annotation.Email)
|
||
}
|
||
|
||
return response.JSON(http.StatusOK, annotation)
|
||
}
|
||
|
||
// swagger:route DELETE /annotations/{annotation_id} annotations deleteAnnotationByID
|
||
//
|
||
// Delete Annotation By ID.
|
||
//
|
||
// Deletes the annotation that matches the specified ID.
|
||
//
|
||
// Responses:
|
||
// 200: okResponse
|
||
// 401: unauthorisedError
|
||
// 403: forbiddenError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.Response {
|
||
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
|
||
if err != nil {
|
||
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
|
||
}
|
||
|
||
err = hs.annotationsRepo.Delete(c.Req.Context(), &annotations.DeleteParams{
|
||
OrgID: c.GetOrgID(),
|
||
ID: annotationID,
|
||
})
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Failed to delete annotation", err)
|
||
}
|
||
|
||
return response.Success("Annotation deleted")
|
||
}
|
||
|
||
func findAnnotationByID(ctx context.Context, repo annotations.Repository, annotationID int64, user *user.SignedInUser) (*annotations.ItemDTO, response.Response) {
|
||
query := &annotations.ItemQuery{
|
||
AnnotationID: annotationID,
|
||
OrgID: user.OrgID,
|
||
SignedInUser: user,
|
||
}
|
||
items, err := repo.Find(ctx, query)
|
||
|
||
if err != nil {
|
||
return nil, response.Error(http.StatusInternalServerError, "Failed to find annotation", err)
|
||
}
|
||
|
||
if len(items) == 0 {
|
||
return nil, response.Error(http.StatusNotFound, "Annotation not found", nil)
|
||
}
|
||
|
||
return items[0], nil
|
||
}
|
||
|
||
// swagger:route GET /annotations/tags annotations getAnnotationTags
|
||
//
|
||
// Find Annotations Tags.
|
||
//
|
||
// Find all the event tags created in the annotations.
|
||
//
|
||
// Responses:
|
||
// 200: getAnnotationTagsResponse
|
||
// 401: unauthorisedError
|
||
// 500: internalServerError
|
||
func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Response {
|
||
query := &annotations.TagsQuery{
|
||
OrgID: c.GetOrgID(),
|
||
Tag: c.Query("tag"),
|
||
Limit: c.QueryInt64("limit"),
|
||
}
|
||
|
||
result, err := hs.annotationsRepo.FindTags(c.Req.Context(), query)
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Failed to find annotation tags", err)
|
||
}
|
||
|
||
return response.JSON(http.StatusOK, annotations.GetAnnotationTagsResponse{Result: result})
|
||
}
|
||
|
||
// AnnotationTypeScopeResolver provides an ScopeAttributeResolver able to
|
||
// resolve annotation types. Scope "annotations:id:<id>" will be translated to "annotations:type:<type>,
|
||
// where <type> is the type of annotation with id <id>.
|
||
// Dashboard annotation scope will be resolved to the corresponding
|
||
// dashboard and folder scopes (eg, "dashboards:uid:<annotation_dashboard_uid>", "folders:uid:<parent_folder_uid>" etc).
|
||
func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) {
|
||
prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("")
|
||
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||
scopeParts := strings.Split(initialScope, ":")
|
||
if scopeParts[0] != accesscontrol.ScopeAnnotationsRoot || len(scopeParts) != 3 {
|
||
return nil, accesscontrol.ErrInvalidScope
|
||
}
|
||
|
||
annotationIdStr := scopeParts[2]
|
||
annotationId, err := strconv.Atoi(annotationIdStr)
|
||
if err != nil {
|
||
return nil, accesscontrol.ErrInvalidScope
|
||
}
|
||
|
||
// tempUser is used to resolve annotation type.
|
||
// The annotation doesn't get returned to the real user, so real user's permissions don't matter here.
|
||
tempUser := &user.SignedInUser{
|
||
OrgID: orgID,
|
||
Permissions: map[int64]map[string][]string{
|
||
orgID: {
|
||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeOrganization, dashboards.ScopeDashboardsAll},
|
||
},
|
||
},
|
||
}
|
||
|
||
annotation, resp := findAnnotationByID(ctx, annotationsRepo, int64(annotationId), tempUser)
|
||
if resp != nil {
|
||
return nil, errors.New("could not resolve annotation type")
|
||
}
|
||
|
||
if annotation.DashboardUID == nil || *annotation.DashboardUID == "" {
|
||
return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil
|
||
} else {
|
||
return identity.WithServiceIdentityFn(ctx, orgID, func(ctx context.Context) ([]string, error) {
|
||
dashboard, err := dashSvc.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: *annotation.DashboardUID, OrgID: orgID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}
|
||
// Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder
|
||
if dashboard.FolderUID != "" {
|
||
scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID))
|
||
inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scopes = append(scopes, inheritedScopes...)
|
||
} else {
|
||
scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
|
||
}
|
||
return scopes, nil
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
|
||
if dashboardUID != "" {
|
||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
|
||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
||
} else { // organization annotations
|
||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
||
}
|
||
}
|
||
|
||
func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashboardUID string) (bool, error) {
|
||
if dashboardUID == "" {
|
||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
||
}
|
||
|
||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboardUID))
|
||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
||
}
|
||
|
||
// swagger:parameters getAnnotationByID
|
||
type GetAnnotationByIDParams struct {
|
||
// in:path
|
||
// required:true
|
||
AnnotationID string `json:"annotation_id"`
|
||
}
|
||
|
||
// swagger:parameters deleteAnnotationByID
|
||
type DeleteAnnotationByIDParams struct {
|
||
// in:path
|
||
// required:true
|
||
AnnotationID string `json:"annotation_id"`
|
||
}
|
||
|
||
// swagger:parameters getAnnotations
|
||
type GetAnnotationsParams struct {
|
||
// Find annotations created after specific epoch datetime in milliseconds.
|
||
// in:query
|
||
// required:false
|
||
From int64 `json:"from"`
|
||
// Find annotations created before specific epoch datetime in milliseconds.
|
||
// in:query
|
||
// required:false
|
||
To int64 `json:"to"`
|
||
// Limit response to annotations created by specific user.
|
||
// in:query
|
||
// required:false
|
||
UserID int64 `json:"userId"`
|
||
// Find annotations for a specified alert rule by its ID.
|
||
// deprecated: AlertID is deprecated and will be removed in future versions. Please use AlertUID instead.
|
||
// in:query
|
||
// required:false
|
||
AlertID int64 `json:"alertId"`
|
||
// Find annotations for a specified alert rule by its UID.
|
||
// in:query
|
||
// required:false
|
||
AlertUID string `json:"alertUID"`
|
||
// Find annotations that are scoped to a specific dashboard
|
||
// in:query
|
||
// required:false
|
||
DashboardID int64 `json:"dashboardId"`
|
||
// Find annotations that are scoped to a specific dashboard
|
||
// in:query
|
||
// required:false
|
||
DashboardUID string `json:"dashboardUID"`
|
||
// Find annotations that are scoped to a specific panel
|
||
// in:query
|
||
// required:false
|
||
PanelID int64 `json:"panelId"`
|
||
// Max limit for results returned.
|
||
// in:query
|
||
// required:false
|
||
Limit int64 `json:"limit"`
|
||
// Use this to filter organization annotations. Organization annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. You can filter by multiple tags.
|
||
// in:query
|
||
// required:false
|
||
// type: array
|
||
// collectionFormat: multi
|
||
Tags []string `json:"tags"`
|
||
// Return alerts or user created annotations
|
||
// in:query
|
||
// required:false
|
||
// Description:
|
||
// * `alert`
|
||
// * `annotation`
|
||
// enum: alert,annotation
|
||
Type string `json:"type"`
|
||
// Match any or all tags
|
||
// in:query
|
||
// required:false
|
||
MatchAny bool `json:"matchAny"`
|
||
}
|
||
|
||
// swagger:parameters getAnnotationTags
|
||
type GetAnnotationTagsParams struct {
|
||
// Tag is a string that you can use to filter tags.
|
||
// in:query
|
||
// required:false
|
||
Tag string `json:"tag"`
|
||
// Max limit for results returned.
|
||
// in:query
|
||
// required:false
|
||
// default: 100
|
||
Limit string `json:"limit"`
|
||
}
|
||
|
||
// swagger:parameters massDeleteAnnotations
|
||
type MassDeleteAnnotationsParams struct {
|
||
// in:body
|
||
// required:true
|
||
Body dtos.MassDeleteAnnotationsCmd `json:"body"`
|
||
}
|
||
|
||
// swagger:parameters postAnnotation
|
||
type PostAnnotationParams struct {
|
||
// in:body
|
||
// required:true
|
||
Body dtos.PostAnnotationsCmd `json:"body"`
|
||
}
|
||
|
||
// swagger:parameters postGraphiteAnnotation
|
||
type PostGraphiteAnnotationParams struct {
|
||
// in:body
|
||
// required:true
|
||
Body dtos.PostGraphiteAnnotationsCmd `json:"body"`
|
||
}
|
||
|
||
// swagger:parameters updateAnnotation
|
||
type UpdateAnnotationParams struct {
|
||
// in:path
|
||
// required:true
|
||
AnnotationID string `json:"annotation_id"`
|
||
// in:body
|
||
// required:true
|
||
Body dtos.UpdateAnnotationsCmd `json:"body"`
|
||
}
|
||
|
||
// swagger:parameters patchAnnotation
|
||
type PatchAnnotationParams struct {
|
||
// in:path
|
||
// required:true
|
||
AnnotationID string `json:"annotation_id"`
|
||
// in:body
|
||
// required:true
|
||
Body dtos.PatchAnnotationsCmd `json:"body"`
|
||
}
|
||
|
||
// swagger:response getAnnotationsResponse
|
||
type GetAnnotationsResponse struct {
|
||
// The response message
|
||
// in: body
|
||
Body []*annotations.ItemDTO `json:"body"`
|
||
}
|
||
|
||
// swagger:response getAnnotationByIDResponse
|
||
type GetAnnotationByIDResponse struct {
|
||
// The response message
|
||
// in: body
|
||
Body *annotations.ItemDTO `json:"body"`
|
||
}
|
||
|
||
// swagger:response postAnnotationResponse
|
||
type PostAnnotationResponse struct {
|
||
// The response message
|
||
// in: body
|
||
Body struct {
|
||
// ID Identifier of the created annotation.
|
||
// required: true
|
||
// example: 65
|
||
ID int64 `json:"id"`
|
||
|
||
// Message Message of the created annotation.
|
||
// required: true
|
||
Message string `json:"message"`
|
||
} `json:"body"`
|
||
}
|
||
|
||
// swagger:response getAnnotationTagsResponse
|
||
type GetAnnotationTagsResponse struct {
|
||
// The response message
|
||
// in: body
|
||
Body annotations.GetAnnotationTagsResponse `json:"body"`
|
||
}
|