mirror of
https://github.com/mattermost/focalboard.git
synced 2024-11-24 08:22:29 +02:00
Adds limits implementation to the server (#3213)
* Adds limits implementation to the server * Add test for deleted boards on active card count
This commit is contained in:
parent
d34bf0391b
commit
fa6de94070
@ -17,6 +17,7 @@ require (
|
||||
github.com/Masterminds/squirrel v1.5.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
|
@ -210,6 +210,7 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
|
||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg=
|
||||
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||
@ -152,6 +153,8 @@ func (p *Plugin) OnActivate() error {
|
||||
WSAdapter: p.wsPluginAdapter,
|
||||
NotifyBackends: notifyBackends,
|
||||
PermissionsService: permissionsService,
|
||||
PluginAPI: p.API,
|
||||
Client: client,
|
||||
}
|
||||
|
||||
server, err := server.New(params)
|
||||
@ -162,6 +165,19 @@ func (p *Plugin) OnActivate() error {
|
||||
|
||||
backendParams.appAPI.init(db, server.App())
|
||||
|
||||
if utils.IsCloudLicense(p.API.GetLicense()) {
|
||||
limits, err := p.API.GetCloudLimits()
|
||||
if err != nil {
|
||||
fmt.Println("ERROR FETCHING CLOUD LIMITS WHEN STARTING THE PLUGIN", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := server.App().SetCloudLimits(limits); err != nil {
|
||||
fmt.Println("ERROR SETTING CLOUD LIMITS WHEN STARTING THE PLUGIN", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
p.server = server
|
||||
return server.Start()
|
||||
}
|
||||
@ -510,3 +526,9 @@ func isBoardsLink(link string) bool {
|
||||
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
|
||||
}
|
||||
|
||||
func (p *Plugin) OnCloudLimitsUpdated(limits *mmModel.ProductLimits) {
|
||||
if err := p.server.App().SetCloudLimits(limits); err != nil {
|
||||
fmt.Println("Error setting the cloud limits for Boards", err)
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,10 @@ import wsClient, {
|
||||
ACTION_UPDATE_BLOCK,
|
||||
ACTION_UPDATE_CLIENT_CONFIG,
|
||||
ACTION_UPDATE_SUBSCRIPTION,
|
||||
ACTION_UPDATE_CATEGORY, ACTION_UPDATE_BOARD_CATEGORY, ACTION_UPDATE_BOARD,
|
||||
ACTION_UPDATE_CARD_LIMIT_TIMESTAMP,
|
||||
ACTION_UPDATE_CATEGORY,
|
||||
ACTION_UPDATE_BOARD_CATEGORY,
|
||||
ACTION_UPDATE_BOARD,
|
||||
} from './../../../webapp/src/wsclient'
|
||||
|
||||
import manifest from './manifest'
|
||||
@ -275,6 +278,7 @@ export default class Plugin {
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CARD_LIMIT_TIMESTAMP}`, (e: any) => wsClient.updateCardLimitTimestampHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler('preferences_changed', (e: any) => {
|
||||
|
@ -153,6 +153,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv2.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET")
|
||||
apiv2.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
|
||||
|
||||
// limits
|
||||
apiv2.HandleFunc("/limits", a.sessionRequired(a.handleCloudLimits)).Methods("GET")
|
||||
|
||||
// System APIs
|
||||
r.HandleFunc("/hello", a.handleHello).Methods("GET")
|
||||
}
|
||||
@ -375,6 +378,13 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
mlog.Int("block_count", len(blocks)),
|
||||
)
|
||||
|
||||
var bErr error
|
||||
blocks, bErr = a.app.ApplyCloudLimits(blocks)
|
||||
if bErr != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", bErr)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(blocks)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -875,7 +885,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
newBlocks, err := a.app.InsertBlocks(blocks, session.UserID, true)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
if errors.Is(err, app.ErrViewsLimitReached) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
|
||||
} else {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -1408,6 +1423,10 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.AddMeta("blockID", blockID)
|
||||
|
||||
err = a.app.PatchBlock(blockID, patch, userID)
|
||||
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -1489,6 +1508,10 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
err = a.app.PatchBlocks(teamID, patches, userID)
|
||||
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -1830,7 +1853,7 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
|
||||
// File upload
|
||||
|
||||
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET "api/v2/files/teams/{teamID}/{boardID}/{filename} getFile
|
||||
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename} getFile
|
||||
//
|
||||
// Returns the contents of an uploaded file
|
||||
//
|
||||
@ -4002,6 +4025,10 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
|
||||
auditRec.AddMeta("blocksCount", len(pbab.BlockIDs))
|
||||
|
||||
bab, err := a.app.PatchBoardsAndBlocks(pbab, userID)
|
||||
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -4141,6 +4168,41 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /limits cloudLimits
|
||||
//
|
||||
// Fetches the cloud limits of the server.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/BoardsCloudLimits"
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
boardsCloudLimits, err := a.app.GetBoardsCloudLimits()
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(boardsCloudLimits)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /hello hello
|
||||
//
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
@ -13,9 +14,10 @@ import (
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -24,6 +26,10 @@ const (
|
||||
blockChangeNotifierShutdownTimeout = time.Second * 10
|
||||
)
|
||||
|
||||
type pluginAPI interface {
|
||||
GetUsers(options *mmModel.UserGetOptions) ([]*mmModel.User, *mmModel.AppError)
|
||||
}
|
||||
|
||||
type Services struct {
|
||||
Auth *auth.Auth
|
||||
Store store.Store
|
||||
@ -34,6 +40,7 @@ type Services struct {
|
||||
Logger *mlog.Logger
|
||||
Permissions permissions.PermissionsService
|
||||
SkipTemplateInit bool
|
||||
PluginAPI pluginAPI
|
||||
}
|
||||
|
||||
type App struct {
|
||||
@ -47,6 +54,10 @@ type App struct {
|
||||
notifications *notify.Service
|
||||
logger *mlog.Logger
|
||||
blockChangeNotifier *utils.CallbackQueue
|
||||
pluginAPI pluginAPI
|
||||
|
||||
cardLimitMux sync.RWMutex
|
||||
cardLimit int
|
||||
}
|
||||
|
||||
func (a *App) SetConfig(config *config.Configuration) {
|
||||
@ -69,7 +80,20 @@ func New(config *config.Configuration, wsAdapter ws.Adapter, services Services)
|
||||
notifications: services.Notifications,
|
||||
logger: services.Logger,
|
||||
blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger),
|
||||
pluginAPI: services.PluginAPI,
|
||||
}
|
||||
app.initialize(services.SkipTemplateInit)
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *App) CardLimit() int {
|
||||
a.cardLimitMux.RLock()
|
||||
defer a.cardLimitMux.RUnlock()
|
||||
return a.cardLimit
|
||||
}
|
||||
|
||||
func (a *App) SetCardLimit(cardLimit int) {
|
||||
a.cardLimitMux.Lock()
|
||||
defer a.cardLimitMux.Unlock()
|
||||
a.cardLimit = cardLimit
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
@ -114,7 +113,6 @@ func TestRegisterUser(t *testing.T) {
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
fmt.Println(test.email)
|
||||
err := th.App.RegisterUser(test.userName, test.email, test.password)
|
||||
if test.isError {
|
||||
require.Error(t, err)
|
||||
|
@ -13,6 +13,8 @@ import (
|
||||
)
|
||||
|
||||
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
|
||||
var ErrViewsLimitReached = errors.New("views limit reached for board")
|
||||
var ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited")
|
||||
|
||||
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) {
|
||||
if boardID == "" {
|
||||
@ -50,6 +52,16 @@ func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTe
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed duplicating a block",
|
||||
mlog.Err(uErr),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
@ -60,7 +72,17 @@ func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) {
|
||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||
oldBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
if a.IsCloudLimited() {
|
||||
containsLimitedBlocks, lErr := a.ContainsLimitedBlocks([]model.Block{*oldBlock})
|
||||
if lErr != nil {
|
||||
return lErr
|
||||
}
|
||||
if containsLimitedBlocks {
|
||||
return ErrPatchUpdatesLimitedCards
|
||||
}
|
||||
}
|
||||
|
||||
board, err := a.store.GetBoard(oldBlock.BoardID)
|
||||
@ -76,7 +98,7 @@ func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedB
|
||||
a.metrics.IncrementBlocksPatched(1)
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
// broadcast on websocket
|
||||
@ -93,17 +115,22 @@ func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedB
|
||||
}
|
||||
|
||||
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
||||
oldBlocks := make([]model.Block, 0, len(blockPatches.BlockIDs))
|
||||
for _, blockID := range blockPatches.BlockIDs {
|
||||
oldBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
oldBlocks = append(oldBlocks, *oldBlock)
|
||||
oldBlocks, err := a.store.GetBlocksByIDs(blockPatches.BlockIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := a.store.PatchBlocks(blockPatches, modifiedByID)
|
||||
if err != nil {
|
||||
if a.IsCloudLimited() {
|
||||
containsLimitedBlocks, err := a.ContainsLimitedBlocks(oldBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if containsLimitedBlocks {
|
||||
return ErrPatchUpdatesLimitedCards
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.store.PatchBlocks(blockPatches, modifiedByID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -112,7 +139,7 @@ func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, mo
|
||||
for i, blockID := range blockPatches.BlockIDs {
|
||||
newBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, *newBlock)
|
||||
a.webhook.NotifyUpdate(*newBlock)
|
||||
@ -136,9 +163,20 @@ func (a *App) InsertBlock(block model.Block, modifiedByID string) error {
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
a.webhook.NotifyUpdate(block)
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after inserting a block",
|
||||
mlog.Err(uErr),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -180,9 +218,19 @@ func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotif
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after inserting blocks",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
@ -289,8 +337,19 @@ func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
||||
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
|
||||
a.metrics.IncrementBlocksDeleted(1)
|
||||
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after deleting a block",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -341,9 +400,19 @@ func (a *App) UndeleteBlock(blockID string, modifiedBy string) (*model.Block, er
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
a.webhook.NotifyUpdate(*block)
|
||||
a.notifyBlockChanged(notify.Add, block, nil, modifiedBy)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after undeleting a block",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return block, nil
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
)
|
||||
|
||||
type blockError struct {
|
||||
@ -48,17 +51,63 @@ func TestPatchBlocks(t *testing.T) {
|
||||
defer tearDown()
|
||||
|
||||
t.Run("patchBlocks success scenerio", func(t *testing.T) {
|
||||
blockPatches := model.BlockPatchBatch{}
|
||||
blockPatches := model.BlockPatchBatch{
|
||||
BlockIDs: []string{"block1"},
|
||||
BlockPatches: []model.BlockPatch{
|
||||
{Title: mmModel.NewString("new title")},
|
||||
},
|
||||
}
|
||||
|
||||
block1 := model.Block{ID: "block1"}
|
||||
th.Store.EXPECT().GetBlocksByIDs([]string{"block1"}).Return([]model.Block{block1}, nil)
|
||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
|
||||
th.Store.EXPECT().GetBlock("block1").Return(&block1, nil)
|
||||
// this call comes from the WS server notification
|
||||
th.Store.EXPECT().GetMembersForBoard(gomock.Any()).Times(1)
|
||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("patchBlocks error scenerio", func(t *testing.T) {
|
||||
blockPatches := model.BlockPatchBatch{}
|
||||
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
blockPatches := model.BlockPatchBatch{BlockIDs: []string{}}
|
||||
th.Store.EXPECT().GetBlocksByIDs([]string{}).Return(nil, sql.ErrNoRows)
|
||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
})
|
||||
|
||||
t.Run("cloud limit error scenario", func(t *testing.T) {
|
||||
th.App.SetCardLimit(5)
|
||||
|
||||
fakeLicense := &mmModel.License{
|
||||
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||
}
|
||||
|
||||
blockPatches := model.BlockPatchBatch{
|
||||
BlockIDs: []string{"block1"},
|
||||
BlockPatches: []model.BlockPatch{
|
||||
{Title: mmModel.NewString("new title")},
|
||||
},
|
||||
}
|
||||
|
||||
block1 := model.Block{
|
||||
ID: "block1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board-id",
|
||||
BoardID: "board-id",
|
||||
UpdateAt: 100,
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board-id",
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetBlocksByIDs([]string{"block1"}).Return([]model.Block{block1}, nil)
|
||||
th.Store.EXPECT().GetBoard("board-id").Return(board1, nil)
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil)
|
||||
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
|
||||
require.ErrorIs(t, err, ErrPatchUpdatesLimitedCards)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
@ -199,6 +202,18 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if len(bab.Blocks) != 0 {
|
||||
go func() {
|
||||
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after duplicating a board",
|
||||
mlog.Err(uErr),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return bab, members, err
|
||||
}
|
||||
|
||||
@ -273,6 +288,15 @@ func (a *App) DeleteBoard(boardID, userID string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after deleting a board",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -451,5 +475,14 @@ func (a *App) UndeleteBoard(boardID string, modifiedBy string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after undeleting a board",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -44,17 +44,39 @@ func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, a
|
||||
}
|
||||
}
|
||||
|
||||
if len(newBab.Blocks) != 0 {
|
||||
go func() {
|
||||
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after creating boards and blocks",
|
||||
mlog.Err(uErr),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return newBab, nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
|
||||
oldBlocksMap := map[string]*model.Block{}
|
||||
for _, blockID := range pbab.BlockIDs {
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
oldBlocks, err := a.store.GetBlocksByIDs(pbab.BlockIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.IsCloudLimited() {
|
||||
containsLimitedBlocks, cErr := a.ContainsLimitedBlocks(oldBlocks)
|
||||
if cErr != nil {
|
||||
return nil, cErr
|
||||
}
|
||||
oldBlocksMap[blockID] = block
|
||||
if containsLimitedBlocks {
|
||||
return nil, ErrPatchUpdatesLimitedCards
|
||||
}
|
||||
}
|
||||
|
||||
oldBlocksMap := map[string]model.Block{}
|
||||
for _, block := range oldBlocks {
|
||||
oldBlocksMap[block.ID] = block
|
||||
}
|
||||
|
||||
bab, err := a.store.PatchBoardsAndBlocks(pbab, userID)
|
||||
@ -76,7 +98,7 @@ func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID stri
|
||||
a.metrics.IncrementBlocksPatched(1)
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, b)
|
||||
a.webhook.NotifyUpdate(b)
|
||||
a.notifyBlockChanged(notify.Update, &b, oldBlock, userID)
|
||||
a.notifyBlockChanged(notify.Update, &b, &oldBlock, userID)
|
||||
}
|
||||
|
||||
for _, board := range bab.Boards {
|
||||
@ -122,5 +144,16 @@ func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID st
|
||||
return nil
|
||||
})
|
||||
|
||||
if len(dbab.Blocks) != 0 {
|
||||
go func() {
|
||||
if uErr := a.UpdateCardLimitTimestamp(); uErr != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after deleting boards and blocks",
|
||||
mlog.Err(uErr),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
256
server/app/cloud.go
Normal file
256
server/app/cloud.go
Normal file
@ -0,0 +1,256 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
var ErrNilPluginAPI = errors.New("server not running in plugin mode")
|
||||
|
||||
// GetBoardsCloudLimits returns the limits of the server, and an empty
|
||||
// limits struct if there are no limits set.
|
||||
func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) {
|
||||
if !a.IsCloud() {
|
||||
return &model.BoardsCloudLimits{}, nil
|
||||
}
|
||||
|
||||
productLimits, err := a.store.GetCloudLimits()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usedCards, err := a.store.GetUsedCardsCount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boardsCloudLimits := &model.BoardsCloudLimits{
|
||||
UsedCards: usedCards,
|
||||
CardLimitTimestamp: cardLimitTimestamp,
|
||||
}
|
||||
if productLimits != nil && productLimits.Boards != nil {
|
||||
if productLimits.Boards.Cards != nil {
|
||||
boardsCloudLimits.Cards = *productLimits.Boards.Cards
|
||||
}
|
||||
if productLimits.Boards.Views != nil {
|
||||
boardsCloudLimits.Views = *productLimits.Boards.Views
|
||||
}
|
||||
}
|
||||
|
||||
return boardsCloudLimits, nil
|
||||
}
|
||||
|
||||
// IsCloud returns true if the server is running as a plugin in a
|
||||
// cloud licensed server.
|
||||
func (a *App) IsCloud() bool {
|
||||
return utils.IsCloudLicense(a.store.GetLicense())
|
||||
}
|
||||
|
||||
// IsCloudLimited returns true if the server is running in cloud mode
|
||||
// and the card limit has been set.
|
||||
func (a *App) IsCloudLimited() bool {
|
||||
return a.CardLimit() != 0 && a.IsCloud()
|
||||
}
|
||||
|
||||
// SetCloudLimits sets the limits of the server.
|
||||
func (a *App) SetCloudLimits(limits *mmModel.ProductLimits) error {
|
||||
oldCardLimit := a.CardLimit()
|
||||
|
||||
// if the limit object doesn't come complete, we assume limits are
|
||||
// being disabled
|
||||
cardLimit := 0
|
||||
if limits != nil && limits.Boards != nil && limits.Boards.Cards != nil {
|
||||
cardLimit = *limits.Boards.Cards
|
||||
}
|
||||
|
||||
if oldCardLimit != cardLimit {
|
||||
a.SetCardLimit(cardLimit)
|
||||
return a.doUpdateCardLimitTimestamp()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doUpdateCardLimitTimestamp performs the update without running any
|
||||
// checks.
|
||||
func (a *App) doUpdateCardLimitTimestamp() error {
|
||||
cardLimitTimestamp, err := a.store.UpdateCardLimitTimestamp(a.CardLimit())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.wsAdapter.BroadcastCardLimitTimestampChange(cardLimitTimestamp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCardLimitTimestamp checks if the server is a cloud instance
|
||||
// with limits applied, and if that's true, recalculates the card
|
||||
// limit timestamp and propagates the new one to the connected
|
||||
// clients.
|
||||
func (a *App) UpdateCardLimitTimestamp() error {
|
||||
if !a.IsCloudLimited() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.doUpdateCardLimitTimestamp()
|
||||
}
|
||||
|
||||
// getTemplateMapForBlocks gets all board ids for the blocks, and
|
||||
// builds a map with the board IDs as the key and their isTemplate
|
||||
// field as the value.
|
||||
func (a *App) getTemplateMapForBlocks(blocks []model.Block) (map[string]bool, error) {
|
||||
boardMap := map[string]*model.Board{}
|
||||
for _, block := range blocks {
|
||||
if _, ok := boardMap[block.BoardID]; !ok {
|
||||
board, err := a.store.GetBoard(block.BoardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boardMap[block.BoardID] = board
|
||||
}
|
||||
}
|
||||
|
||||
templateMap := map[string]bool{}
|
||||
for boardID, board := range boardMap {
|
||||
templateMap[boardID] = board.IsTemplate
|
||||
}
|
||||
|
||||
return templateMap, nil
|
||||
}
|
||||
|
||||
// ApplyCloudLimits takes a set of blocks and, if the server is cloud
|
||||
// limited, limits those that are outside of the card limit and don't
|
||||
// belong to a template.
|
||||
func (a *App) ApplyCloudLimits(blocks []model.Block) ([]model.Block, error) {
|
||||
// if there is no limit currently being applied, return
|
||||
if !a.IsCloudLimited() {
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateMap, err := a.getTemplateMapForBlocks(blocks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limitedBlocks := make([]model.Block, len(blocks))
|
||||
for i, block := range blocks {
|
||||
// if the block belongs to a template, it will never be
|
||||
// limited
|
||||
if isTemplate, ok := templateMap[block.BoardID]; ok && isTemplate {
|
||||
limitedBlocks[i] = block
|
||||
continue
|
||||
}
|
||||
|
||||
if block.ShouldBeLimited(cardLimitTimestamp) {
|
||||
limitedBlocks[i] = block.GetLimited()
|
||||
} else {
|
||||
limitedBlocks[i] = block
|
||||
}
|
||||
}
|
||||
|
||||
return limitedBlocks, nil
|
||||
}
|
||||
|
||||
// ContainsLimitedBlocks checks if a list of blocks contain any block
|
||||
// that references a limited card.
|
||||
func (a *App) ContainsLimitedBlocks(blocks []model.Block) (bool, error) {
|
||||
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if cardLimitTimestamp == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cards := []model.Block{}
|
||||
cardIDMap := map[string]bool{}
|
||||
for _, block := range blocks {
|
||||
switch block.Type {
|
||||
case model.TypeCard:
|
||||
cards = append(cards, block)
|
||||
default:
|
||||
cardIDMap[block.ParentID] = true
|
||||
}
|
||||
}
|
||||
|
||||
cardIDs := []string{}
|
||||
// if the card is already present on the set, we don't need to
|
||||
// fetch it from the database
|
||||
for cardID := range cardIDMap {
|
||||
alreadyPresent := false
|
||||
for _, card := range cards {
|
||||
if card.ID == cardID {
|
||||
alreadyPresent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !alreadyPresent {
|
||||
cardIDs = append(cardIDs, cardID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cardIDs) > 0 {
|
||||
fetchedCards, fErr := a.store.GetBlocksByIDs(cardIDs)
|
||||
if fErr != nil {
|
||||
return false, fErr
|
||||
}
|
||||
cards = append(cards, fetchedCards...)
|
||||
}
|
||||
|
||||
templateMap, err := a.getTemplateMapForBlocks(cards)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, card := range cards {
|
||||
isTemplate, ok := templateMap[card.BoardID]
|
||||
if !ok {
|
||||
return false, newErrBoardNotFoundInTemplateMap(card.BoardID)
|
||||
}
|
||||
|
||||
// if the block belongs to a template, it will never be
|
||||
// limited
|
||||
if isTemplate {
|
||||
continue
|
||||
}
|
||||
|
||||
if card.ShouldBeLimited(cardLimitTimestamp) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type errBoardNotFoundInTemplateMap struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func newErrBoardNotFoundInTemplateMap(id string) *errBoardNotFoundInTemplateMap {
|
||||
return &errBoardNotFoundInTemplateMap{id}
|
||||
}
|
||||
|
||||
func (eb *errBoardNotFoundInTemplateMap) Error() string {
|
||||
return fmt.Sprintf("board %q not found in template map", eb.id)
|
||||
}
|
642
server/app/cloud_test.go
Normal file
642
server/app/cloud_test.go
Normal file
@ -0,0 +1,642 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
)
|
||||
|
||||
func TestIsCloud(t *testing.T) {
|
||||
t.Run("if it's not running on plugin mode", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(nil)
|
||||
require.False(t, th.App.IsCloud())
|
||||
})
|
||||
|
||||
t.Run("if it's running on plugin mode but the license is incomplete", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
fakeLicense := &mmModel.License{}
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
require.False(t, th.App.IsCloud())
|
||||
|
||||
fakeLicense = &mmModel.License{Features: &mmModel.Features{}}
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
require.False(t, th.App.IsCloud())
|
||||
})
|
||||
|
||||
t.Run("if it's running on plugin mode, with a non-cloud license", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
fakeLicense := &mmModel.License{
|
||||
Features: &mmModel.Features{Cloud: mmModel.NewBool(false)},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
require.False(t, th.App.IsCloud())
|
||||
})
|
||||
|
||||
t.Run("if it's running on plugin mode with a cloud license", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
fakeLicense := &mmModel.License{
|
||||
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
require.True(t, th.App.IsCloud())
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsCloudLimited(t *testing.T) {
|
||||
t.Run("if no limit has been set, it should be false", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
require.False(t, th.App.IsCloudLimited())
|
||||
})
|
||||
|
||||
t.Run("if the limit is set, it should be true", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
fakeLicense := &mmModel.License{
|
||||
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||
}
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
|
||||
th.App.SetCardLimit(5)
|
||||
require.True(t, th.App.IsCloudLimited())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetCloudLimits(t *testing.T) {
|
||||
t.Run("if the limits are empty, it should do nothing", func(t *testing.T) {
|
||||
t.Run("limits empty", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
|
||||
require.NoError(t, th.App.SetCloudLimits(nil))
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
})
|
||||
|
||||
t.Run("limits not empty but board limits empty", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
|
||||
limits := &mmModel.ProductLimits{}
|
||||
|
||||
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
})
|
||||
|
||||
t.Run("limits not empty but board limits values empty", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
|
||||
limits := &mmModel.ProductLimits{
|
||||
Boards: &mmModel.BoardsLimits{},
|
||||
}
|
||||
|
||||
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("if the limits are not empty, it should update them and calculate the new timestamp", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
|
||||
newCardLimitTimestamp := int64(27)
|
||||
th.Store.EXPECT().UpdateCardLimitTimestamp(5).Return(newCardLimitTimestamp, nil)
|
||||
|
||||
limits := &mmModel.ProductLimits{
|
||||
Boards: &mmModel.BoardsLimits{Cards: mmModel.NewInt(5)},
|
||||
}
|
||||
|
||||
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||
require.Equal(t, 5, th.App.CardLimit())
|
||||
})
|
||||
|
||||
t.Run("if the limits are already set and we unset them, the timestamp will be unset too", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.App.SetCardLimit(20)
|
||||
|
||||
th.Store.EXPECT().UpdateCardLimitTimestamp(0)
|
||||
|
||||
require.NoError(t, th.App.SetCloudLimits(nil))
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
})
|
||||
|
||||
t.Run("if the limits are already set and we try to set the same ones again", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.App.SetCardLimit(20)
|
||||
|
||||
// the call to update card limit timestamp should not happen
|
||||
// as the limits didn't change
|
||||
th.Store.EXPECT().UpdateCardLimitTimestamp(gomock.Any()).Times(0)
|
||||
|
||||
limits := &mmModel.ProductLimits{
|
||||
Boards: &mmModel.BoardsLimits{Cards: mmModel.NewInt(20)},
|
||||
}
|
||||
|
||||
require.NoError(t, th.App.SetCloudLimits(limits))
|
||||
require.Equal(t, 20, th.App.CardLimit())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCardLimitTimestamp(t *testing.T) {
|
||||
fakeLicense := &mmModel.License{
|
||||
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||
}
|
||||
|
||||
t.Run("if the server is a cloud instance but not limited, it should do nothing", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
|
||||
// the license check will not be done as the limit not being
|
||||
// set is enough for the method to return
|
||||
th.Store.EXPECT().GetLicense().Times(0)
|
||||
// no call to UpdateCardLimitTimestamp should happen as the
|
||||
// method should shortcircuit if not cloud limited
|
||||
th.Store.EXPECT().UpdateCardLimitTimestamp(gomock.Any()).Times(0)
|
||||
|
||||
require.NoError(t, th.App.UpdateCardLimitTimestamp())
|
||||
})
|
||||
|
||||
t.Run("if the server is a cloud instance and the timestamp is set, it should run the update", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.App.SetCardLimit(5)
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
// no call to UpdateCardLimitTimestamp should happen as the
|
||||
// method should shortcircuit if not cloud limited
|
||||
th.Store.EXPECT().UpdateCardLimitTimestamp(5)
|
||||
|
||||
require.NoError(t, th.App.UpdateCardLimitTimestamp())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTemplateMapForBlocks(t *testing.T) {
|
||||
t.Run("should fetch the necessary boards from the database", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypeOpen,
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
board2 := &model.Board{
|
||||
ID: "board2",
|
||||
Type: model.BoardTypeOpen,
|
||||
IsTemplate: false,
|
||||
}
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
},
|
||||
{
|
||||
ID: "card2",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board2",
|
||||
BoardID: "board2",
|
||||
},
|
||||
{
|
||||
ID: "text2",
|
||||
Type: model.TypeText,
|
||||
ParentID: "card2",
|
||||
BoardID: "board2",
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().
|
||||
GetBoard("board1").
|
||||
Return(board1, nil).
|
||||
Times(1)
|
||||
th.Store.EXPECT().
|
||||
GetBoard("board2").
|
||||
Return(board2, nil).
|
||||
Times(1)
|
||||
|
||||
templateMap, err := th.App.getTemplateMapForBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templateMap, 2)
|
||||
require.Contains(t, templateMap, "board1")
|
||||
require.True(t, templateMap["board1"])
|
||||
require.Contains(t, templateMap, "board2")
|
||||
require.False(t, templateMap["board2"])
|
||||
})
|
||||
|
||||
t.Run("should fail if the board is not in the database", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
},
|
||||
{
|
||||
ID: "card2",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board2",
|
||||
BoardID: "board2",
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().
|
||||
GetBoard("board1").
|
||||
Return(nil, sql.ErrNoRows).
|
||||
Times(1)
|
||||
|
||||
templateMap, err := th.App.getTemplateMapForBlocks(blocks)
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Empty(t, templateMap)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyCloudLimits(t *testing.T) {
|
||||
fakeLicense := &mmModel.License{
|
||||
Features: &mmModel.Features{Cloud: mmModel.NewBool(true)},
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypeOpen,
|
||||
IsTemplate: false,
|
||||
}
|
||||
|
||||
template := &model.Board{
|
||||
ID: "template",
|
||||
Type: model.BoardTypeOpen,
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
{
|
||||
ID: "text1",
|
||||
Type: model.TypeText,
|
||||
ParentID: "card1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
{
|
||||
ID: "card2",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 200,
|
||||
},
|
||||
{
|
||||
ID: "card-from-template",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "template",
|
||||
BoardID: "template",
|
||||
UpdateAt: 1,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("if the server is not limited, it should return the blocks untouched", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
require.Zero(t, th.App.CardLimit())
|
||||
|
||||
newBlocks, err := th.App.ApplyCloudLimits(blocks)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, blocks, newBlocks)
|
||||
})
|
||||
|
||||
t.Run("if the server is limited, it should limit the blocks that are beyond the card limit timestamp", func(t *testing.T) {
|
||||
findBlock := func(blocks []model.Block, id string) model.Block {
|
||||
for _, block := range blocks {
|
||||
if block.ID == id {
|
||||
return block
|
||||
}
|
||||
}
|
||||
require.FailNow(t, "block %s not found", id)
|
||||
return model.Block{} // this should never be reached
|
||||
}
|
||||
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.App.SetCardLimit(5)
|
||||
|
||||
th.Store.EXPECT().GetLicense().Return(fakeLicense)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(150), nil)
|
||||
th.Store.EXPECT().GetBoard("board1").Return(board1, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard("template").Return(template, nil).Times(1)
|
||||
|
||||
newBlocks, err := th.App.ApplyCloudLimits(blocks)
|
||||
require.NoError(t, err)
|
||||
|
||||
// should be limited as it's beyond the threshold
|
||||
require.True(t, findBlock(newBlocks, "card1").Limited)
|
||||
// only cards are limited
|
||||
require.False(t, findBlock(newBlocks, "text1").Limited)
|
||||
// should not be limited as it's not beyond the threshold
|
||||
require.False(t, findBlock(newBlocks, "card2").Limited)
|
||||
// cards belonging to templates are never limited
|
||||
require.False(t, findBlock(newBlocks, "card-from-template").Limited)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainsLimitedBlocks(t *testing.T) {
|
||||
// for all the following tests, the timestamp will be set to 150,
|
||||
// which means that blocks with an UpdateAt set to 100 will be
|
||||
// outside the active window and possibly limited, and blocks with
|
||||
// UpdateAt set to 200 will not
|
||||
|
||||
t.Run("should return false if the card limit timestamp is zero", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
}
|
||||
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(0), nil)
|
||||
|
||||
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.False(t, containsLimitedBlocks)
|
||||
})
|
||||
|
||||
t.Run("should return true if the block set contains a card that is limited", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypePrivate,
|
||||
}
|
||||
|
||||
th.App.SetCardLimit(500)
|
||||
cardLimitTimestamp := int64(150)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||
|
||||
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.True(t, containsLimitedBlocks)
|
||||
})
|
||||
|
||||
t.Run("should return false if that same block belongs to a template", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypeOpen,
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
th.App.SetCardLimit(500)
|
||||
cardLimitTimestamp := int64(150)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||
|
||||
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.False(t, containsLimitedBlocks)
|
||||
})
|
||||
|
||||
t.Run("should return true if the block contains a content block that belongs to a card that should be limited", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "text1",
|
||||
Type: model.TypeText,
|
||||
ParentID: "card1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 200,
|
||||
},
|
||||
}
|
||||
|
||||
card1 := model.Block{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
|
||||
th.App.SetCardLimit(500)
|
||||
cardLimitTimestamp := int64(150)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||
th.Store.EXPECT().GetBlocksByIDs([]string{"card1"}).Return([]model.Block{card1}, nil)
|
||||
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||
|
||||
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.True(t, containsLimitedBlocks)
|
||||
})
|
||||
|
||||
t.Run("should return false if that same block belongs to a card that is inside the active window", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
{
|
||||
ID: "text1",
|
||||
Type: model.TypeText,
|
||||
ParentID: "card1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 200,
|
||||
},
|
||||
}
|
||||
|
||||
card1 := model.Block{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 200,
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
|
||||
th.App.SetCardLimit(500)
|
||||
cardLimitTimestamp := int64(150)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||
th.Store.EXPECT().GetBlocksByIDs([]string{"card1"}).Return([]model.Block{card1}, nil)
|
||||
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||
|
||||
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.False(t, containsLimitedBlocks)
|
||||
})
|
||||
|
||||
t.Run("should reach to the database to fetch the necessary information only in an efficient way", func(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
blocks := []model.Block{
|
||||
// a content block that references a card that needs
|
||||
// fetching
|
||||
{
|
||||
ID: "text1",
|
||||
Type: model.TypeText,
|
||||
ParentID: "card1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
// a board that needs fetching referenced by a card and a content block
|
||||
{
|
||||
ID: "card2",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board2",
|
||||
BoardID: "board2",
|
||||
// per timestamp should be limited but the board is a
|
||||
// template
|
||||
UpdateAt: 100,
|
||||
},
|
||||
{
|
||||
ID: "text2",
|
||||
Type: model.TypeText,
|
||||
ParentID: "card2",
|
||||
BoardID: "board2",
|
||||
UpdateAt: 200,
|
||||
},
|
||||
// a content block that references a card and a board,
|
||||
// both absent
|
||||
{
|
||||
ID: "image3",
|
||||
Type: model.TypeImage,
|
||||
ParentID: "card3",
|
||||
BoardID: "board3",
|
||||
UpdateAt: 100,
|
||||
},
|
||||
}
|
||||
|
||||
card1 := model.Block{
|
||||
ID: "card1",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
UpdateAt: 200,
|
||||
}
|
||||
|
||||
card3 := model.Block{
|
||||
ID: "card3",
|
||||
Type: model.TypeCard,
|
||||
ParentID: "board3",
|
||||
BoardID: "board3",
|
||||
UpdateAt: 200,
|
||||
}
|
||||
|
||||
board1 := &model.Board{
|
||||
ID: "board1",
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
|
||||
board2 := &model.Board{
|
||||
ID: "board2",
|
||||
Type: model.BoardTypeOpen,
|
||||
IsTemplate: true,
|
||||
}
|
||||
|
||||
board3 := &model.Board{
|
||||
ID: "board3",
|
||||
Type: model.BoardTypePrivate,
|
||||
}
|
||||
|
||||
th.App.SetCardLimit(500)
|
||||
cardLimitTimestamp := int64(150)
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(cardLimitTimestamp, nil)
|
||||
th.Store.EXPECT().GetBlocksByIDs(gomock.InAnyOrder([]string{"card1", "card3"})).Return([]model.Block{card1, card3}, nil)
|
||||
th.Store.EXPECT().GetBoard("board1").Return(board1, nil)
|
||||
th.Store.EXPECT().GetBoard("board2").Return(board2, nil)
|
||||
th.Store.EXPECT().GetBoard("board3").Return(board3, nil)
|
||||
|
||||
containsLimitedBlocks, err := th.App.ContainsLimitedBlocks(blocks)
|
||||
require.NoError(t, err)
|
||||
require.False(t, containsLimitedBlocks)
|
||||
})
|
||||
}
|
@ -40,6 +40,16 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
||||
if err == nil && string(peek) == legacyFileBegin {
|
||||
a.logger.Debug("importing legacy archive")
|
||||
_, errImport := a.ImportBoardJSONL(br, opt)
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after importing a legacy file",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
return errImport
|
||||
}
|
||||
|
||||
@ -98,6 +108,15 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
||||
mlog.String("dir", dir),
|
||||
mlog.String("filename", filename),
|
||||
)
|
||||
|
||||
go func() {
|
||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
||||
a.logger.Error(
|
||||
"UpdateCardLimitTimestamp failed after importing an archive",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -730,3 +730,19 @@ func (c *Client) ImportArchive(teamID string, data io.Reader) *Response {
|
||||
|
||||
return BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) GetLimits() (*model.BoardsCloudLimits, *Response) {
|
||||
r, err := c.DoAPIGet("/limits", "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var limits *model.BoardsCloudLimits
|
||||
err = json.NewDecoder(r.Body).Decode(&limits)
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
||||
return limits, BuildResponse(r)
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ require (
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
|
@ -210,6 +210,7 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
|
||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg=
|
||||
github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
||||
|
@ -62,6 +62,10 @@ type Block struct {
|
||||
// The board id that the block belongs to
|
||||
// required: true
|
||||
BoardID string `json:"boardId"`
|
||||
|
||||
// Indicates if the card is limited
|
||||
// required: false
|
||||
Limited bool `json:"limited,omitempty"`
|
||||
}
|
||||
|
||||
// BlockPatch is a patch for modify blocks
|
||||
@ -210,3 +214,33 @@ func StampModificationMetadata(userID string, blocks []Block, auditRec *audit.Re
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b Block) ShouldBeLimited(cardLimitTimestamp int64) bool {
|
||||
return b.Type == TypeCard &&
|
||||
b.UpdateAt < cardLimitTimestamp
|
||||
}
|
||||
|
||||
// Returns a limited version of the block that doesn't contain the
|
||||
// contents of the block, only its IDs and type.
|
||||
func (b Block) GetLimited() Block {
|
||||
newBlock := Block{
|
||||
Title: b.Title,
|
||||
ID: b.ID,
|
||||
ParentID: b.ParentID,
|
||||
Schema: b.Schema,
|
||||
Type: b.Type,
|
||||
CreateAt: b.CreateAt,
|
||||
UpdateAt: b.UpdateAt,
|
||||
DeleteAt: b.DeleteAt,
|
||||
WorkspaceID: b.WorkspaceID,
|
||||
Limited: true,
|
||||
}
|
||||
|
||||
if iconField, ok := b.Fields["icon"]; ok {
|
||||
newBlock.Fields = map[string]interface{}{
|
||||
"icon": iconField,
|
||||
}
|
||||
}
|
||||
|
||||
return newBlock
|
||||
}
|
||||
|
27
server/model/cloud.go
Normal file
27
server/model/cloud.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
const LimitUnlimited = 0
|
||||
|
||||
// BoardsCloudLimits is the representation of the limits for the
|
||||
// Boards server
|
||||
// swagger:model
|
||||
type BoardsCloudLimits struct {
|
||||
// The maximum number of cards on the server
|
||||
// required: true
|
||||
Cards int `json:"cards"`
|
||||
|
||||
// The current number of cards on the server
|
||||
// required: true
|
||||
UsedCards int `json:"used_cards"`
|
||||
|
||||
// The updated_at timestamp of the limit card
|
||||
// required: true
|
||||
CardLimitTimestamp int64 `json:"card_limit_timestamp"`
|
||||
|
||||
// The maximum number of views for each board
|
||||
// required: true
|
||||
Views int `json:"views"`
|
||||
}
|
@ -4,10 +4,15 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apierrors "github.com/mattermost/mattermost-plugin-api/errors"
|
||||
)
|
||||
|
||||
// ErrBlocksFromDifferentBoards is an error type that can be returned
|
||||
// when a set of blocks belong to different boards.
|
||||
var ErrBlocksFromDifferentBoards = errors.New("blocks belong to different boards")
|
||||
|
||||
// ErrNotFound is an error type that can be returned by store APIs when a query unexpectedly fetches no records.
|
||||
type ErrNotFound struct {
|
||||
resource string
|
||||
@ -47,3 +52,30 @@ func IsErrNotFound(err error) bool {
|
||||
// check if this is a plugin API error
|
||||
return errors.Is(err, apierrors.ErrNotFound)
|
||||
}
|
||||
|
||||
// ErrNotAllFound is an error type that can be returned by store APIs
|
||||
// when a query that should fetch a certain amount of records
|
||||
// unexpectedly fetches less.
|
||||
type ErrNotAllFound struct {
|
||||
resources []string
|
||||
}
|
||||
|
||||
func NewErrNotAllFound(resources []string) *ErrNotAllFound {
|
||||
return &ErrNotAllFound{
|
||||
resources: resources,
|
||||
}
|
||||
}
|
||||
|
||||
func (na *ErrNotAllFound) Error() string {
|
||||
return fmt.Sprintf("not all instances in {%s} found", strings.Join(na.resources, ", "))
|
||||
}
|
||||
|
||||
// IsErrNotAllFound returns true if `err` is or wraps a ErrNotAllFound.
|
||||
func IsErrNotAllFound(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var na *ErrNotAllFound
|
||||
return errors.As(err, &na)
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
@ -21,6 +24,8 @@ type Params struct {
|
||||
WSAdapter ws.Adapter
|
||||
NotifyBackends []notify.Backend
|
||||
PermissionsService permissions.PermissionsService
|
||||
PluginAPI plugin.API
|
||||
Client *pluginapi.Client
|
||||
}
|
||||
|
||||
func (p Params) CheckValid() error {
|
||||
|
@ -33,9 +33,8 @@ import (
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/oklog/run"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -138,6 +137,7 @@ func New(params Params) (*Server, error) {
|
||||
Notifications: notificationService,
|
||||
Logger: params.Logger,
|
||||
Permissions: params.PermissionsService,
|
||||
PluginAPI: params.PluginAPI,
|
||||
SkipTemplateInit: utils.IsRunningUnitTests(),
|
||||
}
|
||||
app := app.New(params.Cfg, wsAdapter, appServices)
|
||||
|
@ -465,3 +465,7 @@ func (s *MattermostAuthLayer) SaveFileInfo(fileInfo *mmModel.FileInfo) error {
|
||||
func (s *MattermostAuthLayer) GetLicense() *mmModel.License {
|
||||
return s.pluginAPI.GetLicense()
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetCloudLimits() (*mmModel.ProductLimits, error) {
|
||||
return s.pluginAPI.GetCloudLimits()
|
||||
}
|
||||
|
@ -399,6 +399,21 @@ func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBlocksByIDs mocks base method.
|
||||
func (m *MockStore) GetBlocksByIDs(arg0 []string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBlocksByIDs", arg0)
|
||||
ret0, _ := ret[0].([]model.Block)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBlocksByIDs indicates an expected call of GetBlocksByIDs.
|
||||
func (mr *MockStoreMockRecorder) GetBlocksByIDs(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksByIDs", reflect.TypeOf((*MockStore)(nil).GetBlocksByIDs), arg0)
|
||||
}
|
||||
|
||||
// GetBlocksForBoard mocks base method.
|
||||
func (m *MockStore) GetBlocksForBoard(arg0 string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -566,6 +581,21 @@ func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1 interface{})
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetCardLimitTimestamp mocks base method.
|
||||
func (m *MockStore) GetCardLimitTimestamp() (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCardLimitTimestamp")
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCardLimitTimestamp indicates an expected call of GetCardLimitTimestamp.
|
||||
func (mr *MockStoreMockRecorder) GetCardLimitTimestamp() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCardLimitTimestamp", reflect.TypeOf((*MockStore)(nil).GetCardLimitTimestamp))
|
||||
}
|
||||
|
||||
// GetCategory mocks base method.
|
||||
func (m *MockStore) GetCategory(arg0 string) (*model.Category, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -581,6 +611,21 @@ func (mr *MockStoreMockRecorder) GetCategory(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0)
|
||||
}
|
||||
|
||||
// GetCloudLimits mocks base method.
|
||||
func (m *MockStore) GetCloudLimits() (*model0.ProductLimits, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCloudLimits")
|
||||
ret0, _ := ret[0].(*model0.ProductLimits)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCloudLimits indicates an expected call of GetCloudLimits.
|
||||
func (mr *MockStoreMockRecorder) GetCloudLimits() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockStore)(nil).GetCloudLimits))
|
||||
}
|
||||
|
||||
// GetFileInfo mocks base method.
|
||||
func (m *MockStore) GetFileInfo(arg0 string) (*model0.FileInfo, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -895,6 +940,21 @@ func (mr *MockStoreMockRecorder) GetTemplateBoards(arg0, arg1 interface{}) *gomo
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateBoards", reflect.TypeOf((*MockStore)(nil).GetTemplateBoards), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUsedCardsCount mocks base method.
|
||||
func (m *MockStore) GetUsedCardsCount() (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUsedCardsCount")
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUsedCardsCount indicates an expected call of GetUsedCardsCount.
|
||||
func (mr *MockStoreMockRecorder) GetUsedCardsCount() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsedCardsCount", reflect.TypeOf((*MockStore)(nil).GetUsedCardsCount))
|
||||
}
|
||||
|
||||
// GetUserByEmail mocks base method.
|
||||
func (m *MockStore) GetUserByEmail(arg0 string) (*model.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -1259,6 +1319,21 @@ func (mr *MockStoreMockRecorder) UndeleteBoard(arg0, arg1 interface{}) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndeleteBoard", reflect.TypeOf((*MockStore)(nil).UndeleteBoard), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateCardLimitTimestamp mocks base method.
|
||||
func (m *MockStore) UpdateCardLimitTimestamp(arg0 int) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateCardLimitTimestamp", arg0)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateCardLimitTimestamp indicates an expected call of UpdateCardLimitTimestamp.
|
||||
func (mr *MockStoreMockRecorder) UpdateCardLimitTimestamp(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCardLimitTimestamp", reflect.TypeOf((*MockStore)(nil).UpdateCardLimitTimestamp), arg0)
|
||||
}
|
||||
|
||||
// UpdateCategory mocks base method.
|
||||
func (m *MockStore) UpdateCategory(arg0 model.Category) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -100,6 +100,32 @@ func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID strin
|
||||
return s.blocksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"id": ids})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`GetBlocksByIDs ERROR`, mlog.Err(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
blocks, err := s.blocksFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(blocks) != len(ids) {
|
||||
return nil, model.NewErrNotAllFound(ids)
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksWithBoardID(db sq.BaseRunner, boardID string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
|
117
server/services/store/sqlstore/cloud.go
Normal file
117
server/services/store/sqlstore/cloud.go
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
var ErrInvalidCardLimitValue = errors.New("card limit value is invalid")
|
||||
|
||||
// activeCardsQuery applies the necessary filters to the query for it
|
||||
// to fetch an active cards window if the cardLimit is set, or all the
|
||||
// active cards if it's 0.
|
||||
func (s *SQLStore) activeCardsQuery(builder sq.StatementBuilderType, selectStr string, cardLimit int) sq.SelectBuilder {
|
||||
query := builder.
|
||||
Select(selectStr).
|
||||
From(s.tablePrefix + "blocks b").
|
||||
Join(s.tablePrefix + "boards bd on b.board_id=bd.id").
|
||||
Where(sq.Eq{
|
||||
"b.delete_at": 0,
|
||||
"b.type": model.TypeCard,
|
||||
"bd.is_template": false,
|
||||
})
|
||||
|
||||
if cardLimit != 0 {
|
||||
query = query.
|
||||
Limit(1).
|
||||
Offset(uint64(cardLimit - 1))
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// getUsedCardsCount returns the amount of active cards in the server.
|
||||
func (s *SQLStore) getUsedCardsCount(db sq.BaseRunner) (int, error) {
|
||||
row := s.activeCardsQuery(s.getQueryBuilder(db), "count(b.id)", 0).
|
||||
QueryRow()
|
||||
|
||||
var usedCards int
|
||||
err := row.Scan(&usedCards)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return usedCards, nil
|
||||
}
|
||||
|
||||
// getCardLimitTimestamp returns the timestamp value from the
|
||||
// system_settings table or zero if it doesn't exist.
|
||||
func (s *SQLStore) getCardLimitTimestamp(db sq.BaseRunner) (int64, error) {
|
||||
scanner := s.getQueryBuilder(db).
|
||||
Select("value").
|
||||
From(s.tablePrefix + "system_settings").
|
||||
Where(sq.Eq{"id": store.CardLimitTimestampSystemKey}).
|
||||
QueryRow()
|
||||
|
||||
var result string
|
||||
err := scanner.Scan(&result)
|
||||
if errors.Is(sql.ErrNoRows, err) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
cardLimitTimestamp, err := strconv.Atoi(result)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidCardLimitValue
|
||||
}
|
||||
|
||||
return int64(cardLimitTimestamp), nil
|
||||
}
|
||||
|
||||
// updateCardLimitTimestamp updates the card limit value in the
|
||||
// system_settings table with the timestamp of the nth last updated
|
||||
// card, being nth the value of the cardLimit parameter. If cardLimit
|
||||
// is zero, the timestamp will be set to zero.
|
||||
func (s *SQLStore) updateCardLimitTimestamp(db sq.BaseRunner, cardLimit int) (int64, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Insert(s.tablePrefix+"system_settings").
|
||||
Columns("id", "value")
|
||||
|
||||
var value interface{} = 0
|
||||
if cardLimit != 0 {
|
||||
value = s.activeCardsQuery(sq.StatementBuilder, "b.update_at", cardLimit).
|
||||
OrderBy("b.update_at DESC").
|
||||
Prefix("COALESCE((").Suffix("), 0)")
|
||||
}
|
||||
query = query.Values(store.CardLimitTimestampSystemKey, value)
|
||||
|
||||
if s.dbType == model.MysqlDBType {
|
||||
query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value)
|
||||
} else {
|
||||
query = query.Suffix(
|
||||
`ON CONFLICT (id)
|
||||
DO UPDATE SET value = EXCLUDED.value`,
|
||||
)
|
||||
}
|
||||
|
||||
result, err := query.Exec()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if _, err := result.RowsAffected(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return s.getCardLimitTimestamp(db)
|
||||
}
|
@ -294,6 +294,11 @@ func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBl
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksByIDs(ids []string) ([]model.Block, error) {
|
||||
return s.getBlocksByIDs(s.db, ids)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksForBoard(boardID string) ([]model.Block, error) {
|
||||
return s.getBlocksForBoard(s.db, boardID)
|
||||
|
||||
@ -349,11 +354,21 @@ func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*mod
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetCardLimitTimestamp() (int64, error) {
|
||||
return s.getCardLimitTimestamp(s.db)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetCategory(id string) (*model.Category, error) {
|
||||
return s.getCategory(s.db, id)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetCloudLimits() (*mmModel.ProductLimits, error) {
|
||||
return s.getCloudLimits(s.db)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetFileInfo(id string) (*mmModel.FileInfo, error) {
|
||||
return s.getFileInfo(s.db, id)
|
||||
|
||||
@ -459,6 +474,11 @@ func (s *SQLStore) GetTemplateBoards(teamID string, userID string) ([]*model.Boa
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetUsedCardsCount() (int, error) {
|
||||
return s.getUsedCardsCount(s.db)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetUserByEmail(email string) (*model.User, error) {
|
||||
return s.getUserByEmail(s.db, email)
|
||||
|
||||
@ -769,6 +789,11 @@ func (s *SQLStore) UndeleteBoard(boardID string, modifiedBy string) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) UpdateCardLimitTimestamp(cardLimit int) (int64, error) {
|
||||
return s.updateCardLimitTimestamp(s.db, cardLimit)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) UpdateCategory(category model.Category) error {
|
||||
return s.updateCategory(s.db, category)
|
||||
|
||||
|
@ -122,3 +122,7 @@ func (s *SQLStore) escapeField(fieldName string) string {
|
||||
func (s *SQLStore) getLicense(db sq.BaseRunner) *mmModel.License {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getCloudLimits(db sq.BaseRunner) (*mmModel.ProductLimits, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -21,5 +21,6 @@ func TestSQLStore(t *testing.T) {
|
||||
t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, SetupTests) })
|
||||
t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, SetupTests) })
|
||||
t.Run("DataRetention", func(t *testing.T) { storetests.StoreTestDataRetention(t, SetupTests) })
|
||||
t.Run("CloudStore", func(t *testing.T) { storetests.StoreTestCloudStore(t, SetupTests) })
|
||||
t.Run("StoreTestFileStore", func(t *testing.T) { storetests.StoreTestFileStore(t, SetupTests) })
|
||||
}
|
||||
|
@ -24,6 +24,10 @@ func (s *SQLStore) IsErrNotFound(err error) bool {
|
||||
return model.IsErrNotFound(err)
|
||||
}
|
||||
|
||||
func (s *SQLStore) IsErrNotAllFound(err error) bool {
|
||||
return model.IsErrNotAllFound(err)
|
||||
}
|
||||
|
||||
func (s *SQLStore) MarshalJSONB(data interface{}) ([]byte, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
|
@ -10,10 +10,13 @@ import (
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
const CardLimitTimestampSystemKey = "card_limit_timestamp"
|
||||
|
||||
// Store represents the abstraction of the data storage.
|
||||
type Store interface {
|
||||
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]model.Block, error)
|
||||
GetBlocksWithParent(boardID, parentID string) ([]model.Block, error)
|
||||
GetBlocksByIDs(ids []string) ([]model.Block, error)
|
||||
GetBlocksWithBoardID(boardID string) ([]model.Block, error)
|
||||
GetBlocksWithType(boardID, blockType string) ([]model.Block, error)
|
||||
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
|
||||
@ -139,7 +142,12 @@ type Store interface {
|
||||
// @withTransaction
|
||||
RunDataRetention(globalRetentionDate int64, batchSize int64) (int64, error)
|
||||
|
||||
GetUsedCardsCount() (int, error)
|
||||
GetCardLimitTimestamp() (int64, error)
|
||||
UpdateCardLimitTimestamp(cardLimit int) (int64, error)
|
||||
|
||||
DBType() string
|
||||
|
||||
GetLicense() *mmModel.License
|
||||
GetCloudLimits() (*mmModel.ProductLimits, error)
|
||||
}
|
||||
|
331
server/services/store/storetests/cloud.go
Normal file
331
server/services/store/storetests/cloud.go
Normal file
@ -0,0 +1,331 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package storetests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
storeservice "github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
)
|
||||
|
||||
func StoreTestCloudStore(t *testing.T, setup func(t *testing.T) (storeservice.Store, func())) {
|
||||
t.Run("GetUsedCardsCount", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testGetUsedCardsCount(t, store)
|
||||
})
|
||||
t.Run("TestGetCardLimitTimestamp", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testGetCardLimitTimestamp(t, store)
|
||||
})
|
||||
t.Run("TestUpdateCardLimitTimestamp", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testUpdateCardLimitTimestamp(t, store)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetUsedCardsCount(t *testing.T, store storeservice.Store) {
|
||||
userID := "user-id"
|
||||
|
||||
t.Run("should return zero when no cards have been created", func(t *testing.T) {
|
||||
count, err := store.GetUsedCardsCount()
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, count)
|
||||
})
|
||||
|
||||
t.Run("should correctly return the cards of all boards", func(t *testing.T) {
|
||||
// two boards
|
||||
for _, boardID := range []string{"board1", "board2"} {
|
||||
boardType := model.BoardTypeOpen
|
||||
if boardID == "board2" {
|
||||
boardType = model.BoardTypePrivate
|
||||
}
|
||||
|
||||
board := &model.Board{
|
||||
ID: boardID,
|
||||
TeamID: testTeamID,
|
||||
Type: boardType,
|
||||
}
|
||||
|
||||
_, err := store.InsertBoard(board, userID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// board 1 has three cards
|
||||
for _, cardID := range []string{"card1", "card2", "card3"} {
|
||||
card := model.Block{
|
||||
ID: cardID,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&card, userID))
|
||||
}
|
||||
|
||||
// board 2 has two cards
|
||||
for _, cardID := range []string{"card4", "card5"} {
|
||||
card := model.Block{
|
||||
ID: cardID,
|
||||
ParentID: "board2",
|
||||
BoardID: "board2",
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&card, userID))
|
||||
}
|
||||
|
||||
count, err := store.GetUsedCardsCount()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 5, count)
|
||||
})
|
||||
|
||||
t.Run("should not take into account content blocks", func(t *testing.T) {
|
||||
// we add a couple of content blocks
|
||||
text := model.Block{
|
||||
ID: "text-id",
|
||||
ParentID: "card1",
|
||||
BoardID: "board1",
|
||||
Type: model.TypeText,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&text, userID))
|
||||
|
||||
view := model.Block{
|
||||
ID: "view-id",
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
Type: model.TypeView,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&view, userID))
|
||||
|
||||
// and count should not change
|
||||
count, err := store.GetUsedCardsCount()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 5, count)
|
||||
})
|
||||
|
||||
t.Run("should not take into account cards belonging to templates", func(t *testing.T) {
|
||||
// we add a template with cards
|
||||
templateID := "template-id"
|
||||
boardTemplate := model.Block{
|
||||
ID: templateID,
|
||||
BoardID: templateID,
|
||||
Type: model.TypeBoard,
|
||||
Fields: map[string]interface{}{
|
||||
"isTemplate": true,
|
||||
},
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&boardTemplate, userID))
|
||||
|
||||
for _, cardID := range []string{"card6", "card7", "card8"} {
|
||||
card := model.Block{
|
||||
ID: cardID,
|
||||
ParentID: templateID,
|
||||
BoardID: templateID,
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&card, userID))
|
||||
}
|
||||
|
||||
// and count should still be the same
|
||||
count, err := store.GetUsedCardsCount()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 5, count)
|
||||
})
|
||||
|
||||
t.Run("should not take into account deleted cards", func(t *testing.T) {
|
||||
// we create a ninth card on the first board
|
||||
card9 := model.Block{
|
||||
ID: "card9",
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
Type: model.TypeCard,
|
||||
DeleteAt: utils.GetMillis(),
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&card9, userID))
|
||||
|
||||
// and count should still be the same
|
||||
count, err := store.GetUsedCardsCount()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 5, count)
|
||||
})
|
||||
|
||||
t.Run("should not take into account cards from deleted boards", func(t *testing.T) {
|
||||
require.NoError(t, store.DeleteBoard("board2", "user-id"))
|
||||
|
||||
count, err := store.GetUsedCardsCount()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetCardLimitTimestamp(t *testing.T, store storeservice.Store) {
|
||||
t.Run("should return 0 if there is no entry in the database", func(t *testing.T) {
|
||||
rawValue, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", rawValue)
|
||||
|
||||
cardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
})
|
||||
|
||||
t.Run("should return an int64 representation of the value", func(t *testing.T) {
|
||||
require.NoError(t, store.SetSystemSetting(storeservice.CardLimitTimestampSystemKey, "1234"))
|
||||
|
||||
cardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1234), cardLimitTimestamp)
|
||||
})
|
||||
|
||||
t.Run("should return an invalid value error if the value is not a number", func(t *testing.T) {
|
||||
require.NoError(t, store.SetSystemSetting(storeservice.CardLimitTimestampSystemKey, "abc"))
|
||||
|
||||
cardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||
require.ErrorContains(t, err, "card limit value is invalid")
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
})
|
||||
}
|
||||
|
||||
func testUpdateCardLimitTimestamp(t *testing.T, store storeservice.Store) {
|
||||
userID := "user-id"
|
||||
|
||||
// two boards
|
||||
for _, boardID := range []string{"board1", "board2"} {
|
||||
boardType := model.BoardTypeOpen
|
||||
if boardID == "board2" {
|
||||
boardType = model.BoardTypePrivate
|
||||
}
|
||||
|
||||
board := &model.Board{
|
||||
ID: boardID,
|
||||
TeamID: testTeamID,
|
||||
Type: boardType,
|
||||
}
|
||||
|
||||
_, err := store.InsertBoard(board, userID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// board 1 has five cards
|
||||
for _, cardID := range []string{"card1", "card2", "card3", "card4", "card5"} {
|
||||
card := model.Block{
|
||||
ID: cardID,
|
||||
ParentID: "board1",
|
||||
BoardID: "board1",
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&card, userID))
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// board 2 has five cards
|
||||
for _, cardID := range []string{"card6", "card7", "card8", "card9", "card10"} {
|
||||
card := model.Block{
|
||||
ID: cardID,
|
||||
ParentID: "board2",
|
||||
BoardID: "board2",
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
require.NoError(t, store.InsertBlock(&card, userID))
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Run("should set the timestamp to zero if the card limit is zero", func(t *testing.T) {
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(0)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
|
||||
cardLimitTimestampStr, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", cardLimitTimestampStr)
|
||||
})
|
||||
|
||||
t.Run("should correctly modify the limit several times in a row", func(t *testing.T) {
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(0)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
|
||||
cardLimitTimestamp, err = store.UpdateCardLimitTimestamp(10)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, cardLimitTimestamp)
|
||||
|
||||
cardLimitTimestampStr, err := store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, "0", cardLimitTimestampStr)
|
||||
|
||||
cardLimitTimestamp, err = store.UpdateCardLimitTimestamp(0)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
|
||||
cardLimitTimestampStr, err = store.GetSystemSetting(storeservice.CardLimitTimestampSystemKey)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", cardLimitTimestampStr)
|
||||
})
|
||||
|
||||
t.Run("should set the correct timestamp", func(t *testing.T) {
|
||||
t.Run("limit 10", func(t *testing.T) {
|
||||
// we fetch the first block
|
||||
card1, err := store.GetBlock("card1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// and assert that if the limit is 10, the stored
|
||||
// timestamp corresponds to the card's update_at
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, card1.UpdateAt, cardLimitTimestamp)
|
||||
})
|
||||
|
||||
t.Run("limit 5", func(t *testing.T) {
|
||||
// if the limit is 5, the timestamp should be the one from
|
||||
// the sixth card (the first five are older and out of the
|
||||
card6, err := store.GetBlock("card6")
|
||||
require.NoError(t, err)
|
||||
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(5)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, card6.UpdateAt, cardLimitTimestamp)
|
||||
})
|
||||
|
||||
t.Run("limit should be zero if we have less cards than the limit", func(t *testing.T) {
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(100)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
})
|
||||
|
||||
t.Run("we update the first inserted card and assert that with limit 1 that's the limit that is set", func(t *testing.T) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
card1, err := store.GetBlock("card1")
|
||||
require.NoError(t, err)
|
||||
|
||||
card1.Title = "New title"
|
||||
require.NoError(t, store.InsertBlock(card1, userID))
|
||||
|
||||
newCard1, err := store.GetBlock("card1")
|
||||
require.NoError(t, err)
|
||||
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCard1.UpdateAt, cardLimitTimestamp)
|
||||
})
|
||||
|
||||
t.Run("limit should stop applying if we remove the last card", func(t *testing.T) {
|
||||
initialCardLimitTimestamp, err := store.GetCardLimitTimestamp()
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, initialCardLimitTimestamp)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
require.NoError(t, store.DeleteBlock("card1", userID))
|
||||
|
||||
cardLimitTimestamp, err := store.UpdateCardLimitTimestamp(10)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cardLimitTimestamp)
|
||||
})
|
||||
})
|
||||
}
|
@ -5,7 +5,7 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
type IDType byte
|
||||
@ -27,22 +27,22 @@ const (
|
||||
// with the padding stripped off, and a one character alpha prefix indicating the
|
||||
// type of entity or a `7` if unknown type.
|
||||
func NewID(idType IDType) string {
|
||||
return string(idType) + mm_model.NewId()
|
||||
return string(idType) + mmModel.NewId()
|
||||
}
|
||||
|
||||
// GetMillis is a convenience method to get milliseconds since epoch.
|
||||
func GetMillis() int64 {
|
||||
return mm_model.GetMillis()
|
||||
return mmModel.GetMillis()
|
||||
}
|
||||
|
||||
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
|
||||
func GetMillisForTime(thisTime time.Time) int64 {
|
||||
return mm_model.GetMillisForTime(thisTime)
|
||||
return mmModel.GetMillisForTime(thisTime)
|
||||
}
|
||||
|
||||
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
|
||||
func GetTimeForMillis(millis int64) time.Time {
|
||||
return mm_model.GetTimeForMillis(millis)
|
||||
return mmModel.GetTimeForMillis(millis)
|
||||
}
|
||||
|
||||
// SecondsToMillis is a convenience method to convert seconds to milliseconds.
|
||||
@ -95,3 +95,10 @@ func Intersection(x ...[]interface{}) []interface{} {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func IsCloudLicense(license *mmModel.License) bool {
|
||||
return license != nil &&
|
||||
license.Features != nil &&
|
||||
license.Features.Cloud != nil &&
|
||||
*license.Features.Cloud
|
||||
}
|
||||
|
@ -6,19 +6,20 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
websocketActionAuth = "AUTH"
|
||||
websocketActionSubscribeTeam = "SUBSCRIBE_TEAM"
|
||||
websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM"
|
||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||
websocketActionUpdateBoard = "UPDATE_BOARD"
|
||||
websocketActionUpdateMember = "UPDATE_MEMBER"
|
||||
websocketActionDeleteMember = "DELETE_MEMBER"
|
||||
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
||||
websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG"
|
||||
websocketActionUpdateCategory = "UPDATE_CATEGORY"
|
||||
websocketActionUpdateCategoryBoard = "UPDATE_BOARD_CATEGORY"
|
||||
websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION"
|
||||
websocketActionAuth = "AUTH"
|
||||
websocketActionSubscribeTeam = "SUBSCRIBE_TEAM"
|
||||
websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM"
|
||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||
websocketActionUpdateBoard = "UPDATE_BOARD"
|
||||
websocketActionUpdateMember = "UPDATE_MEMBER"
|
||||
websocketActionDeleteMember = "DELETE_MEMBER"
|
||||
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
||||
websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG"
|
||||
websocketActionUpdateCategory = "UPDATE_CATEGORY"
|
||||
websocketActionUpdateCategoryBoard = "UPDATE_BOARD_CATEGORY"
|
||||
websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION"
|
||||
websocketActionUpdateCardLimitTimestamp = "UPDATE_CARD_LIMIT_TIMESTAMP"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
@ -36,5 +37,6 @@ type Adapter interface {
|
||||
BroadcastConfigChange(clientConfig model.ClientConfig)
|
||||
BroadcastCategoryChange(category model.Category)
|
||||
BroadcastCategoryBoardChange(teamID, userID string, blockCategory model.BoardCategoryWebsocketData)
|
||||
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
|
||||
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
||||
}
|
||||
|
@ -39,6 +39,18 @@ type UpdateSubscription struct {
|
||||
Subscription *model.Subscription `json:"subscription"`
|
||||
}
|
||||
|
||||
// UpdateClientConfig is sent on block updates.
|
||||
type UpdateClientConfig struct {
|
||||
Action string `json:"action"`
|
||||
ClientConfig model.ClientConfig `json:"clientconfig"`
|
||||
}
|
||||
|
||||
// UpdateClientConfig is sent on block updates.
|
||||
type UpdateCardLimitTimestamp struct {
|
||||
Action string `json:"action"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WebsocketCommand is an incoming command from the client.
|
||||
type WebsocketCommand struct {
|
||||
Action string `json:"action"`
|
||||
|
@ -30,6 +30,7 @@ type PluginAdapterInterface interface {
|
||||
BroadcastBlockChange(teamID string, block model.Block)
|
||||
BroadcastBlockDelete(teamID, blockID, parentID string)
|
||||
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
||||
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
|
||||
HandleClusterEvent(ev mmModel.PluginClusterEvent)
|
||||
}
|
||||
|
||||
@ -593,3 +594,16 @@ func (pa *PluginAdapter) BroadcastSubscriptionChange(teamID string, subscription
|
||||
|
||||
pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message))
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
|
||||
pa.logger.Debug("BroadcastCardLimitTimestampChange",
|
||||
mlog.Int64("cardLimitTimestamp", cardLimitTimestamp),
|
||||
)
|
||||
|
||||
message := UpdateCardLimitTimestamp{
|
||||
Action: websocketActionUpdateCardLimitTimestamp,
|
||||
Timestamp: cardLimitTimestamp,
|
||||
}
|
||||
|
||||
pa.sendMessageToAll(websocketActionUpdateCardLimitTimestamp, utils.StructToMap(message))
|
||||
}
|
||||
|
@ -55,12 +55,6 @@ type Server struct {
|
||||
store Store
|
||||
}
|
||||
|
||||
// UpdateClientConfig is sent on block updates.
|
||||
type UpdateClientConfig struct {
|
||||
Action string `json:"action"`
|
||||
ClientConfig model.ClientConfig `json:"clientconfig"`
|
||||
}
|
||||
|
||||
type websocketSession struct {
|
||||
conn *websocket.Conn
|
||||
userID string
|
||||
@ -754,3 +748,7 @@ func (ws *Server) BroadcastMemberDelete(teamID, boardID, userID string) {
|
||||
func (ws *Server) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
|
||||
// not implemented for standalone server.
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
|
||||
// not implemented for standalone server.
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import wsClient, {WSClient} from '../wsclient'
|
||||
|
||||
export default function useCardListener(onChange: (blocks: Block[]) => void, onReconnect: () => void): void {
|
||||
useEffect(() => {
|
||||
// ToDo: does this onChange need boards as well??
|
||||
const onChangeHandler = (_: WSClient, blocks: Block[]) => onChange(blocks)
|
||||
wsClient.addOnChange(onChangeHandler, 'block')
|
||||
wsClient.addOnReconnect(onReconnect)
|
||||
|
@ -141,7 +141,6 @@ class OctoClient {
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: document
|
||||
private teamPath(teamId?: string): string {
|
||||
let teamIdToUse = teamId
|
||||
if (!teamId) {
|
||||
@ -618,14 +617,6 @@ class OctoClient {
|
||||
return this.getBoardsWithPath(path)
|
||||
}
|
||||
|
||||
// Boards
|
||||
// ToDo: .
|
||||
// - goal? make the interface show boards & blocks for boards
|
||||
// - teams (maybe current team)? boards, members, user roles in the store, whatever that is
|
||||
// - selectors for boards, current team, board members
|
||||
// - ops to add/delete a board, add/delete board members, change roles? .
|
||||
// - WS definition and implementation
|
||||
|
||||
async getBoards(): Promise<Board[]> {
|
||||
const path = this.teamPath() + '/boards'
|
||||
return this.getBoardsWithPath(path)
|
||||
|
@ -6,30 +6,6 @@ import {OctoUtils} from './octoUtils'
|
||||
|
||||
import {TestBlockFactory} from './test/testBlockFactory'
|
||||
|
||||
// ToDo: we need a way to duplicate the board first creating a new
|
||||
// board and then dupliating and inserting all its blocks
|
||||
// test('duplicateBlockTree: Board', async () => {
|
||||
// const [blocks, board] = createBoardTree()
|
||||
//
|
||||
// const [newBlocks, newBoard, idMap] = OctoUtils.duplicateBlockTree(blocks, board.id)
|
||||
//
|
||||
// expect(newBlocks.length).toBe(blocks.length)
|
||||
// expect(newSourceBlock.id).not.toBe(sourceBlock)
|
||||
// expect(newSourceBlock.type).toBe(sourceBlock.type)
|
||||
//
|
||||
// // When duplicating a root block, the boardId should be re-mapped
|
||||
// expect(newSourceBlock.boardId).not.toBe(sourceBlock.boardId)
|
||||
// expect(idMap[sourceBlock.id]).toBe(newSourceBlock.id)
|
||||
//
|
||||
// for (const newBlock of newBlocks) {
|
||||
// expect(newBlock.boardId).toBe(newSourceBlock.id)
|
||||
// }
|
||||
//
|
||||
// for (const textBlock of newBlocks.filter((o) => o.type === 'card')) {
|
||||
// expect(textBlock.parentId).toBe(newSourceBlock.id)
|
||||
// }
|
||||
// })
|
||||
|
||||
test('duplicateBlockTree: Card', async () => {
|
||||
const [blocks, sourceBlock] = createCardTree()
|
||||
|
||||
@ -52,27 +28,6 @@ test('duplicateBlockTree: Card', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// function createBoardTree(): [Block[], Board] {
|
||||
// const blocks: Block[] = []
|
||||
|
||||
// const board = TestBlockFactory.createBoard()
|
||||
// board.id = 'board1'
|
||||
|
||||
// for (let i = 0; i < 5; i++) {
|
||||
// const card = TestBlockFactory.createCard(board)
|
||||
// card.id = `card${i}`
|
||||
// blocks.push(card)
|
||||
|
||||
// for (let j = 0; j < 3; j++) {
|
||||
// const textBlock = TestBlockFactory.createText(card)
|
||||
// textBlock.id = `text${j}`
|
||||
// blocks.push(textBlock)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return [blocks, board]
|
||||
// }
|
||||
|
||||
function createCardTree(): [Block[], Block] {
|
||||
const blocks: Block[] = []
|
||||
|
||||
|
@ -56,10 +56,6 @@ const WebsocketConnection = (props: Props) => {
|
||||
}
|
||||
|
||||
const incrementalBlockUpdate = (_: WSClient, blocks: Block[]) => {
|
||||
// ToDo: update this
|
||||
// - create a selector to get user boards
|
||||
// - replace the teamId check of blocks by a "is in my boards" check
|
||||
/* const teamBlocks = blocks.filter((b: Block) => b.teamId === '0' || b.boardId in userBoardIds) */
|
||||
const teamBlocks = blocks
|
||||
|
||||
batch(() => {
|
||||
|
@ -10,8 +10,6 @@ import {Constants} from "../constants"
|
||||
|
||||
import {RootState} from './index'
|
||||
|
||||
// ToDo: move this to team templates or simply templates
|
||||
|
||||
export const fetchGlobalTemplates = createAsyncThunk(
|
||||
'globalTemplates/fetch',
|
||||
async () => {
|
||||
|
@ -27,6 +27,7 @@ export type WSMessage = {
|
||||
error?: string
|
||||
teamId?: string
|
||||
member?: BoardMember
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export const ACTION_UPDATE_BOARD = 'UPDATE_BOARD'
|
||||
@ -42,6 +43,7 @@ export const ACTION_UPDATE_CLIENT_CONFIG = 'UPDATE_CLIENT_CONFIG'
|
||||
export const ACTION_UPDATE_CATEGORY = 'UPDATE_CATEGORY'
|
||||
export const ACTION_UPDATE_BOARD_CATEGORY = 'UPDATE_BOARD_CATEGORY'
|
||||
export const ACTION_UPDATE_SUBSCRIPTION = 'UPDATE_SUBSCRIPTION'
|
||||
export const ACTION_UPDATE_CARD_LIMIT_TIMESTAMP = 'UPDATE_CARD_LIMIT_TIMESTAMP'
|
||||
|
||||
type WSSubscriptionMsg = {
|
||||
action?: string
|
||||
@ -74,6 +76,7 @@ type OnReconnectHandler = (client: WSClient) => void
|
||||
type OnStateChangeHandler = (client: WSClient, state: 'init' | 'open' | 'close') => void
|
||||
type OnErrorHandler = (client: WSClient, e: Event) => void
|
||||
type OnConfigChangeHandler = (client: WSClient, clientConfig: ClientConfig) => void
|
||||
type OnCardLimitTimestampChangeHandler = (client: WSClient, timestamp: number) => void
|
||||
type FollowChangeHandler = (client: WSClient, subscription: Subscription) => void
|
||||
|
||||
export type ChangeHandlerType = 'block' | 'category' | 'blockCategories' | 'board' | 'boardMembers'
|
||||
@ -110,6 +113,7 @@ class WSClient {
|
||||
onChange: ChangeHandlers = {Block: [], Category: [], BoardCategory: [], Board: [], BoardMember: []}
|
||||
onError: OnErrorHandler[] = []
|
||||
onConfigChange: OnConfigChangeHandler[] = []
|
||||
onCardLimitTimestampChange: OnCardLimitTimestampChangeHandler[] = []
|
||||
onFollowBlock: FollowChangeHandler = () => {}
|
||||
onUnfollowBlock: FollowChangeHandler = () => {}
|
||||
private notificationDelay = 100
|
||||
@ -254,6 +258,17 @@ class WSClient {
|
||||
}
|
||||
}
|
||||
|
||||
addOnCardLimitTimestampChange(handler: OnCardLimitTimestampChangeHandler): void {
|
||||
this.onCardLimitTimestampChange.push(handler)
|
||||
}
|
||||
|
||||
removeOnCardLimitTimestampChange(handler: OnCardLimitTimestampChangeHandler): void {
|
||||
const index = this.onCardLimitTimestampChange.indexOf(handler)
|
||||
if (index !== -1) {
|
||||
this.onCardLimitTimestampChange.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
open(): void {
|
||||
if (this.client !== null) {
|
||||
// configure the Mattermost websocket client callbacks
|
||||
@ -431,6 +446,12 @@ class WSClient {
|
||||
}
|
||||
}
|
||||
|
||||
updateCardLimitTimestampHandler(action: {action: string, timestamp: number}): void {
|
||||
for (const handler of this.onCardLimitTimestampChange) {
|
||||
handler(this, action.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
updateSubscriptionHandler(message: WSSubscriptionMsg): void {
|
||||
Utils.log('updateSubscriptionHandler: ' + message.action + '; blockId=' + message.subscription?.blockId)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user