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/{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("/notices/view", api.APISessionRequired(updateViewedProductNotices)).Methods("PUT")
|
||||||
api.BaseRoutes.System.Handle("/support_packet", api.APISessionRequired(generateSupportPacket)).Methods("GET")
|
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) {
|
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)
|
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) {
|
func completeOnboarding(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
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)
|
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)
|
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)
|
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError)
|
||||||
GetOAuthStateToken(token string) (*model.Token, *model.AppError)
|
GetOAuthStateToken(token string) (*model.Token, *model.AppError)
|
||||||
|
GetOnboarding() (*model.System, *model.AppError)
|
||||||
GetOpenGraphMetadata(requestURL string) ([]byte, error)
|
GetOpenGraphMetadata(requestURL string) ([]byte, error)
|
||||||
GetOrCreateDirectChannel(c *request.Context, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
|
GetOrCreateDirectChannel(c *request.Context, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
|
||||||
GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError)
|
GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete"
|
|||||||
const SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete"
|
const SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete"
|
||||||
const ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete"
|
const ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete"
|
||||||
const PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete"
|
const PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete"
|
||||||
|
const FirstAdminSetupCompleteKey = "FirstAdminSetupComplete"
|
||||||
|
|
||||||
// This function migrates the default built in roles from code/config to the database.
|
// This function migrates the default built in roles from code/config to the database.
|
||||||
func (a *App) DoAdvancedPermissionsMigration() {
|
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() {
|
func (a *App) DoAppMigrations() {
|
||||||
a.Srv().doAppMigrations()
|
a.Srv().doAppMigrations()
|
||||||
}
|
}
|
||||||
@@ -451,4 +496,5 @@ func (s *Server) doAppMigrations() {
|
|||||||
}
|
}
|
||||||
s.doContentExtractionConfigDefaultTrueMigration()
|
s.doContentExtractionConfigDefaultTrueMigration()
|
||||||
s.doPlaybooksRolesCreationMigration()
|
s.doPlaybooksRolesCreationMigration()
|
||||||
|
s.doFirstAdminSetupCompleteMigration()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
"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 {
|
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)
|
}(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()
|
wg.Wait()
|
||||||
|
|
||||||
return nil
|
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
|
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) {
|
func (a *OpenTracingAppLayer) GetOpenGraphMetadata(requestURL string) ([]byte, error) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOpenGraphMetadata")
|
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",
|
"id": "api.emoji.upload.open.app_error",
|
||||||
"translation": "Unable to create the emoji. An error occurred when trying to open the attached image."
|
"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",
|
"id": "api.error_get_first_admin_visit_marketplace_status",
|
||||||
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
"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",
|
"id": "api.error_set_first_admin_visit_marketplace_status",
|
||||||
"translation": "Error trying to save the first admin visit marketplace status in the store."
|
"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",
|
"id": "app.system.get_by_name.app_error",
|
||||||
"translation": "Unable to find the system variable."
|
"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",
|
"id": "app.system.permanent_delete_by_name.app_error",
|
||||||
"translation": "We could not permanently delete the system table entry."
|
"translation": "We could not permanently delete the system table entry."
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ type FeatureFlags struct {
|
|||||||
|
|
||||||
NormalizeLdapDNs bool
|
NormalizeLdapDNs bool
|
||||||
|
|
||||||
|
// Enable special onboarding flow for first admin
|
||||||
|
UseCaseOnboarding bool
|
||||||
|
|
||||||
// Enable Workspace optimization dashboard
|
// Enable Workspace optimization dashboard
|
||||||
WorkspaceOptimizationDashboard bool
|
WorkspaceOptimizationDashboard bool
|
||||||
|
|
||||||
@@ -95,6 +98,7 @@ func (f *FeatureFlags) SetDefaults() {
|
|||||||
f.InlinePostEditing = false
|
f.InlinePostEditing = false
|
||||||
f.BoardsDataRetention = false
|
f.BoardsDataRetention = false
|
||||||
f.NormalizeLdapDNs = false
|
f.NormalizeLdapDNs = false
|
||||||
|
f.UseCaseOnboarding = false
|
||||||
f.WorkspaceOptimizationDashboard = false
|
f.WorkspaceOptimizationDashboard = false
|
||||||
f.GraphQL = false
|
f.GraphQL = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const (
|
|||||||
SystemWarnMetricLastRunTimestampKey = "LastWarnMetricRunTimestamp"
|
SystemWarnMetricLastRunTimestampKey = "LastWarnMetricRunTimestamp"
|
||||||
SystemMetricSupportEmailNotConfigured = "warn_metric_support_email_not_configured"
|
SystemMetricSupportEmailNotConfigured = "warn_metric_support_email_not_configured"
|
||||||
SystemFirstAdminVisitMarketplace = "FirstAdminVisitMarketplace"
|
SystemFirstAdminVisitMarketplace = "FirstAdminVisitMarketplace"
|
||||||
|
SystemFirstAdminCompleteSetup = "FirstAdminCompleteSetup"
|
||||||
AwsMeteringReportInterval = 1
|
AwsMeteringReportInterval = 1
|
||||||
AwsMeteringDimensionUsageHrs = "UsageHrs"
|
AwsMeteringDimensionUsageHrs = "UsageHrs"
|
||||||
UserLimitOverageCycleEndDate = "UserLimitOverageCycleEndDate"
|
UserLimitOverageCycleEndDate = "UserLimitOverageCycleEndDate"
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user