mirror of
https://github.com/mattermost/focalboard.git
synced 2025-04-11 11:19:56 +02:00
Display board statistics (#4025)
* initial commit for displaying board statistics * lint fixes * i18n-extract, remove log entries, cleanup * more lint fixes * add check for standalone mode * update tests due to change to NotImplemented * lint fix * revert removing empty comment lines Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
e9d4aeba0e
commit
e3ae682eea
@ -27,7 +27,7 @@ func normalizeAppErr(appErr *mm_model.AppError) error {
|
|||||||
// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can
|
// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can
|
||||||
// be used as per the Plugin API.
|
// be used as per the Plugin API.
|
||||||
// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app
|
// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app
|
||||||
// can be modified to use the services in modular fashion.
|
// can be modified to use the services in modular fashion.
|
||||||
type serviceAPIAdapter struct {
|
type serviceAPIAdapter struct {
|
||||||
api *boardsProduct
|
api *boardsProduct
|
||||||
ctx *request.Context
|
ctx *request.Context
|
||||||
@ -123,6 +123,10 @@ func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_mode
|
|||||||
// Permissions service.
|
// Permissions service.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
|
||||||
|
return a.api.permissionsService.HasPermissionTo(userID, permission)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
||||||
return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission)
|
return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission)
|
||||||
}
|
}
|
||||||
@ -134,6 +138,7 @@ func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelI
|
|||||||
//
|
//
|
||||||
// Bot service.
|
// Bot service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
||||||
return a.api.botService.EnsureBot(a.ctx, boardsProductID, bot)
|
return a.api.botService.EnsureBot(a.ctx, boardsProductID, bot)
|
||||||
}
|
}
|
||||||
@ -141,6 +146,7 @@ func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
|||||||
//
|
//
|
||||||
// License service.
|
// License service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
|
func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
|
||||||
return a.api.licenseService.GetLicense()
|
return a.api.licenseService.GetLicense()
|
||||||
}
|
}
|
||||||
@ -148,6 +154,7 @@ func (a *serviceAPIAdapter) GetLicense() *mm_model.License {
|
|||||||
//
|
//
|
||||||
// FileInfoStore service.
|
// FileInfoStore service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
||||||
fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID)
|
fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID)
|
||||||
return fi, normalizeAppErr(appErr)
|
return fi, normalizeAppErr(appErr)
|
||||||
@ -156,6 +163,7 @@ func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, erro
|
|||||||
//
|
//
|
||||||
// Cluster store.
|
// Cluster store.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
||||||
a.api.clusterService.PublishWebSocketEvent(boardsProductID, event, payload, broadcast)
|
a.api.clusterService.PublishWebSocketEvent(boardsProductID, event, payload, broadcast)
|
||||||
}
|
}
|
||||||
@ -167,6 +175,7 @@ func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterE
|
|||||||
//
|
//
|
||||||
// Cloud service.
|
// Cloud service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
||||||
return a.api.cloudService.GetCloudLimits()
|
return a.api.cloudService.GetCloudLimits()
|
||||||
}
|
}
|
||||||
@ -174,6 +183,7 @@ func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
|||||||
//
|
//
|
||||||
// Config service.
|
// Config service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
|
func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
|
||||||
return a.api.configService.Config()
|
return a.api.configService.Config()
|
||||||
}
|
}
|
||||||
@ -181,6 +191,7 @@ func (a *serviceAPIAdapter) GetConfig() *mm_model.Config {
|
|||||||
//
|
//
|
||||||
// Logger service.
|
// Logger service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace {
|
func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace {
|
||||||
return a.api.logger
|
return a.api.logger
|
||||||
}
|
}
|
||||||
@ -188,6 +199,7 @@ func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace {
|
|||||||
//
|
//
|
||||||
// KVStore service.
|
// KVStore service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
||||||
b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(boardsProductID, key, value, options)
|
b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(boardsProductID, key, value, options)
|
||||||
return b, normalizeAppErr(appErr)
|
return b, normalizeAppErr(appErr)
|
||||||
@ -196,6 +208,7 @@ func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options m
|
|||||||
//
|
//
|
||||||
// Store service.
|
// Store service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
||||||
return a.api.storeService.GetMasterDB(), nil
|
return a.api.storeService.GetMasterDB(), nil
|
||||||
}
|
}
|
||||||
@ -203,6 +216,7 @@ func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
|||||||
//
|
//
|
||||||
// System service.
|
// System service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetDiagnosticID() string {
|
func (a *serviceAPIAdapter) GetDiagnosticID() string {
|
||||||
return a.api.systemService.GetDiagnosticId()
|
return a.api.systemService.GetDiagnosticId()
|
||||||
}
|
}
|
||||||
@ -210,6 +224,7 @@ func (a *serviceAPIAdapter) GetDiagnosticID() string {
|
|||||||
//
|
//
|
||||||
// Router service.
|
// Router service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
||||||
a.api.routerService.RegisterRouter(boardsProductName, sub)
|
a.api.routerService.RegisterRouter(boardsProductName, sub)
|
||||||
}
|
}
|
||||||
@ -217,6 +232,7 @@ func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
|||||||
//
|
//
|
||||||
// Preferences service.
|
// Preferences service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||||
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
|
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
|
||||||
return p, normalizeAppErr(appErr)
|
return p, normalizeAppErr(appErr)
|
||||||
|
@ -125,6 +125,10 @@ func (a *pluginAPIAdapter) CreateMember(teamID string, userID string) (*mm_model
|
|||||||
// Permissions service.
|
// Permissions service.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
func (a *pluginAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool {
|
||||||
|
return a.api.HasPermissionTo(userID, permission)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
func (a *pluginAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool {
|
||||||
return a.api.HasPermissionToTeam(userID, teamID, permission)
|
return a.api.HasPermissionToTeam(userID, teamID, permission)
|
||||||
}
|
}
|
||||||
@ -136,6 +140,7 @@ func (a *pluginAPIAdapter) HasPermissionToChannel(askingUserID string, channelID
|
|||||||
//
|
//
|
||||||
// Bot service.
|
// Bot service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
func (a *pluginAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
||||||
return a.api.EnsureBotUser(bot)
|
return a.api.EnsureBotUser(bot)
|
||||||
}
|
}
|
||||||
@ -143,6 +148,7 @@ func (a *pluginAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) {
|
|||||||
//
|
//
|
||||||
// License service.
|
// License service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetLicense() *mm_model.License {
|
func (a *pluginAPIAdapter) GetLicense() *mm_model.License {
|
||||||
return a.api.GetLicense()
|
return a.api.GetLicense()
|
||||||
}
|
}
|
||||||
@ -150,6 +156,7 @@ func (a *pluginAPIAdapter) GetLicense() *mm_model.License {
|
|||||||
//
|
//
|
||||||
// FileInfoStore service.
|
// FileInfoStore service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
func (a *pluginAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) {
|
||||||
fi, appErr := a.api.GetFileInfo(fileID)
|
fi, appErr := a.api.GetFileInfo(fileID)
|
||||||
return fi, normalizeAppErr(appErr)
|
return fi, normalizeAppErr(appErr)
|
||||||
@ -158,6 +165,7 @@ func (a *pluginAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error
|
|||||||
//
|
//
|
||||||
// Cluster store.
|
// Cluster store.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
func (a *pluginAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) {
|
||||||
a.api.PublishWebSocketEvent(event, payload, broadcast)
|
a.api.PublishWebSocketEvent(event, payload, broadcast)
|
||||||
}
|
}
|
||||||
@ -169,6 +177,7 @@ func (a *pluginAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEv
|
|||||||
//
|
//
|
||||||
// Cloud service.
|
// Cloud service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
func (a *pluginAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
||||||
return a.api.GetCloudLimits()
|
return a.api.GetCloudLimits()
|
||||||
}
|
}
|
||||||
@ -176,6 +185,7 @@ func (a *pluginAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) {
|
|||||||
//
|
//
|
||||||
// Config service.
|
// Config service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetConfig() *mm_model.Config {
|
func (a *pluginAPIAdapter) GetConfig() *mm_model.Config {
|
||||||
return a.api.GetUnsanitizedConfig()
|
return a.api.GetUnsanitizedConfig()
|
||||||
}
|
}
|
||||||
@ -183,6 +193,7 @@ func (a *pluginAPIAdapter) GetConfig() *mm_model.Config {
|
|||||||
//
|
//
|
||||||
// Logger service.
|
// Logger service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetLogger() mlog.LoggerIFace {
|
func (a *pluginAPIAdapter) GetLogger() mlog.LoggerIFace {
|
||||||
return a.logger
|
return a.logger
|
||||||
}
|
}
|
||||||
@ -190,6 +201,7 @@ func (a *pluginAPIAdapter) GetLogger() mlog.LoggerIFace {
|
|||||||
//
|
//
|
||||||
// KVStore service.
|
// KVStore service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
func (a *pluginAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) {
|
||||||
b, appErr := a.api.KVSetWithOptions(key, value, options)
|
b, appErr := a.api.KVSetWithOptions(key, value, options)
|
||||||
return b, normalizeAppErr(appErr)
|
return b, normalizeAppErr(appErr)
|
||||||
@ -198,6 +210,7 @@ func (a *pluginAPIAdapter) KVSetWithOptions(key string, value []byte, options mm
|
|||||||
//
|
//
|
||||||
// Store service.
|
// Store service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
func (a *pluginAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
||||||
return a.storeService.GetMasterDB()
|
return a.storeService.GetMasterDB()
|
||||||
}
|
}
|
||||||
@ -205,6 +218,7 @@ func (a *pluginAPIAdapter) GetMasterDB() (*sql.DB, error) {
|
|||||||
//
|
//
|
||||||
// System service.
|
// System service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetDiagnosticID() string {
|
func (a *pluginAPIAdapter) GetDiagnosticID() string {
|
||||||
return a.api.GetDiagnosticId()
|
return a.api.GetDiagnosticId()
|
||||||
}
|
}
|
||||||
@ -212,6 +226,7 @@ func (a *pluginAPIAdapter) GetDiagnosticID() string {
|
|||||||
//
|
//
|
||||||
// Router service.
|
// Router service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
|
func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
|
||||||
// NOOP for plugin
|
// NOOP for plugin
|
||||||
}
|
}
|
||||||
@ -219,6 +234,7 @@ func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
|
|||||||
//
|
//
|
||||||
// Preferences service.
|
// Preferences service.
|
||||||
//
|
//
|
||||||
|
|
||||||
func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||||
preferences, appErr := a.api.GetPreferencesForUser(userID)
|
preferences, appErr := a.api.GetPreferencesForUser(userID)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
|
@ -352,6 +352,30 @@ export default class Plugin {
|
|||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Site statistics handler
|
||||||
|
if (registry.registerSiteStatisticsHandler) {
|
||||||
|
registry.registerSiteStatisticsHandler(async () => {
|
||||||
|
const siteStats = await octoClient.getSiteStatistics()
|
||||||
|
if(siteStats){
|
||||||
|
return {
|
||||||
|
boards_count: {
|
||||||
|
name: intl.formatMessage({id: 'SiteStats.total_boards', defaultMessage: 'Total Boards'}),
|
||||||
|
id: 'total_boards',
|
||||||
|
icon: 'icon-product-boards',
|
||||||
|
value: siteStats.board_count,
|
||||||
|
},
|
||||||
|
cards_count: {
|
||||||
|
name: intl.formatMessage({id: 'SiteStats.total_cards', defaultMessage: 'Total Cards'}),
|
||||||
|
id: 'total_cards',
|
||||||
|
icon: 'icon-products',
|
||||||
|
value: siteStats.card_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boardSelectorId = this.registry.registerRootComponent((props: {webSocketClient: MMWebSocketClient}) => (
|
this.boardSelectorId = this.registry.registerRootComponent((props: {webSocketClient: MMWebSocketClient}) => (
|
||||||
|
@ -18,6 +18,7 @@ export interface PluginRegistry {
|
|||||||
registerRightHandSidebarComponent(component: React.ElementType, title: React.Element)
|
registerRightHandSidebarComponent(component: React.ElementType, title: React.Element)
|
||||||
registerRootComponent(component: React.ElementType)
|
registerRootComponent(component: React.ElementType)
|
||||||
registerInsightsHandler(handler: (timeRange: string, page: number, perPage: number, teamId: string, insightType: string) => void)
|
registerInsightsHandler(handler: (timeRange: string, page: number, perPage: number, teamId: string, insightType: string) => void)
|
||||||
|
registerSiteStatisticsHandler(handler: () => void)
|
||||||
|
|
||||||
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
||||||
}
|
}
|
||||||
|
@ -95,6 +95,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
a.registerTemplatesRoutes(apiv2)
|
a.registerTemplatesRoutes(apiv2)
|
||||||
a.registerBoardsRoutes(apiv2)
|
a.registerBoardsRoutes(apiv2)
|
||||||
a.registerBlocksRoutes(apiv2)
|
a.registerBlocksRoutes(apiv2)
|
||||||
|
a.registerStatisticsRoutes(apiv2)
|
||||||
|
|
||||||
// V3 routes
|
// V3 routes
|
||||||
a.registerCardsRoutes(apiv2)
|
a.registerCardsRoutes(apiv2)
|
||||||
|
70
server/api/statistics.go
Normal file
70
server/api/statistics.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *API) registerStatisticsRoutes(r *mux.Router) {
|
||||||
|
// statistics
|
||||||
|
r.HandleFunc("/statistics", a.sessionRequired(a.handleStatistics)).Methods("GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) handleStatistics(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:operation GET /statistics handleStatistics
|
||||||
|
//
|
||||||
|
// Fetches the statistic of the server.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// security:
|
||||||
|
// - BearerAuth: []
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: success
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/BoardStatistics"
|
||||||
|
// default:
|
||||||
|
// description: internal error
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
if !a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// user must have right to access analytics
|
||||||
|
userID := getUserID(r)
|
||||||
|
if !a.permissions.HasPermissionTo(userID, mmModel.PermissionGetAnalytics) {
|
||||||
|
a.errorResponse(w, r, model.NewErrPermission("access denied System Statistics"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
boardCount, err := a.app.GetBoardCount()
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cardCount, err := a.app.GetUsedCardsCount()
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := model.BoardsStatistics{
|
||||||
|
Boards: int(boardCount),
|
||||||
|
Cards: cardCount,
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(stats)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytesResponse(w, http.StatusOK, data)
|
||||||
|
}
|
@ -55,6 +55,10 @@ func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) {
|
|||||||
return boardsCloudLimits, nil
|
return boardsCloudLimits, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetUsedCardsCount() (int, error) {
|
||||||
|
return a.store.GetUsedCardsCount()
|
||||||
|
}
|
||||||
|
|
||||||
// IsCloud returns true if the server is running as a plugin in a
|
// IsCloud returns true if the server is running as a plugin in a
|
||||||
// cloud licensed server.
|
// cloud licensed server.
|
||||||
func (a *App) IsCloud() bool {
|
func (a *App) IsCloud() bool {
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -56,7 +55,7 @@ func BuildErrorResponse(r *http.Response, err error) *Response {
|
|||||||
|
|
||||||
func closeBody(r *http.Response) {
|
func closeBody(r *http.Response) {
|
||||||
if r.Body != nil {
|
if r.Body != nil {
|
||||||
_, _ = io.Copy(ioutil.Discard, r.Body)
|
_, _ = io.Copy(io.Discard, r.Body)
|
||||||
_ = r.Body.Close()
|
_ = r.Body.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -903,3 +902,19 @@ func (c *Client) GetLimits() (*model.BoardsCloudLimits, *Response) {
|
|||||||
|
|
||||||
return limits, BuildResponse(r)
|
return limits, BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetStatistics() (*model.BoardsStatistics, *Response) {
|
||||||
|
r, err := c.DoAPIGet("/statistics", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
var stats *model.BoardsStatistics
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&stats)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, BuildResponse(r)
|
||||||
|
}
|
||||||
|
@ -72,6 +72,10 @@ type TestHelper struct {
|
|||||||
|
|
||||||
type FakePermissionPluginAPI struct{}
|
type FakePermissionPluginAPI struct{}
|
||||||
|
|
||||||
|
func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mmModel.Permission) bool {
|
||||||
|
return userID == userAdmin
|
||||||
|
}
|
||||||
|
|
||||||
func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool {
|
func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool {
|
||||||
if userID == userNoTeamMember {
|
if userID == userNoTeamMember {
|
||||||
return false
|
return false
|
||||||
|
@ -3773,3 +3773,40 @@ func TestPermissionsChannel(t *testing.T) {
|
|||||||
runTestCases(t, ttCases, testData, clients)
|
runTestCases(t, ttCases, testData, clients)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPermissionsGetStatistics(t *testing.T) {
|
||||||
|
t.Run("plugin", func(t *testing.T) {
|
||||||
|
th := SetupTestHelperPluginMode(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
clients := setupClients(th)
|
||||||
|
testData := setupData(t, th)
|
||||||
|
ttCases := []TestCase{
|
||||||
|
{"/statistics", methodGet, "", userAnon, http.StatusUnauthorized, 0},
|
||||||
|
{"/statistics", methodGet, "", userNoTeamMember, http.StatusForbidden, 0},
|
||||||
|
{"/statistics", methodGet, "", userTeamMember, http.StatusForbidden, 0},
|
||||||
|
{"/statistics", methodGet, "", userViewer, http.StatusForbidden, 0},
|
||||||
|
{"/statistics", methodGet, "", userCommenter, http.StatusForbidden, 0},
|
||||||
|
{"/statistics", methodGet, "", userEditor, http.StatusForbidden, 0},
|
||||||
|
{"/statistics", methodGet, "", userAdmin, http.StatusOK, 1},
|
||||||
|
{"/statistics", methodGet, "", userGuest, http.StatusForbidden, 0},
|
||||||
|
}
|
||||||
|
runTestCases(t, ttCases, testData, clients)
|
||||||
|
})
|
||||||
|
t.Run("local", func(t *testing.T) {
|
||||||
|
th := SetupTestHelperLocalMode(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
clients := setupLocalClients(th)
|
||||||
|
testData := setupData(t, th)
|
||||||
|
ttCases := []TestCase{
|
||||||
|
{"/statistics", methodGet, "", userAnon, http.StatusUnauthorized, 0},
|
||||||
|
{"/statistics", methodGet, "", userNoTeamMember, http.StatusNotImplemented, 0},
|
||||||
|
{"/statistics", methodGet, "", userTeamMember, http.StatusNotImplemented, 0},
|
||||||
|
{"/statistics", methodGet, "", userViewer, http.StatusNotImplemented, 0},
|
||||||
|
{"/statistics", methodGet, "", userCommenter, http.StatusNotImplemented, 0},
|
||||||
|
{"/statistics", methodGet, "", userEditor, http.StatusNotImplemented, 0},
|
||||||
|
{"/statistics", methodGet, "", userAdmin, http.StatusNotImplemented, 1},
|
||||||
|
{"/statistics", methodGet, "", userGuest, http.StatusForbidden, 0},
|
||||||
|
}
|
||||||
|
runTestCases(t, ttCases, testData, clients)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
55
server/integrationtests/statistics_test.go
Normal file
55
server/integrationtests/statistics_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package integrationtests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/client"
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatisticsLocalMode(t *testing.T) {
|
||||||
|
th := SetupTestHelper(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
t.Run("an unauthenticated client should not be able to get statistics", func(t *testing.T) {
|
||||||
|
th.Logout(th.Client)
|
||||||
|
|
||||||
|
stats, resp := th.Client.GetStatistics()
|
||||||
|
th.CheckUnauthorized(resp)
|
||||||
|
require.Nil(t, stats)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Check authenticated user, not admin", func(t *testing.T) {
|
||||||
|
th.Login1()
|
||||||
|
|
||||||
|
stats, resp := th.Client.GetStatistics()
|
||||||
|
th.CheckNotImplemented(resp)
|
||||||
|
require.Nil(t, stats)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatisticsPluginMode(t *testing.T) {
|
||||||
|
th := SetupTestHelperPluginMode(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
// Permissions are tested in permissions_test.go
|
||||||
|
// This tests the functionality.
|
||||||
|
t.Run("Check authenticated user, admin", func(t *testing.T) {
|
||||||
|
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||||
|
th.Client.HTTPHeader["Mattermost-User-Id"] = userAdmin
|
||||||
|
|
||||||
|
stats, resp := th.Client.GetStatistics()
|
||||||
|
th.CheckOK(resp)
|
||||||
|
require.NotNil(t, stats)
|
||||||
|
|
||||||
|
numberCards := 2
|
||||||
|
th.CreateBoardAndCards("testTeam", model.BoardTypeOpen, numberCards)
|
||||||
|
|
||||||
|
stats, resp = th.Client.GetStatistics()
|
||||||
|
th.CheckOK(resp)
|
||||||
|
require.NotNil(t, stats)
|
||||||
|
require.Equal(t, 1, stats.Boards)
|
||||||
|
require.Equal(t, numberCards, stats.Cards)
|
||||||
|
})
|
||||||
|
}
|
15
server/model/board_statistics.go
Normal file
15
server/model/board_statistics.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
package model
|
||||||
|
|
||||||
|
// BoardsStatistics is the representation of the statistics for the Boards server
|
||||||
|
// swagger:model
|
||||||
|
type BoardsStatistics struct {
|
||||||
|
// The maximum number of cards on the server
|
||||||
|
// required: true
|
||||||
|
Boards int `json:"board_count"`
|
||||||
|
|
||||||
|
// The maximum number of cards on the server
|
||||||
|
// required: true
|
||||||
|
Cards int `json:"card_count"`
|
||||||
|
}
|
@ -347,6 +347,20 @@ func (mr *MockServicesAPIMockRecorder) GetUsersFromProfiles(arg0 interface{}) *g
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersFromProfiles", reflect.TypeOf((*MockServicesAPI)(nil).GetUsersFromProfiles), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersFromProfiles", reflect.TypeOf((*MockServicesAPI)(nil).GetUsersFromProfiles), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasPermissionTo mocks base method.
|
||||||
|
func (m *MockServicesAPI) HasPermissionTo(arg0 string, arg1 *model.Permission) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "HasPermissionTo", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermissionTo indicates an expected call of HasPermissionTo.
|
||||||
|
func (mr *MockServicesAPIMockRecorder) HasPermissionTo(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionTo", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionTo), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// HasPermissionToChannel mocks base method.
|
// HasPermissionToChannel mocks base method.
|
||||||
func (m *MockServicesAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool {
|
func (m *MockServicesAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -49,6 +49,7 @@ type ServicesAPI interface {
|
|||||||
CreateMember(teamID string, userID string) (*mm_model.TeamMember, error)
|
CreateMember(teamID string, userID string) (*mm_model.TeamMember, error)
|
||||||
|
|
||||||
// Permissions service
|
// Permissions service
|
||||||
|
HasPermissionTo(userID string, permission *mm_model.Permission) bool
|
||||||
HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool
|
HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool
|
||||||
HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool
|
HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool
|
||||||
|
|
||||||
|
@ -23,6 +23,10 @@ func New(store permissions.Store, logger mlog.LoggerIFace) *Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) HasPermissionTo(userID string, permission *mmModel.Permission) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool {
|
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool {
|
||||||
if userID == "" || teamID == "" || permission == nil {
|
if userID == "" || teamID == "" || permission == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIInterface interface {
|
type APIInterface interface {
|
||||||
|
HasPermissionTo(userID string, permission *mmModel.Permission) bool
|
||||||
HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool
|
HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool
|
||||||
HasPermissionToChannel(userID string, channelID string, permission *mmModel.Permission) bool
|
HasPermissionToChannel(userID string, channelID string, permission *mmModel.Permission) bool
|
||||||
}
|
}
|
||||||
@ -30,6 +31,13 @@ func New(store permissions.Store, api APIInterface, logger mlog.LoggerIFace) *Se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) HasPermissionTo(userID string, permission *mmModel.Permission) bool {
|
||||||
|
if userID == "" || permission == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.api.HasPermissionTo(userID, permission)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool {
|
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool {
|
||||||
if userID == "" || teamID == "" || permission == nil {
|
if userID == "" || teamID == "" || permission == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PermissionsService interface {
|
type PermissionsService interface {
|
||||||
|
HasPermissionTo(userID string, permission *mmModel.Permission) bool
|
||||||
HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool
|
HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool
|
||||||
HasPermissionToChannel(userID, channelID string, permission *mmModel.Permission) bool
|
HasPermissionToChannel(userID, channelID string, permission *mmModel.Permission) bool
|
||||||
HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool
|
HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool
|
||||||
|
@ -521,7 +521,8 @@ func (s *SQLStore) getBoardCount(db sq.BaseRunner) (int64, error) {
|
|||||||
query := s.getQueryBuilder(db).
|
query := s.getQueryBuilder(db).
|
||||||
Select("COUNT(*) AS count").
|
Select("COUNT(*) AS count").
|
||||||
From(s.tablePrefix + "boards").
|
From(s.tablePrefix + "boards").
|
||||||
Where(sq.Eq{"delete_at": 0})
|
Where(sq.Eq{"delete_at": 0}).
|
||||||
|
Where(sq.Eq{"is_template": false})
|
||||||
|
|
||||||
row := query.QueryRow()
|
row := query.QueryRow()
|
||||||
|
|
||||||
|
@ -257,6 +257,8 @@
|
|||||||
"SidebarTour.SidebarCategories.Body": "All your boards are now organized under your new sidebar. No more switching between workspaces. One-time custom categories based on your prior workspaces may have automatically been created for you as part of your v7.2 upgrade. These can be removed or edited to your preference.",
|
"SidebarTour.SidebarCategories.Body": "All your boards are now organized under your new sidebar. No more switching between workspaces. One-time custom categories based on your prior workspaces may have automatically been created for you as part of your v7.2 upgrade. These can be removed or edited to your preference.",
|
||||||
"SidebarTour.SidebarCategories.Link": "Learn more",
|
"SidebarTour.SidebarCategories.Link": "Learn more",
|
||||||
"SidebarTour.SidebarCategories.Title": "Sidebar categories",
|
"SidebarTour.SidebarCategories.Title": "Sidebar categories",
|
||||||
|
"SiteStats.total_boards": "Total Boards",
|
||||||
|
"SiteStats.total_cards": "Total Cards",
|
||||||
"TableComponent.add-icon": "Add icon",
|
"TableComponent.add-icon": "Add icon",
|
||||||
"TableComponent.name": "Name",
|
"TableComponent.name": "Name",
|
||||||
"TableComponent.plus-new": "+ New",
|
"TableComponent.plus-new": "+ New",
|
||||||
|
@ -17,6 +17,7 @@ import {Constants} from './constants'
|
|||||||
|
|
||||||
import {BoardsCloudLimits} from './boardsCloudLimits'
|
import {BoardsCloudLimits} from './boardsCloudLimits'
|
||||||
import {TopBoardResponse} from './insights'
|
import {TopBoardResponse} from './insights'
|
||||||
|
import {BoardSiteStatistics} from './statistics'
|
||||||
|
|
||||||
//
|
//
|
||||||
// OctoClient is the client interface to the server APIs
|
// OctoClient is the client interface to the server APIs
|
||||||
@ -921,6 +922,18 @@ class OctoClient {
|
|||||||
return limits
|
return limits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSiteStatistics(): Promise<BoardSiteStatistics | undefined> {
|
||||||
|
const path = '/api/v2/statistics'
|
||||||
|
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = (await this.getJson(response, {})) as BoardSiteStatistics
|
||||||
|
Utils.log(`Site Statistics: cards=${stats.card_count} boards=${stats.board_count}`)
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
// insights
|
// insights
|
||||||
async getMyTopBoards(timeRange: string, page: number, perPage: number, teamId: string): Promise<TopBoardResponse | undefined> {
|
async getMyTopBoards(timeRange: string, page: number, perPage: number, teamId: string): Promise<TopBoardResponse | undefined> {
|
||||||
const path = `/api/v2/users/me/boards/insights?time_range=${timeRange}&page=${page}&per_page=${perPage}&team_id=${teamId}`
|
const path = `/api/v2/users/me/boards/insights?time_range=${timeRange}&page=${page}&per_page=${perPage}&team_id=${teamId}`
|
||||||
|
7
webapp/src/statistics/index.ts
Normal file
7
webapp/src/statistics/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export interface BoardSiteStatistics {
|
||||||
|
board_count: number
|
||||||
|
card_count: number
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user