You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +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:
		| @@ -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 | ||||
| // 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 | ||||
| //       can be modified to use the services in modular fashion. | ||||
| // can be modified to use the services in modular fashion. | ||||
| type serviceAPIAdapter struct { | ||||
| 	api *boardsProduct | ||||
| 	ctx *request.Context | ||||
| @@ -123,6 +123,10 @@ func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_mode | ||||
| // 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 { | ||||
| 	return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission) | ||||
| } | ||||
| @@ -134,6 +138,7 @@ func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelI | ||||
| // | ||||
| // Bot service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) { | ||||
| 	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. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetLicense() *mm_model.License { | ||||
| 	return a.api.licenseService.GetLicense() | ||||
| } | ||||
| @@ -148,6 +154,7 @@ func (a *serviceAPIAdapter) GetLicense() *mm_model.License { | ||||
| // | ||||
| // FileInfoStore service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) { | ||||
| 	fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID) | ||||
| 	return fi, normalizeAppErr(appErr) | ||||
| @@ -156,6 +163,7 @@ func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, erro | ||||
| // | ||||
| // Cluster store. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) { | ||||
| 	a.api.clusterService.PublishWebSocketEvent(boardsProductID, event, payload, broadcast) | ||||
| } | ||||
| @@ -167,6 +175,7 @@ func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterE | ||||
| // | ||||
| // Cloud service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) { | ||||
| 	return a.api.cloudService.GetCloudLimits() | ||||
| } | ||||
| @@ -174,6 +183,7 @@ func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) { | ||||
| // | ||||
| // Config service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetConfig() *mm_model.Config { | ||||
| 	return a.api.configService.Config() | ||||
| } | ||||
| @@ -181,6 +191,7 @@ func (a *serviceAPIAdapter) GetConfig() *mm_model.Config { | ||||
| // | ||||
| // Logger service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace { | ||||
| 	return a.api.logger | ||||
| } | ||||
| @@ -188,6 +199,7 @@ func (a *serviceAPIAdapter) GetLogger() mlog.LoggerIFace { | ||||
| // | ||||
| // KVStore service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) { | ||||
| 	b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(boardsProductID, key, value, options) | ||||
| 	return b, normalizeAppErr(appErr) | ||||
| @@ -196,6 +208,7 @@ func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options m | ||||
| // | ||||
| // Store service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) { | ||||
| 	return a.api.storeService.GetMasterDB(), nil | ||||
| } | ||||
| @@ -203,6 +216,7 @@ func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) { | ||||
| // | ||||
| // System service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetDiagnosticID() string { | ||||
| 	return a.api.systemService.GetDiagnosticId() | ||||
| } | ||||
| @@ -210,6 +224,7 @@ func (a *serviceAPIAdapter) GetDiagnosticID() string { | ||||
| // | ||||
| // Router service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) { | ||||
| 	a.api.routerService.RegisterRouter(boardsProductName, sub) | ||||
| } | ||||
| @@ -217,6 +232,7 @@ func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) { | ||||
| // | ||||
| // Preferences service. | ||||
| // | ||||
|  | ||||
| func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) { | ||||
| 	p, appErr := a.api.preferencesService.GetPreferencesForUser(userID) | ||||
| 	return p, normalizeAppErr(appErr) | ||||
|   | ||||
| @@ -125,6 +125,10 @@ func (a *pluginAPIAdapter) CreateMember(teamID string, userID string) (*mm_model | ||||
| // 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 { | ||||
| 	return a.api.HasPermissionToTeam(userID, teamID, permission) | ||||
| } | ||||
| @@ -136,6 +140,7 @@ func (a *pluginAPIAdapter) HasPermissionToChannel(askingUserID string, channelID | ||||
| // | ||||
| // Bot service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) { | ||||
| 	return a.api.EnsureBotUser(bot) | ||||
| } | ||||
| @@ -143,6 +148,7 @@ func (a *pluginAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) { | ||||
| // | ||||
| // License service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetLicense() *mm_model.License { | ||||
| 	return a.api.GetLicense() | ||||
| } | ||||
| @@ -150,6 +156,7 @@ func (a *pluginAPIAdapter) GetLicense() *mm_model.License { | ||||
| // | ||||
| // FileInfoStore service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) { | ||||
| 	fi, appErr := a.api.GetFileInfo(fileID) | ||||
| 	return fi, normalizeAppErr(appErr) | ||||
| @@ -158,6 +165,7 @@ func (a *pluginAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error | ||||
| // | ||||
| // Cluster store. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) { | ||||
| 	a.api.PublishWebSocketEvent(event, payload, broadcast) | ||||
| } | ||||
| @@ -169,6 +177,7 @@ func (a *pluginAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEv | ||||
| // | ||||
| // Cloud service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) { | ||||
| 	return a.api.GetCloudLimits() | ||||
| } | ||||
| @@ -176,6 +185,7 @@ func (a *pluginAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) { | ||||
| // | ||||
| // Config service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetConfig() *mm_model.Config { | ||||
| 	return a.api.GetUnsanitizedConfig() | ||||
| } | ||||
| @@ -183,6 +193,7 @@ func (a *pluginAPIAdapter) GetConfig() *mm_model.Config { | ||||
| // | ||||
| // Logger service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetLogger() mlog.LoggerIFace { | ||||
| 	return a.logger | ||||
| } | ||||
| @@ -190,6 +201,7 @@ func (a *pluginAPIAdapter) GetLogger() mlog.LoggerIFace { | ||||
| // | ||||
| // KVStore service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) { | ||||
| 	b, appErr := a.api.KVSetWithOptions(key, value, options) | ||||
| 	return b, normalizeAppErr(appErr) | ||||
| @@ -198,6 +210,7 @@ func (a *pluginAPIAdapter) KVSetWithOptions(key string, value []byte, options mm | ||||
| // | ||||
| // Store service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetMasterDB() (*sql.DB, error) { | ||||
| 	return a.storeService.GetMasterDB() | ||||
| } | ||||
| @@ -205,6 +218,7 @@ func (a *pluginAPIAdapter) GetMasterDB() (*sql.DB, error) { | ||||
| // | ||||
| // System service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetDiagnosticID() string { | ||||
| 	return a.api.GetDiagnosticId() | ||||
| } | ||||
| @@ -212,6 +226,7 @@ func (a *pluginAPIAdapter) GetDiagnosticID() string { | ||||
| // | ||||
| // Router service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) { | ||||
| 	// NOOP for plugin | ||||
| } | ||||
| @@ -219,6 +234,7 @@ func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) { | ||||
| // | ||||
| // Preferences service. | ||||
| // | ||||
|  | ||||
| func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) { | ||||
| 	preferences, appErr := a.api.GetPreferencesForUser(userID) | ||||
| 	if appErr != nil { | ||||
|   | ||||
| @@ -352,6 +352,30 @@ export default class Plugin { | ||||
|                     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}) => ( | ||||
|   | ||||
| @@ -18,6 +18,7 @@ export interface PluginRegistry { | ||||
|     registerRightHandSidebarComponent(component: React.ElementType, title: React.Element) | ||||
|     registerRootComponent(component: React.ElementType) | ||||
|     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 | ||||
| } | ||||
|   | ||||
| @@ -95,6 +95,7 @@ func (a *API) RegisterRoutes(r *mux.Router) { | ||||
| 	a.registerTemplatesRoutes(apiv2) | ||||
| 	a.registerBoardsRoutes(apiv2) | ||||
| 	a.registerBlocksRoutes(apiv2) | ||||
| 	a.registerStatisticsRoutes(apiv2) | ||||
|  | ||||
| 	// V3 routes | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| func (a *App) GetUsedCardsCount() (int, error) { | ||||
| 	return a.store.GetUsedCardsCount() | ||||
| } | ||||
|  | ||||
| // IsCloud returns true if the server is running as a plugin in a | ||||
| // cloud licensed server. | ||||
| func (a *App) IsCloud() bool { | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"mime/multipart" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| @@ -56,7 +55,7 @@ func BuildErrorResponse(r *http.Response, err error) *Response { | ||||
|  | ||||
| func closeBody(r *http.Response) { | ||||
| 	if r.Body != nil { | ||||
| 		_, _ = io.Copy(ioutil.Discard, r.Body) | ||||
| 		_, _ = io.Copy(io.Discard, r.Body) | ||||
| 		_ = r.Body.Close() | ||||
| 	} | ||||
| } | ||||
| @@ -903,3 +902,19 @@ func (c *Client) GetLimits() (*model.BoardsCloudLimits, *Response) { | ||||
|  | ||||
| 	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{} | ||||
|  | ||||
| func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mmModel.Permission) bool { | ||||
| 	return userID == userAdmin | ||||
| } | ||||
|  | ||||
| func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool { | ||||
| 	if userID == userNoTeamMember { | ||||
| 		return false | ||||
|   | ||||
| @@ -3773,3 +3773,40 @@ func TestPermissionsChannel(t *testing.T) { | ||||
| 		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) | ||||
| } | ||||
|  | ||||
| // 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. | ||||
| func (m *MockServicesAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool { | ||||
| 	m.ctrl.T.Helper() | ||||
|   | ||||
| @@ -49,6 +49,7 @@ type ServicesAPI interface { | ||||
| 	CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) | ||||
|  | ||||
| 	// Permissions service | ||||
| 	HasPermissionTo(userID 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 | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
| 	if userID == "" || teamID == "" || permission == nil { | ||||
| 		return false | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| type APIInterface interface { | ||||
| 	HasPermissionTo(userID string, permission *mmModel.Permission) bool | ||||
| 	HasPermissionToTeam(userID string, teamID 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 { | ||||
| 	if userID == "" || teamID == "" || permission == nil { | ||||
| 		return false | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| type PermissionsService interface { | ||||
| 	HasPermissionTo(userID string, permission *mmModel.Permission) bool | ||||
| 	HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool | ||||
| 	HasPermissionToChannel(userID, channelID 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). | ||||
| 		Select("COUNT(*) AS count"). | ||||
| 		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() | ||||
|  | ||||
|   | ||||
| @@ -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.Link": "Learn more", | ||||
|   "SidebarTour.SidebarCategories.Title": "Sidebar categories", | ||||
|   "SiteStats.total_boards": "Total Boards", | ||||
|   "SiteStats.total_cards": "Total Cards", | ||||
|   "TableComponent.add-icon": "Add icon", | ||||
|   "TableComponent.name": "Name", | ||||
|   "TableComponent.plus-new": "+ New", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import {Constants} from './constants' | ||||
|  | ||||
| import {BoardsCloudLimits} from './boardsCloudLimits' | ||||
| import {TopBoardResponse} from './insights' | ||||
| import {BoardSiteStatistics} from './statistics' | ||||
|  | ||||
| // | ||||
| // OctoClient is the client interface to the server APIs | ||||
| @@ -921,6 +922,18 @@ class OctoClient { | ||||
|         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 | ||||
|     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}` | ||||
|   | ||||
							
								
								
									
										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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user