MM 60222 - apply filter to export csv (#28212)
* MM-60222_apply filter to export csv * get the report exporting with the filters ready * add unit tests * cover one more file with some tests * style the confirm modal note * add translations * remove unnecessary print line * disable export button if there is no data to export * fix linter issues * fix linter errors --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -28,6 +28,7 @@ func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
baseOptions := fillReportingBaseOptions(r.URL.Query())
|
baseOptions := fillReportingBaseOptions(r.URL.Query())
|
||||||
options, err := fillUserReportOptions(r.URL.Query())
|
options, err := fillUserReportOptions(r.URL.Query())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
@@ -80,13 +81,21 @@ func startUsersBatchExport(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dateRange := r.URL.Query().Get("date_range")
|
baseOptions := fillReportingBaseOptions(r.URL.Query())
|
||||||
|
options, err := fillUserReportOptions(r.URL.Query())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
options.ReportingBaseOptions = baseOptions
|
||||||
|
dateRange := options.ReportingBaseOptions.DateRange
|
||||||
if dateRange == "" {
|
if dateRange == "" {
|
||||||
dateRange = "all_time"
|
dateRange = "all_time"
|
||||||
}
|
}
|
||||||
|
|
||||||
startAt, endAt := model.GetReportDateRange(dateRange, time.Now())
|
startAt, endAt := model.GetReportDateRange(dateRange, time.Now())
|
||||||
if err := c.App.StartUsersBatchExport(c.AppContext, dateRange, startAt, endAt); err != nil {
|
if err := c.App.StartUsersBatchExport(c.AppContext, options, startAt, endAt); err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -136,7 +145,6 @@ func fillUserReportOptions(values url.Values) (*model.UserReportOptions, *model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.UserReportOptions{
|
return &model.UserReportOptions{
|
||||||
|
|
||||||
Team: teamFilter,
|
Team: teamFilter,
|
||||||
Role: values.Get("role_filter"),
|
Role: values.Get("role_filter"),
|
||||||
HasNoTeam: values.Get("has_no_team") == "true",
|
HasNoTeam: values.Get("has_no_team") == "true",
|
||||||
|
|||||||
154
server/channels/api4/report_test.go
Обычный файл
154
server/channels/api4/report_test.go
Обычный файл
@@ -0,0 +1,154 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetUsersForReporting(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
client := th.Client
|
||||||
|
|
||||||
|
defaultRolePermissions := th.SaveDefaultRolePermissions()
|
||||||
|
defer th.RestoreDefaultRolePermissions(defaultRolePermissions)
|
||||||
|
|
||||||
|
t.Run("should return forbidden error when user lacks permission", func(t *testing.T) {
|
||||||
|
th.RemovePermissionFromRole(model.PermissionSysconsoleReadUserManagementUsers.Id, model.SystemUserRoleId)
|
||||||
|
|
||||||
|
_, resp, err := client.GetUsersForReporting(context.Background(), &model.UserReportOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return user reports when user has permission", func(t *testing.T) {
|
||||||
|
th.AddPermissionToRole(model.PermissionSysconsoleReadUserManagementUsers.Id, model.SystemUserRoleId)
|
||||||
|
|
||||||
|
options := &model.UserReportOptions{
|
||||||
|
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||||
|
PageSize: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userReports, resp, err := client.GetUsersForReporting(context.Background(), options)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, userReports)
|
||||||
|
require.GreaterOrEqual(t, len(userReports), 1)
|
||||||
|
CheckOKStatus(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return bad request on invalid parameters", func(t *testing.T) {
|
||||||
|
th.AddPermissionToRole(model.PermissionSysconsoleReadUserManagementUsers.Id, model.SystemUserRoleId)
|
||||||
|
|
||||||
|
options := &model.UserReportOptions{
|
||||||
|
Team: "invalid_team_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, resp, err := client.GetUsersForReporting(context.Background(), options)
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillReportingBaseOptions(t *testing.T) {
|
||||||
|
t.Run("default values", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
|
||||||
|
options := fillReportingBaseOptions(values)
|
||||||
|
|
||||||
|
require.Equal(t, "Username", options.SortColumn)
|
||||||
|
require.Equal(t, "next", options.Direction)
|
||||||
|
require.Equal(t, false, options.SortDesc)
|
||||||
|
require.Equal(t, 50, options.PageSize)
|
||||||
|
require.Equal(t, "", options.FromColumnValue)
|
||||||
|
require.Equal(t, "", options.FromId)
|
||||||
|
require.Equal(t, "", options.DateRange)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom values", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("sort_column", "Email")
|
||||||
|
values.Set("direction", "prev")
|
||||||
|
values.Set("sort_direction", "desc")
|
||||||
|
values.Set("page_size", "25")
|
||||||
|
values.Set("from_column_value", "some_value")
|
||||||
|
values.Set("from_id", "some_id")
|
||||||
|
values.Set("date_range", "last_seven")
|
||||||
|
|
||||||
|
options := fillReportingBaseOptions(values)
|
||||||
|
|
||||||
|
require.Equal(t, "Email", options.SortColumn)
|
||||||
|
require.Equal(t, "prev", options.Direction)
|
||||||
|
require.Equal(t, true, options.SortDesc)
|
||||||
|
require.Equal(t, 25, options.PageSize)
|
||||||
|
require.Equal(t, "some_value", options.FromColumnValue)
|
||||||
|
require.Equal(t, "some_id", options.FromId)
|
||||||
|
require.Equal(t, "last_seven", options.DateRange)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid page_size", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("page_size", "an_very_invalid_number")
|
||||||
|
|
||||||
|
options := fillReportingBaseOptions(values)
|
||||||
|
|
||||||
|
require.Equal(t, 50, options.PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid direction", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("direction", "a_crazy_direction")
|
||||||
|
|
||||||
|
options := fillReportingBaseOptions(values)
|
||||||
|
|
||||||
|
require.Equal(t, "next", options.Direction)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillUserReportOptions(t *testing.T) {
|
||||||
|
validTeamID := model.NewId()
|
||||||
|
|
||||||
|
t.Run("default values", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("team_filter", validTeamID)
|
||||||
|
|
||||||
|
options, _ := fillUserReportOptions(values)
|
||||||
|
|
||||||
|
expected := &model.UserReportOptions{
|
||||||
|
Team: validTeamID,
|
||||||
|
Role: "",
|
||||||
|
HasNoTeam: false,
|
||||||
|
HideActive: false,
|
||||||
|
HideInactive: false,
|
||||||
|
SearchTerm: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, expected, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty team_filter", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("team_filter", "")
|
||||||
|
|
||||||
|
options, _ := fillUserReportOptions(values)
|
||||||
|
|
||||||
|
require.Equal(t, "", options.Team)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid team_filter", func(t *testing.T) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("team_filter", validTeamID)
|
||||||
|
|
||||||
|
options, _ := fillUserReportOptions(values)
|
||||||
|
|
||||||
|
require.Equal(t, validTeamID, options.Team)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1146,7 +1146,7 @@ type AppIface interface {
|
|||||||
SlackImport(c request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer)
|
SlackImport(c request.CTX, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer)
|
||||||
SoftDeleteTeam(teamID string) *model.AppError
|
SoftDeleteTeam(teamID string) *model.AppError
|
||||||
Srv() *Server
|
Srv() *Server
|
||||||
StartUsersBatchExport(rctx request.CTX, dateRange string, startAt int64, endAt int64) *model.AppError
|
StartUsersBatchExport(rctx request.CTX, ro *model.UserReportOptions, startAt int64, endAt int64) *model.AppError
|
||||||
SubmitInteractiveDialog(c request.CTX, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError)
|
SubmitInteractiveDialog(c request.CTX, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError)
|
||||||
SwitchEmailToLdap(c request.CTX, email, password, code, ldapLoginId, ldapPassword string) (string, *model.AppError)
|
SwitchEmailToLdap(c request.CTX, email, password, code, ldapLoginId, ldapPassword string) (string, *model.AppError)
|
||||||
SwitchEmailToOAuth(c request.CTX, w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError)
|
SwitchEmailToOAuth(c request.CTX, w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError)
|
||||||
|
|||||||
@@ -17236,7 +17236,7 @@ func (a *OpenTracingAppLayer) SoftDeleteTeam(teamID string) *model.AppError {
|
|||||||
return resultVar0
|
return resultVar0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) StartUsersBatchExport(rctx request.CTX, dateRange string, startAt int64, endAt int64) *model.AppError {
|
func (a *OpenTracingAppLayer) StartUsersBatchExport(rctx request.CTX, ro *model.UserReportOptions, startAt int64, endAt int64) *model.AppError {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.StartUsersBatchExport")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.StartUsersBatchExport")
|
||||||
|
|
||||||
@@ -17248,7 +17248,7 @@ func (a *OpenTracingAppLayer) StartUsersBatchExport(rctx request.CTX, dateRange
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
resultVar0 := a.app.StartUsersBatchExport(rctx, dateRange, startAt, endAt)
|
resultVar0 := a.app.StartUsersBatchExport(rctx, ro, startAt, endAt)
|
||||||
|
|
||||||
if resultVar0 != nil {
|
if resultVar0 != nil {
|
||||||
span.LogFields(spanlog.Error(resultVar0))
|
span.LogFields(spanlog.Error(resultVar0))
|
||||||
|
|||||||
@@ -198,41 +198,28 @@ func (a *App) GetUserCountForReport(filter *model.UserReportOptions) (*int64, *m
|
|||||||
return &count, nil
|
return &count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) StartUsersBatchExport(rctx request.CTX, dateRange string, startAt int64, endAt int64) *model.AppError {
|
func (a *App) StartUsersBatchExport(rctx request.CTX, ro *model.UserReportOptions, startAt int64, endAt int64) *model.AppError {
|
||||||
if license := a.Srv().License(); license == nil || (license.SkuShortName != model.LicenseShortSkuProfessional && license.SkuShortName != model.LicenseShortSkuEnterprise) {
|
if license := a.Srv().License(); license == nil || (license.SkuShortName != model.LicenseShortSkuProfessional && license.SkuShortName != model.LicenseShortSkuEnterprise) {
|
||||||
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.license_error", nil, "", http.StatusBadRequest)
|
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.license_error", nil, "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
options := map[string]string{
|
options := map[string]string{
|
||||||
"requesting_user_id": rctx.Session().UserId,
|
"requesting_user_id": rctx.Session().UserId,
|
||||||
"date_range": dateRange,
|
"date_range": ro.DateRange,
|
||||||
|
"role": ro.Role,
|
||||||
|
"team": ro.Team,
|
||||||
|
"hide_active": strconv.FormatBool(ro.HideActive),
|
||||||
|
"hide_inactive": strconv.FormatBool(ro.HideInactive),
|
||||||
"start_at": strconv.FormatInt(startAt, 10),
|
"start_at": strconv.FormatInt(startAt, 10),
|
||||||
"end_at": strconv.FormatInt(endAt, 10),
|
"end_at": strconv.FormatInt(endAt, 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existing job
|
// Check for existing jobs
|
||||||
// TODO: Maybe make this a reusable function?
|
if err := a.checkForExistingJobs(rctx, options, model.JobTypeExportUsersToCSV); err != nil {
|
||||||
pendingJobs, err := a.Srv().Jobs.GetJobsByTypeAndStatus(rctx, model.JobTypeExportUsersToCSV, model.JobStatusPending)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, job := range pendingJobs {
|
|
||||||
if job.Data["date_range"] == options["date_range"] && job.Data["requesting_user_id"] == rctx.Session().UserId {
|
|
||||||
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.job_exists", nil, "", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inProgressJobs, err := a.Srv().Jobs.GetJobsByTypeAndStatus(rctx, model.JobTypeExportUsersToCSV, model.JobStatusInProgress)
|
_, err := a.Srv().Jobs.CreateJob(rctx, model.JobTypeExportUsersToCSV, options)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, job := range inProgressJobs {
|
|
||||||
if job.Data["date_range"] == options["date_range"] && job.Data["requesting_user_id"] == rctx.Session().UserId {
|
|
||||||
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.job_exists", nil, "", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = a.Srv().Jobs.CreateJob(rctx, model.JobTypeExportUsersToCSV, options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -258,7 +245,7 @@ func (a *App) StartUsersBatchExport(rctx request.CTX, dateRange string, startAt
|
|||||||
T := i18n.GetUserTranslations(user.Locale)
|
T := i18n.GetUserTranslations(user.Locale)
|
||||||
post := &model.Post{
|
post := &model.Post{
|
||||||
ChannelId: channel.Id,
|
ChannelId: channel.Id,
|
||||||
Message: T("app.report.start_users_batch_export.started_export", map[string]string{"DateRange": getTranslatedDateRange(dateRange)}),
|
Message: T("app.report.start_users_batch_export.started_export", map[string]string{"DateRange": getTranslatedDateRange(ro.DateRange)}),
|
||||||
Type: model.PostTypeDefault,
|
Type: model.PostTypeDefault,
|
||||||
UserId: systemBot.UserId,
|
UserId: systemBot.UserId,
|
||||||
}
|
}
|
||||||
@@ -271,6 +258,41 @@ func (a *App) StartUsersBatchExport(rctx request.CTX, dateRange string, startAt
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check for existing or pending jobs
|
||||||
|
func (a *App) checkForExistingJobs(rctx request.CTX, options map[string]string, jobType string) *model.AppError {
|
||||||
|
checkJobExists := func(jobs []*model.Job, options map[string]string) bool {
|
||||||
|
for _, job := range jobs {
|
||||||
|
if job.Data["date_range"] == options["date_range"] &&
|
||||||
|
job.Data["requesting_user_id"] == options["requesting_user_id"] &&
|
||||||
|
job.Data["role"] == options["role"] &&
|
||||||
|
job.Data["team"] == options["team"] &&
|
||||||
|
job.Data["hide_active"] == options["hide_active"] &&
|
||||||
|
job.Data["hide_inactive"] == options["hide_inactive"] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingJobs, err := a.Srv().Jobs.GetJobsByTypeAndStatus(rctx, jobType, model.JobStatusPending)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if checkJobExists(pendingJobs, options) {
|
||||||
|
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.job_exists", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgressJobs, err := a.Srv().Jobs.GetJobsByTypeAndStatus(rctx, jobType, model.JobStatusInProgress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if checkJobExists(inProgressJobs, options) {
|
||||||
|
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.job_exists", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getTranslatedDateRange(dateRange string) string {
|
func getTranslatedDateRange(dateRange string) string {
|
||||||
switch dateRange {
|
switch dateRange {
|
||||||
case model.ReportDurationLast30Days:
|
case model.ReportDurationLast30Days:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,3 +108,100 @@ some-other-other-name,600,2022-01-01
|
|||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckForExistingJobs(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
t.Run("should return error if job with same options exists in pending jobs", func(t *testing.T) {
|
||||||
|
app := th.App
|
||||||
|
rctx := request.TestContext(t)
|
||||||
|
options := map[string]string{
|
||||||
|
"date_range": "last_30_days",
|
||||||
|
"requesting_user_id": th.BasicUser.Id,
|
||||||
|
"role": "user",
|
||||||
|
"team": "",
|
||||||
|
"hide_active": "false",
|
||||||
|
"hide_inactive": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
jobType := model.JobTypeExportUsersToCSV
|
||||||
|
|
||||||
|
// Create a pending job with same options
|
||||||
|
job, err := app.Srv().Jobs.CreateJob(rctx, jobType, options)
|
||||||
|
defer func() {
|
||||||
|
_ = app.Srv().Jobs.RequestCancellation(rctx, job.Id)
|
||||||
|
}()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, job)
|
||||||
|
|
||||||
|
// checkForExistingJobs
|
||||||
|
appErr := app.checkForExistingJobs(rctx, options, jobType)
|
||||||
|
require.NotNil(t, appErr)
|
||||||
|
require.Equal(t, "app.report.start_users_batch_export.job_exists", appErr.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if job with same options exists in in-progress jobs", func(t *testing.T) {
|
||||||
|
app := th.App
|
||||||
|
rctx := request.TestContext(t)
|
||||||
|
options := map[string]string{
|
||||||
|
"date_range": "last_30_days",
|
||||||
|
"requesting_user_id": th.BasicUser.Id,
|
||||||
|
"role": "user",
|
||||||
|
"team": "",
|
||||||
|
"hide_active": "false",
|
||||||
|
"hide_inactive": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
jobType := model.JobTypeExportUsersToCSV
|
||||||
|
|
||||||
|
// Create an in-progress job with same options
|
||||||
|
job, err := app.Srv().Jobs.CreateJob(rctx, jobType, options)
|
||||||
|
defer func() {
|
||||||
|
_ = app.Srv().Jobs.RequestCancellation(rctx, job.Id)
|
||||||
|
}()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, job)
|
||||||
|
|
||||||
|
// Manually set job status to in-progress
|
||||||
|
err = app.Srv().Jobs.SetJobProgress(job, 60)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Call checkForExistingJobs
|
||||||
|
appErr := app.checkForExistingJobs(rctx, options, jobType)
|
||||||
|
require.NotNil(t, appErr)
|
||||||
|
require.Equal(t, "app.report.start_users_batch_export.job_exists", appErr.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not return error if existing jobs have different options", func(t *testing.T) {
|
||||||
|
app := th.App
|
||||||
|
rctx := request.TestContext(t)
|
||||||
|
options := map[string]string{
|
||||||
|
"date_range": "last_30_days",
|
||||||
|
"requesting_user_id": th.BasicUser.Id,
|
||||||
|
"role": "user",
|
||||||
|
"team": "",
|
||||||
|
"hide_active": "false",
|
||||||
|
"hide_inactive": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
jobType := model.JobTypeExportUsersToCSV
|
||||||
|
|
||||||
|
differentOptions := map[string]string{
|
||||||
|
"date_range": "all_time",
|
||||||
|
"requesting_user_id": th.BasicUser2.Id,
|
||||||
|
"role": "admin",
|
||||||
|
"team": "",
|
||||||
|
"hide_active": "false",
|
||||||
|
"hide_inactive": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := app.Srv().Jobs.CreateJob(rctx, jobType, differentOptions)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, job)
|
||||||
|
|
||||||
|
// Call checkForExistingJobs
|
||||||
|
appErr := app.checkForExistingJobs(rctx, options, jobType)
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package export_users_to_csv
|
package export_users_to_csv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -60,6 +61,22 @@ func parseJobMetadata(data model.StringMap) (*model.UserReportOptions, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideInactive := false
|
||||||
|
if val, ok := data["hide_inactive"]; ok && val != "" {
|
||||||
|
hideInactive, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse hide_inactive: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideActive := false
|
||||||
|
if val, ok := data["hide_active"]; ok && val != "" {
|
||||||
|
hideActive, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse hide_active: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
options := model.UserReportOptions{
|
options := model.UserReportOptions{
|
||||||
ReportingBaseOptions: model.ReportingBaseOptions{
|
ReportingBaseOptions: model.ReportingBaseOptions{
|
||||||
SortColumn: "Username",
|
SortColumn: "Username",
|
||||||
@@ -69,6 +86,10 @@ func parseJobMetadata(data model.StringMap) (*model.UserReportOptions, error) {
|
|||||||
StartAt: startAt,
|
StartAt: startAt,
|
||||||
EndAt: endAt,
|
EndAt: endAt,
|
||||||
},
|
},
|
||||||
|
HideInactive: hideInactive,
|
||||||
|
HideActive: hideActive,
|
||||||
|
Role: data["role"],
|
||||||
|
Team: data["team"],
|
||||||
}
|
}
|
||||||
|
|
||||||
return &options, nil
|
return &options, nil
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {ServerError} from '@mattermost/types/errors';
|
import type {ServerError} from '@mattermost/types/errors';
|
||||||
import type {UserReportOptions, UserReport, UserReportFilter, ReportDuration} from '@mattermost/types/reports';
|
import type {UserReportOptions, UserReport, UserReportFilter} from '@mattermost/types/reports';
|
||||||
|
|
||||||
import {logError} from 'mattermost-redux/actions/errors';
|
import {logError} from 'mattermost-redux/actions/errors';
|
||||||
import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
|
import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
|
||||||
@@ -67,10 +67,10 @@ export function getUserCountForReporting(filter = {} as UserReportFilter): Actio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startUsersBatchExport(dateRange: ReportDuration): ActionFuncAsync {
|
export function startUsersBatchExport(tableFilters = {} as UserReportOptions): ActionFuncAsync {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
try {
|
try {
|
||||||
await Client4.startUsersBatchExport(dateRange);
|
await Client4.startUsersBatchExport(tableFilters);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ function SystemUsers(props: Props) {
|
|||||||
/>
|
/>
|
||||||
<SystemUsersExport
|
<SystemUsersExport
|
||||||
currentUserId={props.currentUser.id}
|
currentUserId={props.currentUser.id}
|
||||||
dateRange={props.tablePropertyDateRange}
|
usersLenght={userReports.length}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AdminConsoleListTable<UserReport>
|
<AdminConsoleListTable<UserReport>
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExportUserDataModal({onConfirm, onExited}: Props) {
|
export function ExportUserDataModal({onConfirm, onExited}: Props) {
|
||||||
const dateRange = useSelector(getAdminConsoleUserManagementTableProperties).dateRange ?? ReportDuration.AllTime;
|
const tableFilterProps = useSelector(getAdminConsoleUserManagementTableProperties);
|
||||||
|
const dateRange = tableFilterProps.dateRange ?? ReportDuration.AllTime;
|
||||||
|
|
||||||
const title = (
|
const title = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -55,6 +56,21 @@ export function ExportUserDataModal({onConfirm, onExited}: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tableFiltersAreSet = tableFilterProps.filterRole !== '' || tableFilterProps.filterStatus || tableFilterProps.filterTeam !== '';
|
||||||
|
if (tableFiltersAreSet) {
|
||||||
|
message = (
|
||||||
|
<>
|
||||||
|
{message}
|
||||||
|
<p className='mt-3 text-muted'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='export_user_data_modal.export_data.table_filters_note'
|
||||||
|
defaultMessage={'Note: The exported data will use the filters you have set in the users list. To export all data first remove the filters.'}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const exportDataButton = (
|
const exportDataButton = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='export_user_data_modal.export_data'
|
id='export_user_data_modal.export_data'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
import type {ReportDuration} from '@mattermost/types/reports';
|
import {ReportDuration} from '@mattermost/types/reports';
|
||||||
import type {GlobalState} from '@mattermost/types/store';
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ import {get} from 'mattermost-redux/selectors/entities/preferences';
|
|||||||
|
|
||||||
import {startUsersBatchExport} from 'actions/views/admin';
|
import {startUsersBatchExport} from 'actions/views/admin';
|
||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
|
import {getAdminConsoleUserManagementTableProperties} from 'selectors/views/admin';
|
||||||
|
|
||||||
import WithTooltip from 'components/with_tooltip';
|
import WithTooltip from 'components/with_tooltip';
|
||||||
|
|
||||||
@@ -25,11 +26,12 @@ import {ExportErrorModal} from './export_error_modal';
|
|||||||
import {ExportUserDataModal} from './export_user_data_modal';
|
import {ExportUserDataModal} from './export_user_data_modal';
|
||||||
import {UpgradeExportDataModal} from './upgrade_export_data_modal';
|
import {UpgradeExportDataModal} from './upgrade_export_data_modal';
|
||||||
|
|
||||||
|
import {convertTableOptionsToUserReportOptions} from '../utils';
|
||||||
import './system_users_export.scss';
|
import './system_users_export.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentUserId: UserProfile['id'];
|
currentUserId: UserProfile['id'];
|
||||||
dateRange: ReportDuration;
|
usersLenght: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SystemUsersExport(props: Props) {
|
export function SystemUsersExport(props: Props) {
|
||||||
@@ -38,12 +40,17 @@ export function SystemUsersExport(props: Props) {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const skipDialog = useSelector((state: GlobalState) => get(state, Preferences.CATEGORY_REPORTING, Preferences.HIDE_BATCH_EXPORT_CONFIRM_MODAL, '')) === 'true';
|
const skipDialog = useSelector((state: GlobalState) => get(state, Preferences.CATEGORY_REPORTING, Preferences.HIDE_BATCH_EXPORT_CONFIRM_MODAL, '')) === 'true';
|
||||||
|
const tableFilterProps = useSelector(getAdminConsoleUserManagementTableProperties);
|
||||||
|
const tableOptionsToUserReport = convertTableOptionsToUserReportOptions(tableFilterProps);
|
||||||
|
if (tableOptionsToUserReport.date_range === undefined) {
|
||||||
|
tableOptionsToUserReport.date_range = ReportDuration.AllTime;
|
||||||
|
}
|
||||||
|
|
||||||
const license = useSelector(getLicense);
|
const license = useSelector(getLicense);
|
||||||
const isLicensed = license.IsLicensed === 'true' && (license.SkuShortName === LicenseSkus.Professional || license.SkuShortName === LicenseSkus.Enterprise);
|
const isLicensed = license.IsLicensed === 'true' && (license.SkuShortName === LicenseSkus.Professional || license.SkuShortName === LicenseSkus.Enterprise);
|
||||||
|
|
||||||
async function doExport(checked?: boolean) {
|
async function doExport(checked?: boolean) {
|
||||||
const {error} = await dispatch(startUsersBatchExport(props.dateRange));
|
const {error} = await dispatch(startUsersBatchExport(tableOptionsToUserReport));
|
||||||
if (error) {
|
if (error) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.EXPORT_ERROR_MODAL,
|
modalId: ModalIdentifiers.EXPORT_ERROR_MODAL,
|
||||||
@@ -64,6 +71,9 @@ export function SystemUsersExport(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleExport() {
|
function handleExport() {
|
||||||
|
if (!props.usersLenght) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.UPGRADE_EXPORT_DATA_MODAL,
|
modalId: ModalIdentifiers.UPGRADE_EXPORT_DATA_MODAL,
|
||||||
@@ -89,6 +99,7 @@ export function SystemUsersExport(props: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
className='btn btn-md btn-tertiary'
|
className='btn btn-md btn-tertiary'
|
||||||
|
disabled={!props.usersLenght}
|
||||||
>
|
>
|
||||||
<span className='icon icon-download-outline'/>
|
<span className='icon icon-download-outline'/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|||||||
@@ -3639,6 +3639,7 @@
|
|||||||
"export_user_data_modal.dange_range.previous_month": "You're about to export user data for the previous month. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.",
|
"export_user_data_modal.dange_range.previous_month": "You're about to export user data for the previous month. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.",
|
||||||
"export_user_data_modal.do_not_show": "Do not show this again",
|
"export_user_data_modal.do_not_show": "Do not show this again",
|
||||||
"export_user_data_modal.export_data": "Export data",
|
"export_user_data_modal.export_data": "Export data",
|
||||||
|
"export_user_data_modal.export_data.table_filters_note": "Note: The exported data will use the filters you have set in the users list. To export all data first remove the filters.",
|
||||||
"export_user_data_modal.title": "Export user data",
|
"export_user_data_modal.title": "Export user data",
|
||||||
"feature_restricted_modal.agreement": "By selecting <highlight>Try free for {trialLength} days</highlight>, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>Privacy Policy</linkPrivacy>, and receiving product emails.",
|
"feature_restricted_modal.agreement": "By selecting <highlight>Try free for {trialLength} days</highlight>, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>Privacy Policy</linkPrivacy>, and receiving product emails.",
|
||||||
"feature_restricted_modal.button.notify": "Notify admin",
|
"feature_restricted_modal.button.notify": "Notify admin",
|
||||||
|
|||||||
@@ -1025,8 +1025,8 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
startUsersBatchExport = (dateRange: string) => {
|
startUsersBatchExport = (filter: UserReportFilter) => {
|
||||||
const queryString = buildQueryString({date_range: dateRange});
|
const queryString = buildQueryString(filter);
|
||||||
return this.doFetch<StatusOK>(
|
return this.doFetch<StatusOK>(
|
||||||
`${this.getReportsRoute()}/users/export${queryString}`,
|
`${this.getReportsRoute()}/users/export${queryString}`,
|
||||||
{method: 'post'},
|
{method: 'post'},
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user