mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 10:02:13 +08:00
Chore: remove comments feature (#64644)
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -93,7 +93,6 @@
|
|||||||
/pkg/services/annotations/ @grafana/backend-platform
|
/pkg/services/annotations/ @grafana/backend-platform
|
||||||
/pkg/services/apikey/ @grafana/backend-platform
|
/pkg/services/apikey/ @grafana/backend-platform
|
||||||
/pkg/services/cleanup/ @grafana/backend-platform
|
/pkg/services/cleanup/ @grafana/backend-platform
|
||||||
/pkg/services/comments/ @grafana/backend-platform
|
|
||||||
/pkg/services/contexthandler/ @grafana/backend-platform
|
/pkg/services/contexthandler/ @grafana/backend-platform
|
||||||
/pkg/services/correlations/ @grafana/backend-platform
|
/pkg/services/correlations/ @grafana/backend-platform
|
||||||
/pkg/services/dashboardimport/ @grafana/backend-platform
|
/pkg/services/dashboardimport/ @grafana/backend-platform
|
||||||
@ -347,7 +346,6 @@ lerna.json @grafana/frontend-ops
|
|||||||
/public/app/features/api-keys/ @grafana/user-essentials
|
/public/app/features/api-keys/ @grafana/user-essentials
|
||||||
/public/app/features/canvas/ @grafana/dataviz-squad
|
/public/app/features/canvas/ @grafana/dataviz-squad
|
||||||
/public/app/features/commandPalette/ @grafana/user-essentials
|
/public/app/features/commandPalette/ @grafana/user-essentials
|
||||||
/public/app/features/comments/ @grafana/dataviz-squad
|
|
||||||
/public/app/features/connections/ @grafana/plugins-platform-frontend
|
/public/app/features/connections/ @grafana/plugins-platform-frontend
|
||||||
/public/app/features/correlations/ @grafana/explore-squad
|
/public/app/features/correlations/ @grafana/explore-squad
|
||||||
/public/app/features/dashboard/ @grafana/dashboards-squad
|
/public/app/features/dashboard/ @grafana/dashboards-squad
|
||||||
|
@ -62,8 +62,6 @@ Alpha features might be changed or removed without prior notice.
|
|||||||
| `publicDashboardsEmailSharing` | Enables public dashboard sharing to be restricted to only allowed emails |
|
| `publicDashboardsEmailSharing` | Enables public dashboard sharing to be restricted to only allowed emails |
|
||||||
| `lokiLive` | Support WebSocket streaming for loki (early prototype) |
|
| `lokiLive` | Support WebSocket streaming for loki (early prototype) |
|
||||||
| `lokiDataframeApi` | Use experimental loki api for WebSocket streaming (early prototype) |
|
| `lokiDataframeApi` | Use experimental loki api for WebSocket streaming (early prototype) |
|
||||||
| `dashboardComments` | Enable dashboard-wide comments |
|
|
||||||
| `annotationComments` | Enable annotation comments |
|
|
||||||
| `storage` | Configurable storage for dashboards, datasources, and resources |
|
| `storage` | Configurable storage for dashboards, datasources, and resources |
|
||||||
| `exploreMixedDatasource` | Enable mixed datasource in Explore |
|
| `exploreMixedDatasource` | Enable mixed datasource in Explore |
|
||||||
| `tracing` | Adds trace ID to error notifications |
|
| `tracing` | Adds trace ID to error notifications |
|
||||||
|
@ -31,8 +31,6 @@ export interface FeatureToggles {
|
|||||||
lokiLive?: boolean;
|
lokiLive?: boolean;
|
||||||
lokiDataframeApi?: boolean;
|
lokiDataframeApi?: boolean;
|
||||||
featureHighlights?: boolean;
|
featureHighlights?: boolean;
|
||||||
dashboardComments?: boolean;
|
|
||||||
annotationComments?: boolean;
|
|
||||||
migrationLocking?: boolean;
|
migrationLocking?: boolean;
|
||||||
storage?: boolean;
|
storage?: boolean;
|
||||||
k8s?: boolean;
|
k8s?: boolean;
|
||||||
|
@ -617,11 +617,6 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
|
|
||||||
// short urls
|
// short urls
|
||||||
apiRoute.Post("/short-urls", routing.Wrap(hs.createShortURL))
|
apiRoute.Post("/short-urls", routing.Wrap(hs.createShortURL))
|
||||||
|
|
||||||
apiRoute.Group("/comments", func(commentRoute routing.RouteRegister) {
|
|
||||||
commentRoute.Post("/get", routing.Wrap(hs.commentsGet))
|
|
||||||
commentRoute.Post("/create", routing.Wrap(hs.commentsCreate))
|
|
||||||
})
|
|
||||||
}, reqSignedIn)
|
}, reqSignedIn)
|
||||||
|
|
||||||
// admin api
|
// admin api
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments"
|
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (hs *HTTPServer) commentsGet(c *contextmodel.ReqContext) response.Response {
|
|
||||||
cmd := comments.GetCmd{}
|
|
||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
||||||
}
|
|
||||||
items, err := hs.commentsService.Get(c.Req.Context(), c.OrgID, c.SignedInUser, cmd)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, comments.ErrPermissionDenied) {
|
|
||||||
return response.Error(http.StatusForbidden, "permission denied", err)
|
|
||||||
}
|
|
||||||
return response.Error(http.StatusInternalServerError, "internal error", err)
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, util.DynMap{
|
|
||||||
"comments": items,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs *HTTPServer) commentsCreate(c *contextmodel.ReqContext) response.Response {
|
|
||||||
cmd := comments.CreateCmd{}
|
|
||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
||||||
}
|
|
||||||
if c.SignedInUser.UserID == 0 && !c.SignedInUser.HasRole(org.RoleAdmin) {
|
|
||||||
return response.Error(http.StatusForbidden, "admin role required", nil)
|
|
||||||
}
|
|
||||||
comment, err := hs.commentsService.Create(c.Req.Context(), c.OrgID, c.SignedInUser, cmd)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, comments.ErrPermissionDenied) {
|
|
||||||
return response.Error(http.StatusForbidden, "permission denied", err)
|
|
||||||
}
|
|
||||||
return response.Error(http.StatusInternalServerError, "internal error", err)
|
|
||||||
}
|
|
||||||
return response.JSON(http.StatusOK, util.DynMap{
|
|
||||||
"comment": comment,
|
|
||||||
})
|
|
||||||
}
|
|
@ -41,7 +41,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/auth"
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
"github.com/grafana/grafana/pkg/services/comments"
|
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
"github.com/grafana/grafana/pkg/services/correlations"
|
"github.com/grafana/grafana/pkg/services/correlations"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
@ -181,7 +180,6 @@ type HTTPServer struct {
|
|||||||
dashboardProvisioningService dashboards.DashboardProvisioningService
|
dashboardProvisioningService dashboards.DashboardProvisioningService
|
||||||
folderService folder.Service
|
folderService folder.Service
|
||||||
DatasourcePermissionsService permissions.DatasourcePermissionsService
|
DatasourcePermissionsService permissions.DatasourcePermissionsService
|
||||||
commentsService *comments.Service
|
|
||||||
AlertNotificationService *alerting.AlertNotificationService
|
AlertNotificationService *alerting.AlertNotificationService
|
||||||
dashboardsnapshotsService dashboardsnapshots.Service
|
dashboardsnapshotsService dashboardsnapshots.Service
|
||||||
PluginSettings pluginSettings.Service
|
PluginSettings pluginSettings.Service
|
||||||
@ -241,7 +239,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
|
notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService,
|
||||||
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
|
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
|
||||||
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
|
datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService,
|
||||||
dashboardsnapshotsService dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings pluginSettings.Service,
|
dashboardsnapshotsService dashboardsnapshots.Service, pluginSettings pluginSettings.Service,
|
||||||
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
|
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
|
||||||
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
|
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
|
||||||
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
|
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
|
||||||
@ -328,7 +326,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
dashboardProvisioningService: dashboardProvisioningService,
|
dashboardProvisioningService: dashboardProvisioningService,
|
||||||
folderService: folderService,
|
folderService: folderService,
|
||||||
DatasourcePermissionsService: datasourcePermissionsService,
|
DatasourcePermissionsService: datasourcePermissionsService,
|
||||||
commentsService: commentsService,
|
|
||||||
teamPermissionsService: teamsPermissionsService,
|
teamPermissionsService: teamsPermissionsService,
|
||||||
AlertNotificationService: alertNotificationService,
|
AlertNotificationService: alertNotificationService,
|
||||||
dashboardsnapshotsService: dashboardsnapshotsService,
|
dashboardsnapshotsService: dashboardsnapshotsService,
|
||||||
|
@ -42,7 +42,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/jwt"
|
"github.com/grafana/grafana/pkg/services/auth/jwt"
|
||||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
"github.com/grafana/grafana/pkg/services/comments"
|
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
|
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
"github.com/grafana/grafana/pkg/services/dashboardimport"
|
||||||
@ -265,7 +264,6 @@ var wireSet = wire.NewSet(
|
|||||||
plugindashboardsservice.ProvideDashboardUpdater,
|
plugindashboardsservice.ProvideDashboardUpdater,
|
||||||
alerting.ProvideDashAlertExtractorService,
|
alerting.ProvideDashAlertExtractorService,
|
||||||
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
|
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
|
||||||
comments.ProvideService,
|
|
||||||
guardian.ProvideService,
|
guardian.ProvideService,
|
||||||
sanitizer.ProvideService,
|
sanitizer.ProvideService,
|
||||||
secretsStore.ProvideService,
|
secretsStore.ProvideService,
|
||||||
|
@ -42,7 +42,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/authn/authnimpl"
|
"github.com/grafana/grafana/pkg/services/authn/authnimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
"github.com/grafana/grafana/pkg/services/comments"
|
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
|
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/correlations"
|
"github.com/grafana/grafana/pkg/services/correlations"
|
||||||
@ -305,7 +304,6 @@ var wireBasicSet = wire.NewSet(
|
|||||||
plugindashboardsservice.ProvideDashboardUpdater,
|
plugindashboardsservice.ProvideDashboardUpdater,
|
||||||
alerting.ProvideDashAlertExtractorService,
|
alerting.ProvideDashAlertExtractorService,
|
||||||
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
|
wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)),
|
||||||
comments.ProvideService,
|
|
||||||
guardian.ProvideService,
|
guardian.ProvideService,
|
||||||
sanitizer.ProvideService,
|
sanitizer.ProvideService,
|
||||||
secretsStore.ProvideService,
|
secretsStore.ProvideService,
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
package commentmodel
|
|
||||||
|
|
||||||
type EventType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
EventCommentCreated EventType = "commentCreated"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Event represents comment event structure.
|
|
||||||
type Event struct {
|
|
||||||
Event EventType `json:"event"`
|
|
||||||
CommentCreated *CommentDto `json:"commentCreated"`
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
package commentmodel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ObjectTypeOrg is reserved for future use for per-org comments.
|
|
||||||
ObjectTypeOrg = "org"
|
|
||||||
// ObjectTypeDashboard used for dashboard-wide comments.
|
|
||||||
ObjectTypeDashboard = "dashboard"
|
|
||||||
// ObjectTypeAnnotation used for annotation comments.
|
|
||||||
ObjectTypeAnnotation = "annotation"
|
|
||||||
)
|
|
||||||
|
|
||||||
var RegisteredObjectTypes = map[string]struct{}{
|
|
||||||
ObjectTypeOrg: {},
|
|
||||||
ObjectTypeDashboard: {},
|
|
||||||
ObjectTypeAnnotation: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommentGroup struct {
|
|
||||||
Id int64
|
|
||||||
OrgId int64
|
|
||||||
ObjectType string
|
|
||||||
ObjectId string
|
|
||||||
Settings Settings
|
|
||||||
|
|
||||||
Created int64
|
|
||||||
Updated int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i CommentGroup) TableName() string {
|
|
||||||
return "comment_group"
|
|
||||||
}
|
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ driver.Valuer = Settings{}
|
|
||||||
_ sql.Scanner = &Settings{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s Settings) Value() (driver.Value, error) {
|
|
||||||
d, err := json.Marshal(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return string(d), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Settings) Scan(value interface{}) error {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
return json.Unmarshal([]byte(v), s)
|
|
||||||
case []uint8:
|
|
||||||
return json.Unmarshal(v, s)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("type assertion on scan failed: got %T", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comment struct {
|
|
||||||
Id int64
|
|
||||||
GroupId int64
|
|
||||||
UserId int64
|
|
||||||
Content string
|
|
||||||
|
|
||||||
Created int64
|
|
||||||
Updated int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommentUser struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Login string `json:"login"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
AvatarUrl string `json:"avatarUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommentDto struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
UserId int64 `json:"userId"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Created int64 `json:"created"`
|
|
||||||
User *CommentUser `json:"user,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i Comment) ToDTO(user *CommentUser) *CommentDto {
|
|
||||||
return &CommentDto{
|
|
||||||
Id: i.Id,
|
|
||||||
UserId: i.UserId,
|
|
||||||
Content: i.Content,
|
|
||||||
Created: i.Created,
|
|
||||||
User: user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i Comment) TableName() string {
|
|
||||||
return "comment"
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
package commentmodel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PermissionChecker struct {
|
|
||||||
sqlStore db.DB
|
|
||||||
features featuremgmt.FeatureToggles
|
|
||||||
accessControl accesscontrol.AccessControl
|
|
||||||
dashboardService dashboards.DashboardService
|
|
||||||
annotationsRepo annotations.Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPermissionChecker(sqlStore db.DB, features featuremgmt.FeatureToggles,
|
|
||||||
accessControl accesscontrol.AccessControl, dashboardService dashboards.DashboardService,
|
|
||||||
annotationsRepo annotations.Repository,
|
|
||||||
) *PermissionChecker {
|
|
||||||
return &PermissionChecker{sqlStore: sqlStore, features: features, accessControl: accessControl, annotationsRepo: annotationsRepo}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PermissionChecker) getDashboardByUid(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
|
|
||||||
query := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID}
|
|
||||||
queryResult, err := c.dashboardService.GetDashboard(ctx, &query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return queryResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PermissionChecker) getDashboardById(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error) {
|
|
||||||
query := dashboards.GetDashboardQuery{ID: id, OrgID: orgID}
|
|
||||||
queryResult, err := c.dashboardService.GetDashboard(ctx, &query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return queryResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PermissionChecker) CheckReadPermissions(ctx context.Context, orgId int64, signedInUser *user.SignedInUser, objectType string, objectID string) (bool, error) {
|
|
||||||
switch objectType {
|
|
||||||
case ObjectTypeOrg:
|
|
||||||
return false, nil
|
|
||||||
case ObjectTypeDashboard:
|
|
||||||
if !c.features.IsEnabled(featuremgmt.FlagDashboardComments) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dash, err := c.getDashboardByUid(ctx, orgId, objectID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if ok, err := guard.CanView(); err != nil || !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
case ObjectTypeAnnotation:
|
|
||||||
if !c.features.IsEnabled(featuremgmt.FlagAnnotationComments) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
annotationID, err := strconv.ParseInt(objectID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
items, err := c.annotationsRepo.Find(ctx, &annotations.ItemQuery{AnnotationID: annotationID, OrgID: orgId, SignedInUser: signedInUser})
|
|
||||||
if err != nil || len(items) != 1 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dashboardID := items[0].DashboardID
|
|
||||||
if dashboardID == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dash, err := c.getDashboardById(ctx, orgId, dashboardID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if ok, err := guard.CanView(); err != nil || !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PermissionChecker) CheckWritePermissions(ctx context.Context, orgId int64, signedInUser *user.SignedInUser, objectType string, objectID string) (bool, error) {
|
|
||||||
switch objectType {
|
|
||||||
case ObjectTypeOrg:
|
|
||||||
return false, nil
|
|
||||||
case ObjectTypeDashboard:
|
|
||||||
if !c.features.IsEnabled(featuremgmt.FlagDashboardComments) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dash, err := c.getDashboardByUid(ctx, orgId, objectID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if ok, err := guard.CanEdit(); err != nil || !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
case ObjectTypeAnnotation:
|
|
||||||
if !c.features.IsEnabled(featuremgmt.FlagAnnotationComments) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if !c.accessControl.IsDisabled() {
|
|
||||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, accesscontrol.ScopeAnnotationsTypeDashboard)
|
|
||||||
if canEdit, err := c.accessControl.Evaluate(ctx, signedInUser, evaluator); err != nil || !canEdit {
|
|
||||||
return canEdit, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
annotationID, err := strconv.ParseInt(objectID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
items, err := c.annotationsRepo.Find(ctx, &annotations.ItemQuery{AnnotationID: annotationID, OrgID: orgId, SignedInUser: signedInUser})
|
|
||||||
if err != nil || len(items) != 1 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dashboardID := items[0].DashboardID
|
|
||||||
if dashboardID == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
dash, err := c.getDashboardById(ctx, orgId, dashboardID)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
guard, err := guardian.NewByDashboard(ctx, dash, orgId, signedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if ok, err := guard.CanEdit(); err != nil || !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
@ -1,171 +0,0 @@
|
|||||||
package comments
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
func commentsToDto(items []*commentmodel.Comment, userMap map[int64]*commentmodel.CommentUser) []*commentmodel.CommentDto {
|
|
||||||
result := make([]*commentmodel.CommentDto, 0, len(items))
|
|
||||||
for _, m := range items {
|
|
||||||
result = append(result, commentToDto(m, userMap))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func commentToDto(comment *commentmodel.Comment, userMap map[int64]*commentmodel.CommentUser) *commentmodel.CommentDto {
|
|
||||||
var u *commentmodel.CommentUser
|
|
||||||
if comment.UserId > 0 {
|
|
||||||
var ok bool
|
|
||||||
u, ok = userMap[comment.UserId]
|
|
||||||
if !ok {
|
|
||||||
// TODO: handle this gracefully?
|
|
||||||
u = &commentmodel.CommentUser{
|
|
||||||
Id: comment.UserId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return comment.ToDTO(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchUserToCommentUser(searchUser *user.UserSearchHitDTO) *commentmodel.CommentUser {
|
|
||||||
if searchUser == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &commentmodel.CommentUser{
|
|
||||||
Id: searchUser.ID,
|
|
||||||
Name: searchUser.Name,
|
|
||||||
Login: searchUser.Login,
|
|
||||||
Email: searchUser.Email,
|
|
||||||
AvatarUrl: dtos.GetGravatarUrl(searchUser.Email),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserIDFilter struct {
|
|
||||||
userIDs []int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIDFilter(userIDs []int64) user.Filter {
|
|
||||||
return &UserIDFilter{userIDs: userIDs}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *UserIDFilter) WhereCondition() *user.WhereCondition {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *UserIDFilter) JoinCondition() *user.JoinCondition {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *UserIDFilter) InCondition() *user.InCondition {
|
|
||||||
return &user.InCondition{
|
|
||||||
Condition: "u.id",
|
|
||||||
Params: a.userIDs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetCmd struct {
|
|
||||||
ObjectType string `json:"objectType"`
|
|
||||||
ObjectID string `json:"objectId"`
|
|
||||||
Limit uint `json:"limit"`
|
|
||||||
BeforeId int64 `json:"beforeId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateCmd struct {
|
|
||||||
ObjectType string `json:"objectType"`
|
|
||||||
ObjectID string `json:"objectId"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrPermissionDenied = errors.New("permission denied")
|
|
||||||
|
|
||||||
func (s *Service) Create(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, cmd CreateCmd) (*commentmodel.CommentDto, error) {
|
|
||||||
ok, err := s.permissions.CheckWritePermissions(ctx, orgID, signedInUser, cmd.ObjectType, cmd.ObjectID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
userMap := make(map[int64]*commentmodel.CommentUser, 1)
|
|
||||||
if signedInUser.UserID > 0 {
|
|
||||||
userMap[signedInUser.UserID] = &commentmodel.CommentUser{
|
|
||||||
Id: signedInUser.UserID,
|
|
||||||
Name: signedInUser.Name,
|
|
||||||
Login: signedInUser.Login,
|
|
||||||
Email: signedInUser.Email,
|
|
||||||
AvatarUrl: dtos.GetGravatarUrl(signedInUser.Email),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := s.storage.Create(ctx, orgID, cmd.ObjectType, cmd.ObjectID, signedInUser.UserID, cmd.Content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mDto := commentToDto(m, userMap)
|
|
||||||
e := commentmodel.Event{
|
|
||||||
Event: commentmodel.EventCommentCreated,
|
|
||||||
CommentCreated: mDto,
|
|
||||||
}
|
|
||||||
eventJSON, _ := json.Marshal(e)
|
|
||||||
_ = s.live.Publish(orgID, fmt.Sprintf("grafana/comment/%s/%s", cmd.ObjectType, cmd.ObjectID), eventJSON)
|
|
||||||
return mDto, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Get(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, cmd GetCmd) ([]*commentmodel.CommentDto, error) {
|
|
||||||
var res *user.SearchUserQueryResult
|
|
||||||
ok, err := s.permissions.CheckReadPermissions(ctx, orgID, signedInUser, cmd.ObjectType, cmd.ObjectID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
messages, err := s.storage.Get(ctx, orgID, cmd.ObjectType, cmd.ObjectID, GetFilter{
|
|
||||||
Limit: cmd.Limit,
|
|
||||||
BeforeID: cmd.BeforeId,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userIds := make([]int64, 0, len(messages))
|
|
||||||
for _, m := range messages {
|
|
||||||
if m.UserId <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
userIds = append(userIds, m.UserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: probably replace with comment and user table join.
|
|
||||||
query := &user.SearchUsersQuery{
|
|
||||||
Query: "",
|
|
||||||
Page: 0,
|
|
||||||
Limit: len(userIds),
|
|
||||||
SignedInUser: signedInUser,
|
|
||||||
Filters: []user.Filter{NewIDFilter(userIds)},
|
|
||||||
}
|
|
||||||
if res, err = s.userService.Search(ctx, query); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userMap := make(map[int64]*commentmodel.CommentUser, len(res.Users))
|
|
||||||
for _, v := range res.Users {
|
|
||||||
userMap[v.ID] = searchUserToCommentUser(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := commentsToDto(messages, userMap)
|
|
||||||
sort.Slice(result, func(i, j int) bool {
|
|
||||||
return result[i].Id < result[j].Id
|
|
||||||
})
|
|
||||||
return result, nil
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package comments
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/live"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
cfg *setting.Cfg
|
|
||||||
live *live.GrafanaLive
|
|
||||||
sqlStore db.DB
|
|
||||||
storage Storage
|
|
||||||
permissions *commentmodel.PermissionChecker
|
|
||||||
userService user.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, store db.DB, live *live.GrafanaLive,
|
|
||||||
features featuremgmt.FeatureToggles, accessControl accesscontrol.AccessControl,
|
|
||||||
dashboardService dashboards.DashboardService, userService user.Service, annotationsRepo annotations.Repository) *Service {
|
|
||||||
s := &Service{
|
|
||||||
cfg: cfg,
|
|
||||||
live: live,
|
|
||||||
sqlStore: store,
|
|
||||||
storage: &sqlStorage{
|
|
||||||
sql: store,
|
|
||||||
},
|
|
||||||
permissions: commentmodel.NewPermissionChecker(store, features, accessControl, dashboardService, annotationsRepo),
|
|
||||||
userService: userService,
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run Service.
|
|
||||||
func (s *Service) Run(ctx context.Context) error {
|
|
||||||
<-ctx.Done()
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
package comments
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sqlStorage struct {
|
|
||||||
sql db.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkObjectType(contentType string) bool {
|
|
||||||
_, ok := commentmodel.RegisteredObjectTypes[contentType]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkObjectID(objectID string) bool {
|
|
||||||
return objectID != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sqlStorage) Create(ctx context.Context, orgID int64, objectType string, objectID string, userID int64, content string) (*commentmodel.Comment, error) {
|
|
||||||
if !checkObjectType(objectType) {
|
|
||||||
return nil, errUnknownObjectType
|
|
||||||
}
|
|
||||||
if !checkObjectID(objectID) {
|
|
||||||
return nil, errEmptyObjectID
|
|
||||||
}
|
|
||||||
if content == "" {
|
|
||||||
return nil, errEmptyContent
|
|
||||||
}
|
|
||||||
|
|
||||||
var result *commentmodel.Comment
|
|
||||||
|
|
||||||
return result, s.sql.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
|
||||||
var group commentmodel.CommentGroup
|
|
||||||
has, err := dbSession.NoAutoCondition().Where(
|
|
||||||
"org_id=? AND object_type=? AND object_id=?",
|
|
||||||
orgID, objectType, objectID,
|
|
||||||
).Get(&group)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nowUnix := time.Now().Unix()
|
|
||||||
|
|
||||||
groupID := group.Id
|
|
||||||
if !has {
|
|
||||||
group.OrgId = orgID
|
|
||||||
group.ObjectType = objectType
|
|
||||||
group.ObjectId = objectID
|
|
||||||
group.Created = nowUnix
|
|
||||||
group.Updated = nowUnix
|
|
||||||
group.Settings = commentmodel.Settings{}
|
|
||||||
_, err = dbSession.Insert(&group)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
groupID = group.Id
|
|
||||||
}
|
|
||||||
message := commentmodel.Comment{
|
|
||||||
GroupId: groupID,
|
|
||||||
UserId: userID,
|
|
||||||
Content: content,
|
|
||||||
Created: nowUnix,
|
|
||||||
Updated: nowUnix,
|
|
||||||
}
|
|
||||||
_, err = dbSession.Insert(&message)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result = &message
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLimit = 300
|
|
||||||
|
|
||||||
func (s *sqlStorage) Get(ctx context.Context, orgID int64, objectType string, objectID string, filter GetFilter) ([]*commentmodel.Comment, error) {
|
|
||||||
if !checkObjectType(objectType) {
|
|
||||||
return nil, errUnknownObjectType
|
|
||||||
}
|
|
||||||
if !checkObjectID(objectID) {
|
|
||||||
return nil, errEmptyObjectID
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []*commentmodel.Comment
|
|
||||||
|
|
||||||
limit := 100
|
|
||||||
if filter.Limit > 0 {
|
|
||||||
limit = int(filter.Limit)
|
|
||||||
if limit > maxLimit {
|
|
||||||
limit = maxLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, s.sql.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
|
||||||
var group commentmodel.CommentGroup
|
|
||||||
has, err := dbSession.NoAutoCondition().Where(
|
|
||||||
"org_id=? AND object_type=? AND object_id=?",
|
|
||||||
orgID, objectType, objectID,
|
|
||||||
).Get(&group)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !has {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
clause := dbSession.Where("group_id=?", group.Id)
|
|
||||||
if filter.BeforeID > 0 {
|
|
||||||
clause.Where("id < ?", filter.BeforeID)
|
|
||||||
}
|
|
||||||
return clause.OrderBy("id desc").Limit(limit).Find(&result)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package comments
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createSqlStorage(t *testing.T) Storage {
|
|
||||||
t.Helper()
|
|
||||||
sqlStore := db.InitTestDB(t)
|
|
||||||
return &sqlStorage{
|
|
||||||
sql: sqlStore,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSqlStorage(t *testing.T) {
|
|
||||||
s := createSqlStorage(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
items, err := s.Get(ctx, 1, commentmodel.ObjectTypeOrg, "2", GetFilter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, items, 0)
|
|
||||||
|
|
||||||
numComments := 10
|
|
||||||
|
|
||||||
for i := 0; i < numComments; i++ {
|
|
||||||
comment, err := s.Create(ctx, 1, commentmodel.ObjectTypeOrg, "2", 1, "test"+strconv.Itoa(i))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, comment)
|
|
||||||
require.True(t, comment.Id > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
items, err = s.Get(ctx, 1, commentmodel.ObjectTypeOrg, "2", GetFilter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, items, 10)
|
|
||||||
require.Equal(t, "test9", items[0].Content)
|
|
||||||
require.Equal(t, "test0", items[9].Content)
|
|
||||||
require.Equal(t, int64(1), items[0].UserId)
|
|
||||||
require.NotZero(t, items[0].Created)
|
|
||||||
require.NotZero(t, items[0].Updated)
|
|
||||||
|
|
||||||
// Same object, but another content type.
|
|
||||||
items, err = s.Get(ctx, 1, commentmodel.ObjectTypeDashboard, "2", GetFilter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, items, 0)
|
|
||||||
|
|
||||||
// Now test filtering.
|
|
||||||
items, err = s.Get(ctx, 1, commentmodel.ObjectTypeOrg, "2", GetFilter{
|
|
||||||
Limit: 5,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, items, 5)
|
|
||||||
require.Equal(t, "test9", items[0].Content)
|
|
||||||
require.Equal(t, "test5", items[4].Content)
|
|
||||||
|
|
||||||
items, err = s.Get(ctx, 1, commentmodel.ObjectTypeOrg, "2", GetFilter{
|
|
||||||
Limit: 5,
|
|
||||||
BeforeID: items[4].Id,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, items, 5)
|
|
||||||
require.Equal(t, "test4", items[0].Content)
|
|
||||||
require.Equal(t, "test0", items[4].Content)
|
|
||||||
|
|
||||||
items, err = s.Get(ctx, 1, commentmodel.ObjectTypeOrg, "2", GetFilter{
|
|
||||||
Limit: 5,
|
|
||||||
BeforeID: items[4].Id,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, items, 0)
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package comments
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetFilter struct {
|
|
||||||
Limit uint
|
|
||||||
BeforeID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errUnknownObjectType = errors.New("unknown object type")
|
|
||||||
errEmptyObjectID = errors.New("empty object id")
|
|
||||||
errEmptyContent = errors.New("empty comment content")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Storage interface {
|
|
||||||
Get(ctx context.Context, orgID int64, objectType string, objectID string, filter GetFilter) ([]*commentmodel.Comment, error)
|
|
||||||
Create(ctx context.Context, orgID int64, objectType string, objectID string, userID int64, content string) (*commentmodel.Comment, error)
|
|
||||||
}
|
|
@ -101,18 +101,6 @@ var (
|
|||||||
State: FeatureStateStable,
|
State: FeatureStateStable,
|
||||||
Owner: grafanaAsCodeSquad,
|
Owner: grafanaAsCodeSquad,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "dashboardComments",
|
|
||||||
Description: "Enable dashboard-wide comments",
|
|
||||||
State: FeatureStateAlpha,
|
|
||||||
Owner: grafanaAppPlatformSquad,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "annotationComments",
|
|
||||||
Description: "Enable annotation comments",
|
|
||||||
State: FeatureStateAlpha,
|
|
||||||
Owner: grafanaAppPlatformSquad,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "migrationLocking",
|
Name: "migrationLocking",
|
||||||
Description: "Lock database during migrations",
|
Description: "Lock database during migrations",
|
||||||
|
@ -67,14 +67,6 @@ const (
|
|||||||
// Highlight Grafana Enterprise features
|
// Highlight Grafana Enterprise features
|
||||||
FlagFeatureHighlights = "featureHighlights"
|
FlagFeatureHighlights = "featureHighlights"
|
||||||
|
|
||||||
// FlagDashboardComments
|
|
||||||
// Enable dashboard-wide comments
|
|
||||||
FlagDashboardComments = "dashboardComments"
|
|
||||||
|
|
||||||
// FlagAnnotationComments
|
|
||||||
// Enable annotation comments
|
|
||||||
FlagAnnotationComments = "annotationComments"
|
|
||||||
|
|
||||||
// FlagMigrationLocking
|
// FlagMigrationLocking
|
||||||
// Lock database during migrations
|
// Lock database during migrations
|
||||||
FlagMigrationLocking = "migrationLocking"
|
FlagMigrationLocking = "migrationLocking"
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
package features
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
"github.com/grafana/grafana/pkg/services/live/model"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CommentHandler manages all the `grafana/comment/*` channels.
|
|
||||||
type CommentHandler struct {
|
|
||||||
permissionChecker *commentmodel.PermissionChecker
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCommentHandler(permissionChecker *commentmodel.PermissionChecker) *CommentHandler {
|
|
||||||
return &CommentHandler{permissionChecker: permissionChecker}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHandlerForPath called on init.
|
|
||||||
func (h *CommentHandler) GetHandlerForPath(_ string) (model.ChannelHandler, error) {
|
|
||||||
return h, nil // all chats share the same handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnSubscribe handles subscription to comment group channel.
|
|
||||||
func (h *CommentHandler) OnSubscribe(ctx context.Context, user *user.SignedInUser, e model.SubscribeEvent) (model.SubscribeReply, backend.SubscribeStreamStatus, error) {
|
|
||||||
parts := strings.Split(e.Path, "/")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
|
|
||||||
}
|
|
||||||
objectType := parts[0]
|
|
||||||
objectID := parts[1]
|
|
||||||
ok, err := h.permissionChecker.CheckReadPermissions(ctx, user.OrgID, user, objectType, objectID)
|
|
||||||
if err != nil {
|
|
||||||
return model.SubscribeReply{}, 0, err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil
|
|
||||||
}
|
|
||||||
return model.SubscribeReply{}, backend.SubscribeStreamStatusOK, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnPublish is not used for comments.
|
|
||||||
func (h *CommentHandler) OnPublish(_ context.Context, _ *user.SignedInUser, _ model.PublishEvent) (model.PublishReply, backend.PublishStreamStatus, error) {
|
|
||||||
return model.PublishReply{}, backend.PublishStreamStatusPermissionDenied, nil
|
|
||||||
}
|
|
@ -33,7 +33,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
"github.com/grafana/grafana/pkg/services/comments/commentmodel"
|
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
@ -247,7 +246,6 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
|||||||
g.GrafanaScope.Dashboards = dash
|
g.GrafanaScope.Dashboards = dash
|
||||||
g.GrafanaScope.Features["dashboard"] = dash
|
g.GrafanaScope.Features["dashboard"] = dash
|
||||||
g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage)
|
g.GrafanaScope.Features["broadcast"] = features.NewBroadcastRunner(g.storage)
|
||||||
g.GrafanaScope.Features["comment"] = features.NewCommentHandler(commentmodel.NewPermissionChecker(g.SQLStore, g.Features, accessControl, dashboardService, annotationsRepo))
|
|
||||||
|
|
||||||
g.surveyCaller = survey.NewCaller(managedStreamRunner, node)
|
g.surveyCaller = survey.NewCaller(managedStreamRunner, node)
|
||||||
err = g.surveyCaller.SetupHandlers()
|
err = g.surveyCaller.SetupHandlers()
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
func addCommentGroupMigrations(mg *Migrator) {
|
|
||||||
commentGroupTable := Table{
|
|
||||||
Name: "comment_group",
|
|
||||||
Columns: []*Column{
|
|
||||||
{Name: "id", Type: DB_BigInt, Nullable: false, IsPrimaryKey: true, IsAutoIncrement: true},
|
|
||||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
|
||||||
{Name: "object_type", Type: DB_NVarchar, Length: 10, Nullable: false},
|
|
||||||
{Name: "object_id", Type: DB_NVarchar, Length: 128, Nullable: false},
|
|
||||||
{Name: "settings", Type: DB_MediumText, Nullable: false},
|
|
||||||
{Name: "created", Type: DB_Int, Nullable: false},
|
|
||||||
{Name: "updated", Type: DB_Int, Nullable: false},
|
|
||||||
},
|
|
||||||
Indices: []*Index{
|
|
||||||
{Cols: []string{"org_id", "object_type", "object_id"}, Type: UniqueIndex},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mg.AddMigration("create comment group table", NewAddTableMigration(commentGroupTable))
|
|
||||||
mg.AddMigration("add index comment_group.org_id_object_type_object_id", NewAddIndexMigration(commentGroupTable, commentGroupTable.Indices[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCommentMigrations(mg *Migrator) {
|
|
||||||
commentTable := Table{
|
|
||||||
Name: "comment",
|
|
||||||
Columns: []*Column{
|
|
||||||
{Name: "id", Type: DB_BigInt, Nullable: false, IsPrimaryKey: true, IsAutoIncrement: true},
|
|
||||||
{Name: "group_id", Type: DB_BigInt, Nullable: false},
|
|
||||||
{Name: "user_id", Type: DB_BigInt, Nullable: false},
|
|
||||||
{Name: "content", Type: DB_MediumText, Nullable: false},
|
|
||||||
{Name: "created", Type: DB_Int, Nullable: false},
|
|
||||||
{Name: "updated", Type: DB_Int, Nullable: false},
|
|
||||||
},
|
|
||||||
Indices: []*Index{
|
|
||||||
{Cols: []string{"group_id"}, Type: IndexType},
|
|
||||||
{Cols: []string{"created"}, Type: IndexType},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mg.AddMigration("create comment table", NewAddTableMigration(commentTable))
|
|
||||||
mg.AddMigration("add index comment.group_id", NewAddIndexMigration(commentTable, commentTable.Indices[0]))
|
|
||||||
mg.AddMigration("add index comment.created", NewAddIndexMigration(commentTable, commentTable.Indices[1]))
|
|
||||||
}
|
|
@ -75,11 +75,6 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
|||||||
addCorrelationsMigrations(mg)
|
addCorrelationsMigrations(mg)
|
||||||
|
|
||||||
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
|
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
|
||||||
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagDashboardComments) || mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAnnotationComments) {
|
|
||||||
addCommentGroupMigrations(mg)
|
|
||||||
addCommentMigrations(mg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagEntityStore) {
|
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagEntityStore) {
|
||||||
addEntityStoreMigrations(mg)
|
addEntityStoreMigrations(mg)
|
||||||
}
|
}
|
||||||
|
@ -496,7 +496,6 @@ type InitTestDBOpt struct {
|
|||||||
|
|
||||||
var featuresEnabledDuringTests = []string{
|
var featuresEnabledDuringTests = []string{
|
||||||
featuremgmt.FlagDashboardPreviews,
|
featuremgmt.FlagDashboardPreviews,
|
||||||
featuremgmt.FlagDashboardComments,
|
|
||||||
featuremgmt.FlagPanelTitleSearch,
|
featuremgmt.FlagPanelTitleSearch,
|
||||||
featuremgmt.FlagEntityStore,
|
featuremgmt.FlagEntityStore,
|
||||||
}
|
}
|
||||||
|
@ -1,98 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import DangerouslySetHtmlContent from 'dangerously-set-html-content';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, renderMarkdown } from '@grafana/data';
|
|
||||||
import { useStyles2 } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { Message } from './types';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
message: Message;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Comment = ({ message }: Props) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
let senderColor = '#34BA18';
|
|
||||||
let senderName = 'System';
|
|
||||||
let avatarUrl = '/public/img/grafana_icon.svg';
|
|
||||||
if (message.userId > 0) {
|
|
||||||
senderColor = '#19a2e7';
|
|
||||||
senderName = message.user.login;
|
|
||||||
avatarUrl = message.user.avatarUrl;
|
|
||||||
}
|
|
||||||
const timeColor = '#898989';
|
|
||||||
const timeFormatted = new Date(message.created * 1000).toLocaleTimeString();
|
|
||||||
const markdownContent = renderMarkdown(message.content, { breaks: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.comment}>
|
|
||||||
<div className={styles.avatarContainer}>
|
|
||||||
<img src={avatarUrl} alt="User avatar" className={styles.avatar} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<span style={{ color: senderColor }}>{senderName}</span>
|
|
||||||
|
|
||||||
<span style={{ color: timeColor }}>{timeFormatted}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DangerouslySetHtmlContent html={markdownContent} className={styles.content} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
comment: css`
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-top: 3px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
word-break: break-word;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: top;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
background-color: #1e1f24;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
padding: 0 0 0 10px;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
avatarContainer: css`
|
|
||||||
align-self: left;
|
|
||||||
margin-top: 6px;
|
|
||||||
margin-right: 10px;
|
|
||||||
`,
|
|
||||||
avatar: css`
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
`,
|
|
||||||
content: css`
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote p {
|
|
||||||
font-size: 14px;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #43c57e;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
|
@ -1,110 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { Unsubscribable } from 'rxjs';
|
|
||||||
|
|
||||||
import { isLiveChannelMessageEvent, LiveChannelScope } from '@grafana/data';
|
|
||||||
import { getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { CommentView } from './CommentView';
|
|
||||||
import { Message, MessagePacket } from './types';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
objectType: string;
|
|
||||||
objectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
messages: Message[];
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CommentManager extends PureComponent<Props, State> {
|
|
||||||
subscription?: Unsubscribable;
|
|
||||||
packetCounter = 0;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
messages: [],
|
|
||||||
value: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const resp = await getBackendSrv().post('/api/comments/get', {
|
|
||||||
objectType: this.props.objectType,
|
|
||||||
objectId: this.props.objectId,
|
|
||||||
});
|
|
||||||
this.packetCounter++;
|
|
||||||
this.setState({
|
|
||||||
messages: resp.comments,
|
|
||||||
});
|
|
||||||
this.updateSubscription();
|
|
||||||
}
|
|
||||||
|
|
||||||
getLiveChannel = () => {
|
|
||||||
const live = getGrafanaLiveSrv();
|
|
||||||
if (!live) {
|
|
||||||
console.error('Grafana live not running, enable "live" feature toggle');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const address = this.getLiveAddress();
|
|
||||||
if (!address) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return live.getStream<MessagePacket>(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
getLiveAddress = () => {
|
|
||||||
return {
|
|
||||||
scope: LiveChannelScope.Grafana,
|
|
||||||
namespace: 'comment',
|
|
||||||
path: `${this.props.objectType}/${this.props.objectId}`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSubscription = () => {
|
|
||||||
if (this.subscription) {
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
this.subscription = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = this.getLiveChannel();
|
|
||||||
if (channel) {
|
|
||||||
this.subscription = channel.subscribe({
|
|
||||||
next: (msg) => {
|
|
||||||
if (isLiveChannelMessageEvent(msg)) {
|
|
||||||
const { commentCreated } = msg.message;
|
|
||||||
if (commentCreated) {
|
|
||||||
this.setState((prevState) => ({
|
|
||||||
messages: [...prevState.messages, commentCreated],
|
|
||||||
}));
|
|
||||||
this.packetCounter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
addComment = async (comment: string): Promise<boolean> => {
|
|
||||||
const response = await getBackendSrv().post('/api/comments/create', {
|
|
||||||
objectType: this.props.objectType,
|
|
||||||
objectId: this.props.objectId,
|
|
||||||
content: comment,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: set up error handling
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CommentView comments={this.state.messages} packetCounter={this.packetCounter} addComment={this.addComment} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React, { FormEvent, useLayoutEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
|
||||||
import { CustomScrollbar, TextArea, useStyles2 } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { Comment } from './Comment';
|
|
||||||
import { Message } from './types';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
comments: Message[];
|
|
||||||
packetCounter: number;
|
|
||||||
addComment: (comment: string) => Promise<boolean>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CommentView = ({ comments, packetCounter, addComment }: Props) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
const [comment, setComment] = useState('');
|
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
|
||||||
const commentViewContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (commentViewContainer.current) {
|
|
||||||
setScrollTop(commentViewContainer.current.offsetHeight);
|
|
||||||
} else {
|
|
||||||
setScrollTop(0);
|
|
||||||
}
|
|
||||||
}, [packetCounter]);
|
|
||||||
|
|
||||||
const onUpdateComment = (event: FormEvent<HTMLTextAreaElement>) => {
|
|
||||||
const element = event.currentTarget;
|
|
||||||
setComment(element.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyPress = async (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (event?.key === 'Enter' && !event?.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (comment.length > 0) {
|
|
||||||
const result = await addComment(comment);
|
|
||||||
if (result) {
|
|
||||||
setComment('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomScrollbar scrollTop={scrollTop}>
|
|
||||||
<div ref={commentViewContainer} className={styles.commentViewContainer}>
|
|
||||||
{comments.map((msg) => (
|
|
||||||
<Comment key={msg.id} message={msg} />
|
|
||||||
))}
|
|
||||||
<TextArea
|
|
||||||
placeholder="Write a comment"
|
|
||||||
value={comment}
|
|
||||||
onChange={onUpdateComment}
|
|
||||||
onKeyPress={onKeyPress}
|
|
||||||
autoFocus={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomScrollbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
commentViewContainer: css`
|
|
||||||
margin: 5px;
|
|
||||||
`,
|
|
||||||
});
|
|
@ -1,21 +0,0 @@
|
|||||||
export interface MessagePacket {
|
|
||||||
event: string;
|
|
||||||
commentCreated: Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
id: number;
|
|
||||||
content: string;
|
|
||||||
created: number;
|
|
||||||
userId: number;
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Interface may exist elsewhere
|
|
||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
login: string;
|
|
||||||
email: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
}
|
|
@ -25,7 +25,6 @@ import { useAppNotification } from 'app/core/copy/appNotification';
|
|||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import { useBusEvent } from 'app/core/hooks/useBusEvent';
|
import { useBusEvent } from 'app/core/hooks/useBusEvent';
|
||||||
import { t, Trans } from 'app/core/internationalization';
|
import { t, Trans } from 'app/core/internationalization';
|
||||||
import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal';
|
|
||||||
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
|
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
|
||||||
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
||||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||||
@ -244,26 +243,6 @@ export const DashNav = React.memo<Props>((props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dashboard.uid && config.featureToggles.dashboardComments) {
|
|
||||||
buttons.push(
|
|
||||||
<ModalsController key="button-dashboard-comments">
|
|
||||||
{({ showModal, hideModal }) => (
|
|
||||||
<DashNavButton
|
|
||||||
tooltip={t('dashboard.toolbar.comments-show', 'Show dashboard comments')}
|
|
||||||
icon="comment-alt-message"
|
|
||||||
iconSize="lg"
|
|
||||||
onClick={() => {
|
|
||||||
showModal(DashboardCommentsModal, {
|
|
||||||
dashboard,
|
|
||||||
onDismiss: hideModal,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ModalsController>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addCustomContent(customLeftActions, buttons);
|
addCustomContent(customLeftActions, buttons);
|
||||||
return buttons;
|
return buttons;
|
||||||
};
|
};
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
|
||||||
import { Modal, useStyles2 } from '@grafana/ui';
|
|
||||||
import { CommentManager } from 'app/features/comments/CommentManager';
|
|
||||||
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
dashboard: DashboardModel;
|
|
||||||
onDismiss: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DashboardCommentsModal = ({ dashboard, onDismiss }: Props) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={true} title="Dashboard comments" icon="save" onDismiss={onDismiss} className={styles.modal}>
|
|
||||||
<CommentManager objectType={'dashboard'} objectId={dashboard.uid} />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
modal: css`
|
|
||||||
width: 500px;
|
|
||||||
height: 60vh;
|
|
||||||
`,
|
|
||||||
});
|
|
@ -3,9 +3,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2, textUtil } from '@grafana/data';
|
import { GrafanaTheme2, textUtil } from '@grafana/data';
|
||||||
import { HorizontalGroup, IconButton, Tag, useStyles2 } from '@grafana/ui';
|
import { HorizontalGroup, IconButton, Tag, useStyles2 } from '@grafana/ui';
|
||||||
import config from 'app/core/config';
|
|
||||||
import alertDef from 'app/features/alerting/state/alertDef';
|
import alertDef from 'app/features/alerting/state/alertDef';
|
||||||
import { CommentManager } from 'app/features/comments/CommentManager';
|
|
||||||
|
|
||||||
import { AnnotationsDataFrameViewDTO } from '../types';
|
import { AnnotationsDataFrameViewDTO } from '../types';
|
||||||
|
|
||||||
@ -64,10 +62,8 @@ export const AnnotationTooltip = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const areAnnotationCommentsEnabled = config.featureToggles.annotationComments;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} style={areAnnotationCommentsEnabled ? { minWidth: '300px' } : {}}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<HorizontalGroup justify={'space-between'} align={'center'} spacing={'md'}>
|
<HorizontalGroup justify={'space-between'} align={'center'} spacing={'md'}>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
@ -91,11 +87,6 @@ export const AnnotationTooltip = ({
|
|||||||
))}
|
))}
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</>
|
</>
|
||||||
{areAnnotationCommentsEnabled && (
|
|
||||||
<div className={styles.commentWrapper}>
|
|
||||||
<CommentManager objectType={'annotation'} objectId={annotation.id.toString()} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user