Use case onboarding (#19446)
* Add use case onboarding feature flag * Add endpoints and storage for whether first admin completed setup * Add migration for FirstAdminSetupComplete Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
dde7ce5535
Коммит
459f54ac9d
@@ -68,7 +68,8 @@ func (api *API) InitSystem() {
|
||||
api.BaseRoutes.System.Handle("/notices/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getProductNotices)).Methods("GET")
|
||||
api.BaseRoutes.System.Handle("/notices/view", api.APISessionRequired(updateViewedProductNotices)).Methods("PUT")
|
||||
api.BaseRoutes.System.Handle("/support_packet", api.APISessionRequired(generateSupportPacket)).Methods("GET")
|
||||
api.BaseRoutes.System.Handle("/onboarding/complete", api.APIHandler(completeOnboarding)).Methods("POST")
|
||||
api.BaseRoutes.System.Handle("/onboarding/complete", api.APISessionRequired(getOnboarding)).Methods("GET")
|
||||
api.BaseRoutes.System.Handle("/onboarding/complete", api.APISessionRequired(completeOnboarding)).Methods("POST")
|
||||
}
|
||||
|
||||
func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -881,6 +882,29 @@ func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Reque
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("getOnboarding", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
firstAdminCompleteSetupObj, err := c.App.GetOnboarding()
|
||||
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getOnboarding", "app.system.get_onboarding_request.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if err := json.NewEncoder(w).Encode(firstAdminCompleteSetupObj); err != nil {
|
||||
mlog.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func completeOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.Err = model.NewAppError("completeOnboarding", "app.system.complete_onboarding_request.no_first_user", nil, "", http.StatusForbidden)
|
||||
|
||||
@@ -653,6 +653,7 @@ type AppIface interface {
|
||||
GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError)
|
||||
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError)
|
||||
GetOAuthStateToken(token string) (*model.Token, *model.AppError)
|
||||
GetOnboarding() (*model.System, *model.AppError)
|
||||
GetOpenGraphMetadata(requestURL string) ([]byte, error)
|
||||
GetOrCreateDirectChannel(c *request.Context, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
|
||||
GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError)
|
||||
|
||||
@@ -17,6 +17,7 @@ const GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete"
|
||||
const SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete"
|
||||
const ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete"
|
||||
const PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete"
|
||||
const FirstAdminSetupCompleteKey = "FirstAdminSetupComplete"
|
||||
|
||||
// This function migrates the default built in roles from code/config to the database.
|
||||
func (a *App) DoAdvancedPermissionsMigration() {
|
||||
@@ -434,6 +435,50 @@ func (s *Server) doPlaybooksRolesCreationMigration() {
|
||||
|
||||
}
|
||||
|
||||
// arbitrary choice, though if there is an longstanding installation with less than 10 messages,
|
||||
// putting the first admin through onboarding shouldn't be very disruptive.
|
||||
const existingInstallationPostsThreshold = 10
|
||||
|
||||
func (s *Server) doFirstAdminSetupCompleteMigration() {
|
||||
// Don't run the migration until the flag is turned on.
|
||||
|
||||
if !s.Config().FeatureFlags.UseCaseOnboarding {
|
||||
return
|
||||
}
|
||||
|
||||
// If the migration is already marked as completed, don't do it again.
|
||||
if _, err := s.Store.System().GetByName(FirstAdminSetupCompleteKey); err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := s.Store.Team().GetAll()
|
||||
if err != nil {
|
||||
// can not confirm that admin has started in this case.
|
||||
return
|
||||
}
|
||||
|
||||
if len(teams) == 0 {
|
||||
// No teams, and no existing preference. This is most likely a new instance.
|
||||
// So do not mark that the admin has already done the first time setup.
|
||||
return
|
||||
}
|
||||
|
||||
// if there are teams, then if this isn't a new installation, there should be posts
|
||||
postCount, err := s.Store.Post().AnalyticsPostCount("", false, false)
|
||||
if err != nil || postCount < existingInstallationPostsThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
system := model.System{
|
||||
Name: FirstAdminSetupCompleteKey,
|
||||
Value: "true",
|
||||
}
|
||||
|
||||
if err := s.Store.System().Save(&system); err != nil {
|
||||
mlog.Critical("Failed to mark first admin setup migration as completed.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) DoAppMigrations() {
|
||||
a.Srv().doAppMigrations()
|
||||
}
|
||||
@@ -451,4 +496,5 @@ func (s *Server) doAppMigrations() {
|
||||
}
|
||||
s.doContentExtractionConfigDefaultTrueMigration()
|
||||
s.doPlaybooksRolesCreationMigration()
|
||||
s.doFirstAdminSetupCompleteMigration()
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
|
||||
@@ -56,7 +59,33 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo
|
||||
}(pluginID)
|
||||
}
|
||||
|
||||
firstAdminCompleteSetupObj := model.System{
|
||||
Name: model.SystemFirstAdminCompleteSetup,
|
||||
Value: "true",
|
||||
}
|
||||
|
||||
if err := a.Srv().Store.System().SaveOrUpdate(&firstAdminCompleteSetupObj); err != nil {
|
||||
return model.NewAppError("setFirstAdminCompleteSetup", "api.error_set_first_admin_complete_setup", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetOnboarding() (*model.System, *model.AppError) {
|
||||
firstAdminCompleteSetupObj, err := a.Srv().Store.System().GetByName(model.SystemFirstAdminCompleteSetup)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return &model.System{
|
||||
Name: model.SystemFirstAdminCompleteSetup,
|
||||
Value: "false",
|
||||
}, nil
|
||||
default:
|
||||
return nil, model.NewAppError("getFirstAdminCompleteSetup", "api.error_get_first_admin_complete_setup", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
return firstAdminCompleteSetupObj, nil
|
||||
}
|
||||
|
||||
@@ -7121,6 +7121,28 @@ func (a *OpenTracingAppLayer) GetOAuthStateToken(token string) (*model.Token, *m
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetOnboarding() (*model.System, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOnboarding")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetOnboarding()
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetOpenGraphMetadata(requestURL string) ([]byte, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOpenGraphMetadata")
|
||||
|
||||
12
i18n/en.json
12
i18n/en.json
@@ -1681,10 +1681,18 @@
|
||||
"id": "api.emoji.upload.open.app_error",
|
||||
"translation": "Unable to create the emoji. An error occurred when trying to open the attached image."
|
||||
},
|
||||
{
|
||||
"id": "api.error_get_first_admin_complete_setup",
|
||||
"translation": "Error trying to retrieve first admin complete setup from the store."
|
||||
},
|
||||
{
|
||||
"id": "api.error_get_first_admin_visit_marketplace_status",
|
||||
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
||||
},
|
||||
{
|
||||
"id": "api.error_set_first_admin_complete_setup",
|
||||
"translation": "Error trying to save first admin complete setup in the store."
|
||||
},
|
||||
{
|
||||
"id": "api.error_set_first_admin_visit_marketplace_status",
|
||||
"translation": "Error trying to save the first admin visit marketplace status in the store."
|
||||
@@ -6091,6 +6099,10 @@
|
||||
"id": "app.system.get_by_name.app_error",
|
||||
"translation": "Unable to find the system variable."
|
||||
},
|
||||
{
|
||||
"id": "app.system.get_onboarding_request.app_error",
|
||||
"translation": "Failed to get onboarding completion status."
|
||||
},
|
||||
{
|
||||
"id": "app.system.permanent_delete_by_name.app_error",
|
||||
"translation": "We could not permanently delete the system table entry."
|
||||
|
||||
@@ -67,6 +67,9 @@ type FeatureFlags struct {
|
||||
|
||||
NormalizeLdapDNs bool
|
||||
|
||||
// Enable special onboarding flow for first admin
|
||||
UseCaseOnboarding bool
|
||||
|
||||
// Enable Workspace optimization dashboard
|
||||
WorkspaceOptimizationDashboard bool
|
||||
|
||||
@@ -95,6 +98,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.InlinePostEditing = false
|
||||
f.BoardsDataRetention = false
|
||||
f.NormalizeLdapDNs = false
|
||||
f.UseCaseOnboarding = false
|
||||
f.WorkspaceOptimizationDashboard = false
|
||||
f.GraphQL = false
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
SystemWarnMetricLastRunTimestampKey = "LastWarnMetricRunTimestamp"
|
||||
SystemMetricSupportEmailNotConfigured = "warn_metric_support_email_not_configured"
|
||||
SystemFirstAdminVisitMarketplace = "FirstAdminVisitMarketplace"
|
||||
SystemFirstAdminCompleteSetup = "FirstAdminCompleteSetup"
|
||||
AwsMeteringReportInterval = 1
|
||||
AwsMeteringDimensionUsageHrs = "UsageHrs"
|
||||
UserLimitOverageCycleEndDate = "UserLimitOverageCycleEndDate"
|
||||
|
||||
Ссылка в новой задаче
Block a user