diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0a9dea7dd97..c7c69855cc7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -93,7 +93,6 @@ /pkg/services/annotations/ @grafana/backend-platform /pkg/services/apikey/ @grafana/backend-platform /pkg/services/cleanup/ @grafana/backend-platform -/pkg/services/comments/ @grafana/backend-platform /pkg/services/contexthandler/ @grafana/backend-platform /pkg/services/correlations/ @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/canvas/ @grafana/dataviz-squad /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/correlations/ @grafana/explore-squad /public/app/features/dashboard/ @grafana/dashboards-squad diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index deb9903ffa8..576fb12c16d 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | | `lokiLive` | Support WebSocket streaming for loki (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 | | `exploreMixedDatasource` | Enable mixed datasource in Explore | | `tracing` | Adds trace ID to error notifications | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f68163edcba..6cb93a677bc 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -31,8 +31,6 @@ export interface FeatureToggles { lokiLive?: boolean; lokiDataframeApi?: boolean; featureHighlights?: boolean; - dashboardComments?: boolean; - annotationComments?: boolean; migrationLocking?: boolean; storage?: boolean; k8s?: boolean; diff --git a/pkg/api/api.go b/pkg/api/api.go index 170853569e9..0e558c0cd65 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -617,11 +617,6 @@ func (hs *HTTPServer) registerRoutes() { // short urls 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) // admin api diff --git a/pkg/api/comments.go b/pkg/api/comments.go deleted file mode 100644 index 3a3d3c52b42..00000000000 --- a/pkg/api/comments.go +++ /dev/null @@ -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, - }) -} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 17e72b61182..5503cce9e60 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -41,7 +41,6 @@ import ( "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" "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/correlations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -181,7 +180,6 @@ type HTTPServer struct { dashboardProvisioningService dashboards.DashboardProvisioningService folderService folder.Service DatasourcePermissionsService permissions.DatasourcePermissionsService - commentsService *comments.Service AlertNotificationService *alerting.AlertNotificationService dashboardsnapshotsService dashboardsnapshots.Service PluginSettings pluginSettings.Service @@ -241,7 +239,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service, 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, teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, @@ -328,7 +326,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dashboardProvisioningService: dashboardProvisioningService, folderService: folderService, DatasourcePermissionsService: datasourcePermissionsService, - commentsService: commentsService, teamPermissionsService: teamsPermissionsService, AlertNotificationService: alertNotificationService, dashboardsnapshotsService: dashboardsnapshotsService, diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 4bfe0f4f9a0..28075e07ab1 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -42,7 +42,6 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/auth/jwt" "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/authproxy" "github.com/grafana/grafana/pkg/services/dashboardimport" @@ -265,7 +264,6 @@ var wireSet = wire.NewSet( plugindashboardsservice.ProvideDashboardUpdater, alerting.ProvideDashAlertExtractorService, wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), - comments.ProvideService, guardian.ProvideService, sanitizer.ProvideService, secretsStore.ProvideService, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 00bee848218..c475202953c 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -42,7 +42,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authnimpl" "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/authproxy" "github.com/grafana/grafana/pkg/services/correlations" @@ -305,7 +304,6 @@ var wireBasicSet = wire.NewSet( plugindashboardsservice.ProvideDashboardUpdater, alerting.ProvideDashAlertExtractorService, wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), - comments.ProvideService, guardian.ProvideService, sanitizer.ProvideService, secretsStore.ProvideService, diff --git a/pkg/services/comments/commentmodel/events.go b/pkg/services/comments/commentmodel/events.go deleted file mode 100644 index 5e047c24757..00000000000 --- a/pkg/services/comments/commentmodel/events.go +++ /dev/null @@ -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"` -} diff --git a/pkg/services/comments/commentmodel/models.go b/pkg/services/comments/commentmodel/models.go deleted file mode 100644 index bfaa1713591..00000000000 --- a/pkg/services/comments/commentmodel/models.go +++ /dev/null @@ -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" -} diff --git a/pkg/services/comments/commentmodel/permissions.go b/pkg/services/comments/commentmodel/permissions.go deleted file mode 100644 index d0dad756579..00000000000 --- a/pkg/services/comments/commentmodel/permissions.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/comments/handlers.go b/pkg/services/comments/handlers.go deleted file mode 100644 index da07d3db206..00000000000 --- a/pkg/services/comments/handlers.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/comments/service.go b/pkg/services/comments/service.go deleted file mode 100644 index 1a8023bac5e..00000000000 --- a/pkg/services/comments/service.go +++ /dev/null @@ -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() -} diff --git a/pkg/services/comments/sql_storage.go b/pkg/services/comments/sql_storage.go deleted file mode 100644 index f2ef8f2d127..00000000000 --- a/pkg/services/comments/sql_storage.go +++ /dev/null @@ -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) - }) -} diff --git a/pkg/services/comments/sql_storage_test.go b/pkg/services/comments/sql_storage_test.go deleted file mode 100644 index 4c8a01a1a24..00000000000 --- a/pkg/services/comments/sql_storage_test.go +++ /dev/null @@ -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) -} diff --git a/pkg/services/comments/storage.go b/pkg/services/comments/storage.go deleted file mode 100644 index dd9216cb64b..00000000000 --- a/pkg/services/comments/storage.go +++ /dev/null @@ -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) -} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 74b8985acd7..b5a7b51954c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -101,18 +101,6 @@ var ( State: FeatureStateStable, 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", Description: "Lock database during migrations", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index d4aea01c0e1..8e4bb4265e9 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -67,14 +67,6 @@ const ( // Highlight Grafana Enterprise features FlagFeatureHighlights = "featureHighlights" - // FlagDashboardComments - // Enable dashboard-wide comments - FlagDashboardComments = "dashboardComments" - - // FlagAnnotationComments - // Enable annotation comments - FlagAnnotationComments = "annotationComments" - // FlagMigrationLocking // Lock database during migrations FlagMigrationLocking = "migrationLocking" diff --git a/pkg/services/live/features/comment.go b/pkg/services/live/features/comment.go deleted file mode 100644 index 49531148fe7..00000000000 --- a/pkg/services/live/features/comment.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index c23241020e4..9650e6cf6b8 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -33,7 +33,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" - "github.com/grafana/grafana/pkg/services/comments/commentmodel" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "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.Features["dashboard"] = dash 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) err = g.surveyCaller.SetupHandlers() diff --git a/pkg/services/sqlstore/migrations/comments_migrations.go b/pkg/services/sqlstore/migrations/comments_migrations.go deleted file mode 100644 index 2c92b8d756e..00000000000 --- a/pkg/services/sqlstore/migrations/comments_migrations.go +++ /dev/null @@ -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])) -} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 468c864138a..3511b0617bd 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -75,11 +75,6 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { addCorrelationsMigrations(mg) 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) { addEntityStoreMigrations(mg) } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 90e1480d2b0..55a43a0529f 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -496,7 +496,6 @@ type InitTestDBOpt struct { var featuresEnabledDuringTests = []string{ featuremgmt.FlagDashboardPreviews, - featuremgmt.FlagDashboardComments, featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagEntityStore, } diff --git a/public/app/features/comments/Comment.tsx b/public/app/features/comments/Comment.tsx deleted file mode 100644 index 6731fe3a656..00000000000 --- a/public/app/features/comments/Comment.tsx +++ /dev/null @@ -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 ( -
-
- User avatar -
-
-
- {senderName} -   - {timeFormatted} -
-
- -
-
-
- ); -}; - -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; - } - `, -}); diff --git a/public/app/features/comments/CommentManager.tsx b/public/app/features/comments/CommentManager.tsx deleted file mode 100644 index c8d987d4885..00000000000 --- a/public/app/features/comments/CommentManager.tsx +++ /dev/null @@ -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 { - 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(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 => { - 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 ( - - ); - } -} diff --git a/public/app/features/comments/CommentView.tsx b/public/app/features/comments/CommentView.tsx deleted file mode 100644 index e3f6defac89..00000000000 --- a/public/app/features/comments/CommentView.tsx +++ /dev/null @@ -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; -}; - -export const CommentView = ({ comments, packetCounter, addComment }: Props) => { - const styles = useStyles2(getStyles); - - const [comment, setComment] = useState(''); - const [scrollTop, setScrollTop] = useState(0); - const commentViewContainer = useRef(null); - - useLayoutEffect(() => { - if (commentViewContainer.current) { - setScrollTop(commentViewContainer.current.offsetHeight); - } else { - setScrollTop(0); - } - }, [packetCounter]); - - const onUpdateComment = (event: FormEvent) => { - const element = event.currentTarget; - setComment(element.value); - }; - - const onKeyPress = async (event: React.KeyboardEvent) => { - if (event?.key === 'Enter' && !event?.shiftKey) { - event.preventDefault(); - - if (comment.length > 0) { - const result = await addComment(comment); - if (result) { - setComment(''); - } - } - } - }; - - return ( - -
- {comments.map((msg) => ( - - ))} -