From 3055a5e935eb5e5605a56b1200fd77daab9d05cc Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Thu, 20 Apr 2023 13:56:37 +0530 Subject: [PATCH] Revert "MM-49603 : Remove fetching of deleted channels on page load (#22981)" This reverts commit 1bbfdefad73d12b44c6b1d14d219696c54c184c8. --- .github/workflows/channels-ci.yml | 10 - CODEOWNERS | 3 - .../support/server/default_config.ts | 1 + .../boards/services/store/sqlstore/migrate.go | 5 +- server/channels/api4/system_test.go | 1 - server/channels/api4/user.go | 20 - server/channels/api4/user_test.go | 110 +-- server/channels/app/channel.go | 3 - server/channels/app/channel_test.go | 79 -- server/channels/app/license_test.go | 2 + server/channels/app/onboarding.go | 18 - server/channels/app/onboarding_test.go | 30 - server/channels/app/platform/license_test.go | 2 + server/channels/db/migrations/migrations.list | 4 - .../000107_threadmemberships_cleanup.down.sql | 1 - .../000107_threadmemberships_cleanup.up.sql | 5 - .../000107_threadmemberships_cleanup.down.sql | 1 - .../000107_threadmemberships_cleanup.up.sql | 12 - server/channels/einterfaces/metrics.go | 1 - .../einterfaces/mocks/MetricsInterface.go | 5 - .../opentracinglayer/opentracinglayer.go | 18 - .../channels/store/retrylayer/retrylayer.go | 21 - .../sqlstore/channel_store_categories.go | 2 +- .../channels/store/sqlstore/sqlx_wrapper.go | 73 +- .../store/sqlstore/sqlx_wrapper_test.go | 11 +- server/channels/store/sqlstore/store.go | 223 ++--- server/channels/store/sqlstore/store_test.go | 18 +- .../channels/store/sqlstore/thread_store.go | 40 +- server/channels/store/store.go | 4 +- .../storetest/channel_store_categories.go | 32 - .../channels/store/storetest/mocks/Store.go | 16 + .../store/storetest/mocks/ThreadStore.go | 14 - server/channels/store/storetest/settings.go | 1 - .../channels/store/storetest/thread_store.go | 119 --- .../channels/store/timerlayer/timerlayer.go | 16 - server/channels/testlib/helper.go | 2 +- server/channels/utils/license.go | 1 + server/i18n/en.json | 4 - server/model/config.go | 5 - server/model/onboarding.go | 1 - server/model/system.go | 1 - .../platform/services/telemetry/telemetry.go | 1 - .../channels/src/actions/global_actions.tsx | 10 +- .../do_verify_email/do_verify_email.tsx | 18 +- .../user_guide_dropdown/index.ts | 2 + .../user_guide_dropdown.test.tsx | 1 + .../src/components/login/login.test.tsx | 14 - .../channels/src/components/login/login.tsx | 17 +- .../invite_members.test.tsx.snap | 80 -- .../invite_members_link.test.tsx.snap | 29 - .../organization_status.test.tsx.snap | 7 - .../components/preparing_workspace/index.tsx | 3 +- .../preparing_workspace/invite_members.scss | 51 -- .../invite_members.test.tsx | 71 -- .../preparing_workspace/invite_members.tsx | 114 --- .../invite_members_illustration.tsx | 838 ------------------ .../invite_members_link.scss | 51 -- .../invite_members_link.test.tsx | 61 -- .../invite_members_link.tsx | 64 -- .../preparing_workspace/mixins.scss | 12 - .../preparing_workspace/organization.scss | 63 -- .../preparing_workspace/organization.tsx | 206 ----- .../organization_status.test.tsx | 46 - .../organization_status.tsx | 83 -- .../preparing_workspace/page_line.scss | 10 - .../preparing_workspace/page_line.tsx | 35 - .../preparing_workspace/plugins.scss | 3 - .../preparing_workspace/plugins.tsx | 82 +- .../preparing_workspace.scss | 52 -- .../preparing_workspace.tsx | 188 +--- .../single_column_layout.scss | 1 + .../components/preparing_workspace/steps.ts | 27 +- webapp/channels/src/components/root/root.tsx | 8 +- .../components/root/root_redirect/index.ts | 7 +- .../src/components/signup/signup.test.tsx | 11 + .../channels/src/components/signup/signup.tsx | 18 +- .../src/components/terms_of_service/index.ts | 3 + .../terms_of_service.test.tsx | 1 + .../terms_of_service/terms_of_service.tsx | 5 +- .../work_templates/components/customize.tsx | 3 +- .../work_templates/components/preview.tsx | 13 +- .../components/preview/accordion.tsx | 3 +- .../components/preview/section.tsx | 61 +- webapp/channels/src/i18n/en.json | 28 +- .../mattermost-redux/src/actions/channels.ts | 51 +- .../src/selectors/entities/preferences.ts | 4 + webapp/channels/src/utils/constants.tsx | 1 - webapp/platform/types/src/setup.ts | 1 - 88 files changed, 327 insertions(+), 3066 deletions(-) delete mode 100644 server/channels/app/onboarding_test.go delete mode 100644 server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql delete mode 100644 server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql delete mode 100644 server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql delete mode 100644 server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql delete mode 100644 webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap delete mode 100644 webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap delete mode 100644 webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members.scss delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members.test.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_link.scss delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_link.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/mixins.scss delete mode 100644 webapp/channels/src/components/preparing_workspace/organization.scss delete mode 100644 webapp/channels/src/components/preparing_workspace/organization.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/organization_status.test.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/organization_status.tsx delete mode 100644 webapp/channels/src/components/preparing_workspace/page_line.scss delete mode 100644 webapp/channels/src/components/preparing_workspace/page_line.tsx diff --git a/.github/workflows/channels-ci.yml b/.github/workflows/channels-ci.yml index 7d71862ba7..d6a1a6258c 100644 --- a/.github/workflows/channels-ci.yml +++ b/.github/workflows/channels-ci.yml @@ -83,16 +83,6 @@ jobs: npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir rm -rf tmp - - name: ci/lint-boards - working-directory: webapp/boards - run: | - npm run i18n-extract - git --no-pager diff --exit-code i18n/en.json || (echo "Please run \"cd webapp/boards && npm run i18n-extract\" and commit the changes in webapp/boards/i18n/en.json." && exit 1) - - name: ci/lint-playbooks - working-directory: webapp/playbooks - run: | - npm run i18n-extract - git --no-pager diff --exit-code i18n/en.json || (echo "Please run \"cd webapp/playbooks && npm run i18n-extract\" and commit the changes in webapp/playbooks/i18n/en.json." && exit 1) check-types: runs-on: ubuntu-22.04 defaults: diff --git a/CODEOWNERS b/CODEOWNERS index 4ef5c97c43..b0bd218122 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,6 +5,3 @@ /webapp/package-lock.json @mattermost/web-platform /webapp/platform/*/package.json @mattermost/web-platform /webapp/scripts @mattermost/web-platform -/server/channels/db/migrations @mattermost/server-platform -/server/boards/services/store/sqlstore/migrations @mattermost/server-platform -/server/playbooks/server/sqlstore/migrations @mattermost/server-platform diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts index 3e8e5ed881..2d8cb12c58 100644 --- a/e2e-tests/playwright/support/server/default_config.ts +++ b/e2e-tests/playwright/support/server/default_config.ts @@ -665,6 +665,7 @@ const defaultServerConfig: AdminConfig = { BoardsFeatureFlags: '', BoardsDataRetention: false, NormalizeLdapDNs: false, + UseCaseOnboarding: true, GraphQL: false, InsightsEnabled: true, CommandPalette: false, diff --git a/server/boards/services/store/sqlstore/migrate.go b/server/boards/services/store/sqlstore/migrate.go index 63b4665490..1c876a5168 100644 --- a/server/boards/services/store/sqlstore/migrate.go +++ b/server/boards/services/store/sqlstore/migrate.go @@ -70,10 +70,7 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) { } *settings.DriverName = s.dbType - db, err := sqlstore.SetupConnection("master", connectionString, &settings, sqlstore.DBPingAttempts) - if err != nil { - return nil, err - } + db := sqlstore.SetupConnection("master", connectionString, &settings) return db, nil } diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index 25574e4400..5921e32802 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -892,7 +892,6 @@ func TestCompleteOnboarding(t *testing.T) { req := &model.CompleteOnboardingRequest{ InstallPlugins: []string{"testplugin2"}, - Organization: "my-org", } t.Run("as a regular user", func(t *testing.T) { diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index 2269efe88c..c259922e82 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -3106,10 +3106,6 @@ func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionEditOtherUsers) return } - if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) { - c.SetPermissionError(model.PermissionReadChannel) - return - } extendedStr := r.URL.Query().Get("extended") extended, _ := strconv.ParseBool(extendedStr) @@ -3140,10 +3136,6 @@ func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionEditOtherUsers) return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) { - c.SetPermissionError(model.PermissionViewTeam) - return - } options := model.GetUserThreadsOpts{ Since: 0, @@ -3221,10 +3213,6 @@ func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Requ c.SetPermissionError(model.PermissionEditOtherUsers) return } - if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) { - c.SetPermissionError(model.PermissionReadChannel) - return - } thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp) if err != nil { @@ -3291,10 +3279,6 @@ func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionEditOtherUsers) return } - if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) { - c.SetPermissionError(model.PermissionReadChannel) - return - } err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false) if err != nil { @@ -3354,10 +3338,6 @@ func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http. c.SetPermissionError(model.PermissionEditOtherUsers) return } - if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) { - c.SetPermissionError(model.PermissionViewTeam) - return - } err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId) if err != nil { diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index 0f9ce87d33..8d9673ed9e 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -6360,15 +6360,6 @@ func TestGetThreadsForUser(t *testing.T) { require.NoError(t, err) require.Equal(t, uss.TotalUnreadThreads, int64(2)) }) - - t.Run("should error when not a team member", func(t *testing.T) { - th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) - defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam) - - _, resp, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{}) - require.Error(t, err) - CheckForbiddenStatus(t, resp) - }) } func TestThreadSocketEvents(t *testing.T) { @@ -6864,64 +6855,52 @@ func TestSingleThreadGet(t *testing.T) { }) client := th.Client + defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id) + defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id) - t.Run("get single thread", func(t *testing.T) { - defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id) - defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id) + // create a post by regular user + rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"}) + // reply with another + postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id}) - // create a post by regular user - rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"}) - // reply with another - postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id}) - - // create another thread to check that we are not returning it by mistake - rpost2, _ := postAndCheck(t, client, &model.Post{ - ChannelId: th.BasicChannel2.Id, - Message: "testMsg2", - Metadata: &model.PostMetadata{ - Priority: &model.PostPriority{ - Priority: model.NewString(model.PostPriorityUrgent), - }, + // create another thread to check that we are not returning it by mistake + rpost2, _ := postAndCheck(t, client, &model.Post{ + ChannelId: th.BasicChannel2.Id, + Message: "testMsg2", + Metadata: &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(model.PostPriorityUrgent), }, - }) - postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id}) + }, + }) + postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id}) - // regular user should have two threads with 3 replies total - threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil) + // regular user should have two threads with 3 replies total + threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil) - tr, _, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false) - require.NoError(t, err) - require.NotNil(t, tr) - require.Equal(t, threads.Threads[0].PostId, tr.PostId) - require.Empty(t, tr.Participants[0].Username) + tr, _, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false) + require.NoError(t, err) + require.NotNil(t, tr) + require.Equal(t, threads.Threads[0].PostId, tr.PostId) + require.Empty(t, tr.Participants[0].Username) - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.PostPriority = false - }) - - tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) - require.NoError(t, err) - require.NotEmpty(t, tr.Participants[0].Username) - require.Equal(t, false, tr.IsUrgent) - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.PostPriority = true - cfg.FeatureFlags.PostPriority = true - }) - - tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) - require.NoError(t, err) - require.Equal(t, true, tr.IsUrgent) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.PostPriority = false }) - t.Run("should error when not a team member", func(t *testing.T) { - th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) - defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam) + tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) + require.NoError(t, err) + require.NotEmpty(t, tr.Participants[0].Username) + require.Equal(t, false, tr.IsUrgent) - _, resp, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), false) - require.Error(t, err) - CheckForbiddenStatus(t, resp) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.PostPriority = true + cfg.FeatureFlags.PostPriority = true }) + + tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) + require.NoError(t, err) + require.Equal(t, true, tr.IsUrgent) } func TestMaintainUnreadMentionsInThread(t *testing.T) { @@ -7093,23 +7072,6 @@ func TestReadThreads(t *testing.T) { checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil) }) - - t.Run("should error when not a team member", func(t *testing.T) { - th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam) - defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam) - - _, resp, err := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.GetMillis()) - require.Error(t, err) - CheckForbiddenStatus(t, resp) - - _, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.NewId()) - require.Error(t, err) - CheckForbiddenStatus(t, resp) - - resp, err = th.Client.UpdateThreadsReadForUser(th.BasicUser.Id, th.BasicTeam.Id) - require.Error(t, err) - CheckForbiddenStatus(t, resp) - }) } func TestMarkThreadUnreadMentionCount(t *testing.T) { diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index 162254b84b..09fb2ce1cc 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -2518,9 +2518,6 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil { return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } - if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userIDToRemove, channel.Id); err != nil { - return model.NewAppError("removeUserFromChannel", model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err) - } if isGuest { currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove) diff --git a/server/channels/app/channel_test.go b/server/channels/app/channel_test.go index 2b427ba806..eaf1171a6c 100644 --- a/server/channels/app/channel_test.go +++ b/server/channels/app/channel_test.go @@ -609,85 +609,6 @@ func TestLeaveDefaultChannel(t *testing.T) { _, err = th.App.GetChannelMember(th.Context, townSquare.Id, guest.Id) assert.NotNil(t, err) }) - - t.Run("Trying to leave the default channel should not delete thread memberships", func(t *testing.T) { - post := &model.Post{ - ChannelId: townSquare.Id, - Message: "root post", - UserId: th.BasicUser.Id, - } - rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true) - require.Nil(t, err) - - reply := &model.Post{ - ChannelId: townSquare.Id, - Message: "reply post", - UserId: th.BasicUser.Id, - RootId: rpost.Id, - } - _, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true) - require.Nil(t, err) - - threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{}) - require.Nil(t, err) - require.Len(t, threads.Threads, 1) - - err = th.App.LeaveChannel(th.Context, townSquare.Id, th.BasicUser.Id) - assert.NotNil(t, err, "It should fail to remove a regular user from the default channel") - assert.Equal(t, err.Id, "api.channel.remove.default.app_error") - - threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{}) - require.Nil(t, err) - require.Len(t, threads.Threads, 1) - }) -} - -func TestLeaveChannel(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - createThread := func(channel *model.Channel) (rpost *model.Post) { - t.Helper() - post := &model.Post{ - ChannelId: channel.Id, - Message: "root post", - UserId: th.BasicUser.Id, - } - - rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true) - require.Nil(t, err) - - reply := &model.Post{ - ChannelId: channel.Id, - Message: "reply post", - UserId: th.BasicUser.Id, - RootId: rpost.Id, - } - _, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true) - require.Nil(t, err) - - return rpost - } - - t.Run("thread memberships are deleted", func(t *testing.T) { - createThread(th.BasicChannel) - channel2 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen) - createThread(channel2) - - threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{}) - require.Nil(t, err) - require.Len(t, threads.Threads, 2) - - err = th.App.LeaveChannel(th.Context, th.BasicChannel.Id, th.BasicUser.Id) - require.Nil(t, err) - - _, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id) - require.NotNil(t, err, "It should remove channel membership") - - threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{}) - require.Nil(t, err) - require.Len(t, threads.Threads, 1) - }) } func TestLeaveLastChannel(t *testing.T) { diff --git a/server/channels/app/license_test.go b/server/channels/app/license_test.go index ea6bbcf7eb..7b32ee52e6 100644 --- a/server/channels/app/license_test.go +++ b/server/channels/app/license_test.go @@ -71,6 +71,8 @@ func TestGetSanitizedClientLicense(t *testing.T) { assert.False(t, ok) _, ok = m["SkuName"] assert.False(t, ok) + _, ok = m["SkuShortName"] + assert.False(t, ok) } func TestGenerateRenewalToken(t *testing.T) { diff --git a/server/channels/app/onboarding.go b/server/channels/app/onboarding.go index 3b76aefe53..2dd85749d9 100644 --- a/server/channels/app/onboarding.go +++ b/server/channels/app/onboarding.go @@ -28,24 +28,6 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError { } func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError { - isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud - - if !isCloud && request.Organization == "" { - mlog.Error("No organization name provided for self hosted onboarding") - return model.NewAppError("CompleteOnboarding", "api.error_no_organization_name_provided_for_self_hosted_onboarding", nil, "", http.StatusBadRequest) - } - - if request.Organization != "" { - err := a.Srv().Store().System().SaveOrUpdate(&model.System{ - Name: model.SystemOrganizationName, - Value: request.Organization, - }) - if err != nil { - // don't block onboarding because of that. - a.Log().Error("failed to save organization name", mlog.Err(err)) - } - } - pluginsEnvironment := a.Channels().GetPluginsEnvironment() if pluginsEnvironment == nil { return a.markAdminOnboardingComplete(c) diff --git a/server/channels/app/onboarding_test.go b/server/channels/app/onboarding_test.go deleted file mode 100644 index cf8462cf28..0000000000 --- a/server/channels/app/onboarding_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/server/v8/channels/app/request" - mm_model "github.com/mattermost/mattermost-server/server/v8/model" -) - -func TestOnboardingSavesOrganizationName(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{ - Organization: "Mattermost In Tests", - }) - require.Nil(t, err) - defer func() { - th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemOrganizationName) - }() - - sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemOrganizationName) - require.NoError(t, storeErr) - require.Equal(t, "Mattermost In Tests", sys.Value) -} diff --git a/server/channels/app/platform/license_test.go b/server/channels/app/platform/license_test.go index 258c2fbe38..6682348130 100644 --- a/server/channels/app/platform/license_test.go +++ b/server/channels/app/platform/license_test.go @@ -71,6 +71,8 @@ func TestGetSanitizedClientLicense(t *testing.T) { assert.False(t, ok) _, ok = m["SkuName"] assert.False(t, ok) + _, ok = m["SkuShortName"] + assert.False(t, ok) } func TestGenerateRenewalToken(t *testing.T) { diff --git a/server/channels/db/migrations/migrations.list b/server/channels/db/migrations/migrations.list index 47f5bf333b..6a7d33d5c6 100644 --- a/server/channels/db/migrations/migrations.list +++ b/server/channels/db/migrations/migrations.list @@ -212,8 +212,6 @@ channels/db/migrations/mysql/000105_remove_tokens.down.sql channels/db/migrations/mysql/000105_remove_tokens.up.sql channels/db/migrations/mysql/000106_fileinfo_channelid.down.sql channels/db/migrations/mysql/000106_fileinfo_channelid.up.sql -channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql -channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql channels/db/migrations/postgres/000001_create_teams.down.sql channels/db/migrations/postgres/000001_create_teams.up.sql channels/db/migrations/postgres/000002_create_team_members.down.sql @@ -426,5 +424,3 @@ channels/db/migrations/postgres/000105_remove_tokens.down.sql channels/db/migrations/postgres/000105_remove_tokens.up.sql channels/db/migrations/postgres/000106_fileinfo_channelid.down.sql channels/db/migrations/postgres/000106_fileinfo_channelid.up.sql -channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql -channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql diff --git a/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql b/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql deleted file mode 100644 index 4743bd6462..0000000000 --- a/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql +++ /dev/null @@ -1 +0,0 @@ --- Skipping it because the forward migrations are destructive diff --git a/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql b/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql deleted file mode 100644 index 90644be3f3..0000000000 --- a/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - tm USING ThreadMemberships AS tm - JOIN Threads ON Threads.PostId = tm.PostId -WHERE - (tm.UserId, Threads.ChannelId) NOT IN (SELECT UserId, ChannelId FROM ChannelMembers); diff --git a/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql b/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql deleted file mode 100644 index 4743bd6462..0000000000 --- a/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql +++ /dev/null @@ -1 +0,0 @@ --- Skipping it because the forward migrations are destructive diff --git a/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql b/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql deleted file mode 100644 index 0ec82905bc..0000000000 --- a/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -DELETE FROM threadmemberships WHERE (postid, userid) IN ( - SELECT - threadmemberships.postid, - threadmemberships.userid - FROM - threadmemberships - JOIN threads ON threads.postid = threadmemberships.postid - LEFT JOIN channelmembers ON channelmembers.userid = threadmemberships.userid - AND threads.channelid = channelmembers.channelid - WHERE - channelmembers.channelid IS NULL -); diff --git a/server/channels/einterfaces/metrics.go b/server/channels/einterfaces/metrics.go index c44af2a3c5..06f44f7b66 100644 --- a/server/channels/einterfaces/metrics.go +++ b/server/channels/einterfaces/metrics.go @@ -13,7 +13,6 @@ import ( type MetricsInterface interface { Register() RegisterDBCollector(db *sql.DB, name string) - UnregisterDBCollector(db *sql.DB, name string) IncrementPostCreate() IncrementWebhookPost() diff --git a/server/channels/einterfaces/mocks/MetricsInterface.go b/server/channels/einterfaces/mocks/MetricsInterface.go index 06f568546a..0d6f799ee5 100644 --- a/server/channels/einterfaces/mocks/MetricsInterface.go +++ b/server/channels/einterfaces/mocks/MetricsInterface.go @@ -319,11 +319,6 @@ func (_m *MetricsInterface) SetReplicaLagTime(node string, value float64) { _m.Called(node, value) } -// UnregisterDBCollector provides a mock function with given fields: db, name -func (_m *MetricsInterface) UnregisterDBCollector(db *sql.DB, name string) { - _m.Called(db, name) -} - type mockConstructorTestingTNewMetricsInterface interface { mock.TestingT Cleanup(func()) diff --git a/server/channels/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 66a5051260..941704a2f4 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -10123,24 +10123,6 @@ func (s *OpenTracingLayerThreadStore) DeleteMembershipForUser(userId string, pos return err } -func (s *OpenTracingLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error { - origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteMembershipsForChannel") - s.Root.Store.SetContext(newCtx) - defer func() { - s.Root.Store.SetContext(origCtx) - }() - - defer span.Finish() - err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID) - if err != nil { - span.LogFields(spanlog.Error(err)) - ext.Error.Set(span, true) - } - - return err -} - func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows") diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index b39c79ab9b..91a3209c44 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -11563,27 +11563,6 @@ func (s *RetryLayerThreadStore) DeleteMembershipForUser(userId string, postID st } -func (s *RetryLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error { - - tries := 0 - for { - err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID) - if err == nil { - return nil - } - if !isRepeatableError(err) { - return err - } - tries++ - if tries >= 3 { - err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return err - } - timepkg.Sleep(100 * timepkg.Millisecond) - } - -} - func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) { tries := 0 diff --git a/server/channels/store/sqlstore/channel_store_categories.go b/server/channels/store/sqlstore/channel_store_categories.go index 4eca0d5de0..3f2f726a0f 100644 --- a/server/channels/store/sqlstore/channel_store_categories.go +++ b/server/channels/store/sqlstore/channel_store_categories.go @@ -335,7 +335,7 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor Id: newCategoryId, UserId: userId, TeamId: teamId, - Sorting: newCategory.Sorting, + Sorting: model.SidebarCategorySortDefault, SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list Type: model.SidebarCategoryCustom, Muted: newCategory.Muted, diff --git a/server/channels/store/sqlstore/sqlx_wrapper.go b/server/channels/store/sqlstore/sqlx_wrapper.go index e8d771cada..0dab579512 100644 --- a/server/channels/store/sqlstore/sqlx_wrapper.go +++ b/server/channels/store/sqlstore/sqlx_wrapper.go @@ -6,12 +6,9 @@ package sqlstore import ( "context" "database/sql" - "errors" - "net" "regexp" "strconv" "strings" - "sync/atomic" "time" "unicode" @@ -69,18 +66,14 @@ type sqlxDBWrapper struct { *sqlx.DB queryTimeout time.Duration trace bool - isOnline *atomic.Bool } func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper { - w := &sqlxDBWrapper{ + return &sqlxDBWrapper{ DB: db, queryTimeout: timeout, trace: trace, - isOnline: &atomic.Bool{}, } - w.isOnline.Store(true) - return w } func (w *sqlxDBWrapper) Stats() sql.DBStats { @@ -90,19 +83,19 @@ func (w *sqlxDBWrapper) Stats() sql.DBStats { func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) { tx, err := w.DB.Beginx() if err != nil { - return nil, w.checkErr(err) + return nil, err } - return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil + return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil } func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) { tx, err := w.DB.BeginTxx(context.Background(), opts) if err != nil { - return nil, w.checkErr(err) + return nil, err } - return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil + return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil } func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error { @@ -116,7 +109,7 @@ func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error { }(time.Now()) } - return w.checkErr(w.DB.GetContext(ctx, dest, query, args...)) + return w.DB.GetContext(ctx, dest, query, args...) } func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error { @@ -141,7 +134,7 @@ func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) { }(time.Now()) } - return w.checkErrWithResult(w.DB.NamedExecContext(ctx, query, arg)) + return w.DB.NamedExecContext(ctx, query, arg) } func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) { @@ -168,7 +161,7 @@ func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er }(time.Now()) } - return w.checkErrWithResult(w.DB.ExecContext(context.Background(), query, args...)) + return w.DB.ExecContext(context.Background(), query, args...) } // ExecRaw is like Exec but without any rebinding of params. You need to pass @@ -183,7 +176,7 @@ func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) { }(time.Now()) } - return w.checkErrWithResult(w.DB.ExecContext(ctx, query, args...)) + return w.DB.ExecContext(ctx, query, args...) } func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { @@ -199,7 +192,7 @@ func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { }(time.Now()) } - return w.checkErrWithRows(w.DB.NamedQueryContext(ctx, query, arg)) + return w.DB.NamedQueryContext(ctx, query, arg) } func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row { @@ -227,7 +220,7 @@ func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) { }(time.Now()) } - return w.checkErrWithRows(w.DB.QueryxContext(ctx, query, args)) + return w.DB.QueryxContext(ctx, query, args) } func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error { @@ -245,7 +238,7 @@ func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, a }(time.Now()) } - return w.checkErr(w.DB.SelectContext(ctx, dest, query, args...)) + return w.DB.SelectContext(ctx, dest, query, args...) } func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error { @@ -261,15 +254,13 @@ type sqlxTxWrapper struct { *sqlx.Tx queryTimeout time.Duration trace bool - dbw *sqlxDBWrapper } -func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool, dbw *sqlxDBWrapper) *sqlxTxWrapper { +func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper { return &sqlxTxWrapper{ Tx: tx, queryTimeout: timeout, trace: trace, - dbw: dbw, } } @@ -284,7 +275,7 @@ func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error { }(time.Now()) } - return w.dbw.checkErr(w.Tx.GetContext(ctx, dest, query, args...)) + return w.Tx.GetContext(ctx, dest, query, args...) } func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error { @@ -293,13 +284,13 @@ func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error { return err } - return w.dbw.checkErr(w.Get(dest, query, args...)) + return w.Get(dest, query, args...) } func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) { query = w.Tx.Rebind(query) - return w.dbw.checkErrWithResult(w.ExecRaw(query, args...)) + return w.ExecRaw(query, args...) } func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) { @@ -311,7 +302,7 @@ func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er }(time.Now()) } - return w.dbw.checkErrWithResult(w.Tx.ExecContext(context.Background(), query, args...)) + return w.Tx.ExecContext(context.Background(), query, args...) } func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) { @@ -335,7 +326,7 @@ func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) { }(time.Now()) } - return w.dbw.checkErrWithResult(w.Tx.ExecContext(ctx, query, args...)) + return w.Tx.ExecContext(ctx, query, args...) } func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) { @@ -351,7 +342,7 @@ func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) { }(time.Now()) } - return w.dbw.checkErrWithResult(w.Tx.NamedExecContext(ctx, query, arg)) + return w.Tx.NamedExecContext(ctx, query, arg) } func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { @@ -395,7 +386,7 @@ func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { } } - return res.rows, w.dbw.checkErr(res.err) + return res.rows, res.err } func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row { @@ -423,7 +414,7 @@ func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) { }(time.Now()) } - return w.dbw.checkErrWithRows(w.Tx.QueryxContext(ctx, query, args)) + return w.Tx.QueryxContext(ctx, query, args) } func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error { @@ -437,7 +428,7 @@ func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error { }(time.Now()) } - return w.dbw.checkErr(w.Tx.SelectContext(ctx, dest, query, args...)) + return w.Tx.SelectContext(ctx, dest, query, args...) } func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error { @@ -468,23 +459,3 @@ func printArgs(query string, dur time.Duration, args ...any) { } mlog.Debug(query, fields...) } - -func (w *sqlxDBWrapper) checkErrWithResult(res sql.Result, err error) (sql.Result, error) { - return res, w.checkErr(err) -} - -func (w *sqlxDBWrapper) checkErrWithRows(res *sqlx.Rows, err error) (*sqlx.Rows, error) { - return res, w.checkErr(err) -} - -func (w *sqlxDBWrapper) checkErr(err error) error { - var netError *net.OpError - if errors.As(err, &netError) && (!netError.Temporary() && !netError.Timeout()) { - w.isOnline.Store(false) - } - return err -} - -func (w *sqlxDBWrapper) Online() bool { - return w.isOnline.Load() -} diff --git a/server/channels/store/sqlstore/sqlx_wrapper_test.go b/server/channels/store/sqlstore/sqlx_wrapper_test.go index c03d228935..07c6391767 100644 --- a/server/channels/store/sqlstore/sqlx_wrapper_test.go +++ b/server/channels/store/sqlstore/sqlx_wrapper_test.go @@ -6,7 +6,6 @@ package sqlstore import ( "context" "strings" - "sync" "testing" "github.com/stretchr/testify/assert" @@ -29,14 +28,12 @@ func TestSqlX(t *testing.T) { } *settings.QueryTimeout = 1 store := &SqlStore{ - rrCounter: 0, - srCounter: 0, - settings: settings, - quitMonitor: make(chan struct{}), - wgMonitor: &sync.WaitGroup{}, + rrCounter: 0, + srCounter: 0, + settings: settings, } - require.NoError(t, store.initConnection()) + store.initConnection() defer store.Close() diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index acd02b0853..d39f92661c 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -49,7 +49,7 @@ const ( MySQLForeignKeyViolationErrorCode = 1452 PGDuplicateObjectErrorCode = "42710" MySQLDuplicateObjectErrorCode = 1022 - DBPingAttempts = 5 + DBPingAttempts = 18 DBPingTimeoutSecs = 10 // This is a numerical version string by postgres. The format is // 2 characters for major, minor, and patch version prior to 10. @@ -123,9 +123,9 @@ type SqlStore struct { masterX *sqlxDBWrapper - ReplicaXs []*atomic.Pointer[sqlxDBWrapper] + ReplicaXs []*sqlxDBWrapper - searchReplicaXs []*atomic.Pointer[sqlxDBWrapper] + searchReplicaXs []*sqlxDBWrapper replicaLagHandles []*dbsql.DB stores SqlStoreStores @@ -138,28 +138,17 @@ type SqlStore struct { isBinaryParam bool pgDefaultTextSearchConfig string - - quitMonitor chan struct{} - wgMonitor *sync.WaitGroup } func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore { store := &SqlStore{ - rrCounter: 0, - srCounter: 0, - settings: &settings, - metrics: metrics, - quitMonitor: make(chan struct{}), - wgMonitor: &sync.WaitGroup{}, + rrCounter: 0, + srCounter: 0, + settings: &settings, + metrics: metrics, } - err := store.initConnection() - if err != nil { - mlog.Fatal("Error setting up connections", mlog.Err(err)) - } - - store.wgMonitor.Add(1) - go store.monitorReplicas() + store.initConnection() ver, err := store.GetDbVersion(true) if err != nil { @@ -241,28 +230,29 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS // SetupConnection sets up the connection to the database and pings it to make sure it's alive. // It also applies any database configuration settings that are required. -func SetupConnection(connType string, dataSource string, settings *model.SqlSettings, attempts int) (*dbsql.DB, error) { +func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB { db, err := dbsql.Open(*settings.DriverName, dataSource) if err != nil { - return nil, errors.Wrap(err, "failed to open SQL connection") + mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err)) } - for i := 0; i < attempts; i++ { + for i := 0; i < DBPingAttempts; i++ { // At this point, we have passed sql.Open, so we deliberately ignore any errors. sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource) mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized)) ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second) defer cancel() err = db.PingContext(ctx) - if err != nil { - if i == attempts-1 { - return nil, err + if err == nil { + break + } else { + if i == DBPingAttempts-1 { + mlog.Fatal("Failed to ping DB, server will exit.", mlog.Err(err)) + } else { + mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs)) + time.Sleep(DBPingTimeoutSecs * time.Second) } - mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs)) - time.Sleep(DBPingTimeoutSecs * time.Second) - continue } - break } if strings.HasPrefix(connType, replicaLagPrefix) { @@ -282,7 +272,7 @@ func SetupConnection(connType string, dataSource string, settings *model.SqlSett db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond) db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond) - return db, nil + return db } func (ss *SqlStore) SetContext(context context.Context) { @@ -295,7 +285,7 @@ func (ss *SqlStore) Context() context.Context { func noOpMapper(s string) string { return s } -func (ss *SqlStore) initConnection() error { +func (ss *SqlStore) initConnection() { dataSource := *ss.settings.DataSource if ss.DriverName() == model.DatabaseDriverMysql { // TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout @@ -304,14 +294,11 @@ func (ss *SqlStore) initConnection() error { var err error dataSource, err = ResetReadTimeout(dataSource) if err != nil { - return errors.Wrap(err, "failed to reset read timeout from datasource") + mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource)) } } - handle, err := SetupConnection("master", dataSource, ss.settings, DBPingAttempts) - if err != nil { - return err - } + handle := SetupConnection("master", dataSource, ss.settings) ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), time.Duration(*ss.settings.QueryTimeout)*time.Second, *ss.settings.Trace) @@ -323,32 +310,34 @@ func (ss *SqlStore) initConnection() error { } if len(ss.settings.DataSourceReplicas) > 0 { - ss.ReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceReplicas)) + ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas)) for i, replica := range ss.settings.DataSourceReplicas { - ss.ReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{} - handle, err = SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings, DBPingAttempts) - if err != nil { - // Initializing to be offline - ss.ReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}}) - mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("replica-%v", i)), mlog.Err(err)) - continue + handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings) + ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), + time.Duration(*ss.settings.QueryTimeout)*time.Second, + *ss.settings.Trace) + if ss.DriverName() == model.DatabaseDriverMysql { + ss.ReplicaXs[i].MapperFunc(noOpMapper) + } + if ss.metrics != nil { + ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i)) } - ss.setDB(ss.ReplicaXs[i], handle, "replica-"+strconv.Itoa(i)) } } if len(ss.settings.DataSourceSearchReplicas) > 0 { - ss.searchReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceSearchReplicas)) + ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas)) for i, replica := range ss.settings.DataSourceSearchReplicas { - ss.searchReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{} - handle, err = SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings, DBPingAttempts) - if err != nil { - // Initializing to be offline - ss.searchReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}}) - mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("search-replica-%v", i)), mlog.Err(err)) - continue + handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings) + ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), + time.Duration(*ss.settings.QueryTimeout)*time.Second, + *ss.settings.Trace) + if ss.DriverName() == model.DatabaseDriverMysql { + ss.searchReplicaXs[i].MapperFunc(noOpMapper) + } + if ss.metrics != nil { + ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i)) } - ss.setDB(ss.searchReplicaXs[i], handle, "searchreplica-"+strconv.Itoa(i)) } } @@ -358,14 +347,9 @@ func (ss *SqlStore) initConnection() error { if src.DataSource == nil { continue } - ss.replicaLagHandles[i], err = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings, DBPingAttempts) - if err != nil { - mlog.Warn("Failed to setup replica lag handle. Skipping..", mlog.String("db", fmt.Sprintf(replicaLagPrefix+"-%d", i)), mlog.Err(err)) - continue - } + ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings) } } - return nil } func (ss *SqlStore) DriverName() string { @@ -471,15 +455,8 @@ func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper { return ss.GetReplicaX() } - for i := 0; i < len(ss.searchReplicaXs); i++ { - rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs)) - if ss.searchReplicaXs[rrNum].Load().Online() { - return ss.searchReplicaXs[rrNum].Load() - } - } - - // If all search replicas are down, then go with replica. - return ss.GetReplicaX() + rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs)) + return ss.searchReplicaXs[rrNum] } func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper { @@ -487,64 +464,23 @@ func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper { return ss.GetMasterX() } - for i := 0; i < len(ss.ReplicaXs); i++ { - rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs)) - if ss.ReplicaXs[rrNum].Load().Online() { - return ss.ReplicaXs[rrNum].Load() + rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs)) + return ss.ReplicaXs[rrNum] +} + +func (ss *SqlStore) GetInternalReplicaDBs() []*sql.DB { + if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() { + return []*sql.DB{ + ss.GetMasterX().DB.DB, } } - // If all replicas are down, then go with master. - return ss.GetMasterX() -} - -func (ss *SqlStore) monitorReplicas() { - t := time.NewTicker(time.Duration(*ss.settings.ReplicaMonitorIntervalSeconds) * time.Second) - defer func() { - t.Stop() - ss.wgMonitor.Done() - }() - for { - select { - case <-ss.quitMonitor: - return - case <-t.C: - setupReplica := func(r *atomic.Pointer[sqlxDBWrapper], dsn, name string) { - if r.Load().Online() { - return - } - - handle, err := SetupConnection(name, dsn, ss.settings, 1) - if err != nil { - mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", name), mlog.Err(err)) - return - } - if ss.metrics != nil && r.Load() != nil && r.Load().DB != nil { - ss.metrics.UnregisterDBCollector(r.Load().DB.DB, name) - } - ss.setDB(r, handle, name) - } - for i, replica := range ss.ReplicaXs { - setupReplica(replica, ss.settings.DataSourceReplicas[i], "replica-"+strconv.Itoa(i)) - } - - for i, replica := range ss.searchReplicaXs { - setupReplica(replica, ss.settings.DataSourceSearchReplicas[i], "search-replica-"+strconv.Itoa(i)) - } - } + dbs := make([]*sql.DB, len(ss.ReplicaXs)) + for i, rx := range ss.ReplicaXs { + dbs[i] = rx.DB.DB } -} -func (ss *SqlStore) setDB(replica *atomic.Pointer[sqlxDBWrapper], handle *dbsql.DB, name string) { - replica.Store(newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), - time.Duration(*ss.settings.QueryTimeout)*time.Second, - *ss.settings.Trace)) - if ss.DriverName() == model.DatabaseDriverMysql { - replica.Load().MapperFunc(noOpMapper) - } - if ss.metrics != nil { - ss.metrics.RegisterDBCollector(replica.Load().DB.DB, name) - } + return dbs } func (ss *SqlStore) GetInternalReplicaDB() *sql.DB { @@ -553,7 +489,7 @@ func (ss *SqlStore) GetInternalReplicaDB() *sql.DB { } rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs)) - return ss.ReplicaXs[rrNum].Load().DB.DB + return ss.ReplicaXs[rrNum].DB.DB } func (ss *SqlStore) TotalMasterDbConnections() int { @@ -605,10 +541,7 @@ func (ss *SqlStore) TotalReadDbConnections() int { count := 0 for _, db := range ss.ReplicaXs { - if !db.Load().Online() { - continue - } - count = count + db.Load().Stats().OpenConnections + count = count + db.Stats().OpenConnections } return count @@ -621,10 +554,7 @@ func (ss *SqlStore) TotalSearchDbConnections() int { count := 0 for _, db := range ss.searchReplicaXs { - if !db.Load().Online() { - continue - } - count = count + db.Load().Stats().OpenConnections + count = count + db.Stats().OpenConnections } return count @@ -852,14 +782,9 @@ func IsUniqueConstraintError(err error, indexName []string) bool { } func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper { - all := make([]*sqlxDBWrapper, 0, len(ss.ReplicaXs)+1) - for i := range ss.ReplicaXs { - if !ss.ReplicaXs[i].Load().Online() { - continue - } - all = append(all, ss.ReplicaXs[i].Load()) - } - all = append(all, ss.masterX) + all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1) + copy(all, ss.ReplicaXs) + all[len(ss.ReplicaXs)] = ss.masterX return all } @@ -882,24 +807,11 @@ func (ss *SqlStore) RecycleDBConnections(d time.Duration) { func (ss *SqlStore) Close() { ss.masterX.Close() - // Closing monitor and waiting for it to be done. - // This needs to be done before closing the replica handles. - close(ss.quitMonitor) - ss.wgMonitor.Wait() - for _, replica := range ss.ReplicaXs { - if replica.Load().Online() { - replica.Load().Close() - } + replica.Close() } for _, replica := range ss.searchReplicaXs { - if replica.Load().Online() { - replica.Load().Close() - } - } - - for _, replica := range ss.replicaLagHandles { replica.Close() } } @@ -1220,10 +1132,7 @@ func (ss *SqlStore) migrate(direction migrationDirection) error { if err != nil { return err } - db, err2 := SetupConnection("master", dataSource, ss.settings, DBPingAttempts) - if err2 != nil { - return err2 - } + db := SetupConnection("master", dataSource, ss.settings) driver, err = ms.WithInstance(db) defer db.Close() case model.DatabaseDriverPostgres: diff --git a/server/channels/store/sqlstore/store_test.go b/server/channels/store/sqlstore/store_test.go index 699ee53e98..c218fa205d 100644 --- a/server/channels/store/sqlstore/store_test.go +++ b/server/channels/store/sqlstore/store_test.go @@ -761,15 +761,13 @@ func TestReplicaLagQuery(t *testing.T) { mockMetrics.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master") store := &SqlStore{ - rrCounter: 0, - srCounter: 0, - settings: settings, - metrics: mockMetrics, - quitMonitor: make(chan struct{}), - wgMonitor: &sync.WaitGroup{}, + rrCounter: 0, + srCounter: 0, + settings: settings, + metrics: mockMetrics, } - require.NoError(t, store.initConnection()) + store.initConnection() store.stores.post = newSqlPostStore(store, mockMetrics) err = store.migrate(migrationsDirectionUp) require.NoError(t, err) @@ -841,11 +839,9 @@ func TestMySQLReadTimeout(t *testing.T) { settings.DataSource = &dataSource store := &SqlStore{ - settings: settings, - quitMonitor: make(chan struct{}), - wgMonitor: &sync.WaitGroup{}, + settings: settings, } - require.NoError(t, store.initConnection()) + store.initConnection() defer store.Close() _, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`) diff --git a/server/channels/store/sqlstore/thread_store.go b/server/channels/store/sqlstore/thread_store.go index 66ce1f42a1..b731b0b71c 100644 --- a/server/channels/store/sqlstore/thread_store.go +++ b/server/channels/store/sqlstore/thread_store.go @@ -688,28 +688,6 @@ func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (* return s.updateMembership(s.GetMasterX(), membership) } -func (s *SqlThreadStore) DeleteMembershipsForChannel(userID, channelID string) error { - subQuery := s.getSubQueryBuilder(). - Select("1"). - From("Threads"). - Where(sq.And{ - sq.Expr("Threads.PostId = ThreadMemberships.PostId"), - sq.Eq{"Threads.ChannelId": channelID}, - }) - - query := s.getQueryBuilder(). - Delete("ThreadMemberships"). - Where(sq.Eq{"UserId": userID}). - Where(sq.Expr("EXISTS (?)", subQuery)) - - _, err := s.GetMasterX().ExecBuilder(query) - if err != nil { - return errors.Wrapf(err, "failed to remove thread memberships with userid=%s channelid=%s", userID, channelID) - } - - return nil -} - func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) { query := s.getQueryBuilder(). Update("ThreadMemberships"). @@ -734,14 +712,7 @@ func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model. memberships := []*model.ThreadMembership{} query := s.getQueryBuilder(). - Select( - "ThreadMemberships.PostId", - "ThreadMemberships.UserId", - "ThreadMemberships.Following", - "ThreadMemberships.LastUpdated", - "ThreadMemberships.LastViewed", - "ThreadMemberships.UnreadMentions", - ). + Select("ThreadMemberships.*"). Join("Threads ON Threads.PostId = ThreadMemberships.PostId"). From("ThreadMemberships"). Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}). @@ -761,14 +732,7 @@ func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.Thr func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) { var membership model.ThreadMembership query := s.getQueryBuilder(). - Select( - "PostId", - "UserId", - "Following", - "LastUpdated", - "LastViewed", - "UnreadMentions", - ). + Select("*"). From("ThreadMemberships"). Where(sq.And{ sq.Eq{"PostId": postId}, diff --git a/server/channels/store/store.go b/server/channels/store/store.go index cd813239d4..7da24fd24c 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -72,7 +72,10 @@ type Store interface { // GetInternalMasterDB allows access to the raw master DB // handle for the multi-product architecture. GetInternalMasterDB() *sql.DB + // GetInternalReplicaDBs allows access to the raw replica DB + // handles for the multi-product architecture. GetInternalReplicaDB() *sql.DB + GetInternalReplicaDBs() []*sql.DB TotalMasterDbConnections() int TotalReadDbConnections() int TotalSearchDbConnections() int @@ -344,7 +347,6 @@ type ThreadStore interface { PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) DeleteOrphanedRows(limit int) (deleted int64, err error) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) - DeleteMembershipsForChannel(userID, channelID string) error // Insights - threads GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) diff --git a/server/channels/store/storetest/channel_store_categories.go b/server/channels/store/storetest/channel_store_categories.go index 6ba934f45b..ecd49ef8c2 100644 --- a/server/channels/store/storetest/channel_store_categories.go +++ b/server/channels/store/storetest/channel_store_categories.go @@ -672,38 +672,6 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { require.NoError(t, err) assert.Equal(t, []string{}, res2.Channels) }) - - t.Run("should store the correct sorting value", func(t *testing.T) { - userId := model.NewId() - - team := setupTeam(t, ss, userId) - - opts := &store.SidebarCategorySearchOpts{ - TeamID: team.Id, - ExcludeTeam: false, - } - res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts) - require.NoError(t, nErr) - require.NotEmpty(t, res) - // Create the category - created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - DisplayName: model.NewId(), - Sorting: model.SidebarCategorySortManual, - }, - }) - require.NoError(t, err) - - // Confirm that sorting value is correct - res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id) - require.NoError(t, err) - require.Len(t, res.Categories, 4) - // first category will be favorites and second will be newly created - assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type) - assert.Equal(t, created.Id, res.Categories[1].Id) - assert.Equal(t, model.SidebarCategorySortManual, res.Categories[1].Sorting) - assert.Equal(t, model.SidebarCategorySortManual, created.Sorting) - }) } func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) { diff --git a/server/channels/store/storetest/mocks/Store.go b/server/channels/store/storetest/mocks/Store.go index bca15c95e0..bb06fb9005 100644 --- a/server/channels/store/storetest/mocks/Store.go +++ b/server/channels/store/storetest/mocks/Store.go @@ -346,6 +346,22 @@ func (_m *Store) GetInternalReplicaDB() *sql.DB { return r0 } +// GetInternalReplicaDBs provides a mock function with given fields: +func (_m *Store) GetInternalReplicaDBs() []*sql.DB { + ret := _m.Called() + + var r0 []*sql.DB + if rf, ok := ret.Get(0).(func() []*sql.DB); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*sql.DB) + } + } + + return r0 +} + // Group provides a mock function with given fields: func (_m *Store) Group() store.GroupStore { ret := _m.Called() diff --git a/server/channels/store/storetest/mocks/ThreadStore.go b/server/channels/store/storetest/mocks/ThreadStore.go index 661194a935..60b9211db2 100644 --- a/server/channels/store/storetest/mocks/ThreadStore.go +++ b/server/channels/store/storetest/mocks/ThreadStore.go @@ -29,20 +29,6 @@ func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) err return r0 } -// DeleteMembershipsForChannel provides a mock function with given fields: userID, channelID -func (_m *ThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error { - ret := _m.Called(userID, channelID) - - var r0 error - if rf, ok := ret.Get(0).(func(string, string) error); ok { - r0 = rf(userID, channelID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // DeleteOrphanedRows provides a mock function with given fields: limit func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) { ret := _m.Called(limit) diff --git a/server/channels/store/storetest/settings.go b/server/channels/store/storetest/settings.go index 0104b950bb..a1253f28bb 100644 --- a/server/channels/store/storetest/settings.go +++ b/server/channels/store/storetest/settings.go @@ -261,7 +261,6 @@ func MakeSqlSettings(driver string, withReplica bool) *model.SqlSettings { } log("Created temporary " + driver + " database " + dbName) - settings.ReplicaMonitorIntervalSeconds = model.NewInt(5) return settings } diff --git a/server/channels/store/storetest/thread_store.go b/server/channels/store/storetest/thread_store.go index efbc74d3ac..4cd64c8f1e 100644 --- a/server/channels/store/storetest/thread_store.go +++ b/server/channels/store/storetest/thread_store.go @@ -29,7 +29,6 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) }) t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) }) t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) }) - t.Run("DeleteMembershipsForChannel", func(t *testing.T) { testDeleteMembershipsForChannel(t, ss) }) } func testThreadStorePopulation(t *testing.T, ss store.Store) { @@ -1915,121 +1914,3 @@ func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) { assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB") }) } - -func testDeleteMembershipsForChannel(t *testing.T, ss store.Store) { - createThreadMembership := func(userID, postID string) (*model.ThreadMembership, func()) { - t.Helper() - opts := store.ThreadMembershipOpts{ - Following: true, - IncrementMentions: false, - UpdateFollowing: true, - UpdateViewedTimestamp: false, - UpdateParticipants: false, - } - mem, err := ss.Thread().MaintainMembership(userID, postID, opts) - require.NoError(t, err) - - return mem, func() { - err := ss.Thread().DeleteMembershipForUser(userID, postID) - require.NoError(t, err) - } - } - - postingUserID := model.NewId() - userAID := model.NewId() - userBID := model.NewId() - - team, err := ss.Team().Save(&model.Team{ - DisplayName: "DisplayName", - Name: "team" + model.NewId(), - Email: MakeEmail(), - Type: model.TeamOpen, - }) - require.NoError(t, err) - - channel1, err := ss.Channel().Save(&model.Channel{ - TeamId: team.Id, - DisplayName: "DisplayName", - Name: "channel1" + model.NewId(), - Type: model.ChannelTypeOpen, - }, -1) - require.NoError(t, err) - channel2, err := ss.Channel().Save(&model.Channel{ - TeamId: team.Id, - DisplayName: "DisplayName2", - Name: "channel2" + model.NewId(), - Type: model.ChannelTypeOpen, - }, -1) - require.NoError(t, err) - - rootPost1, err := ss.Post().Save(&model.Post{ - ChannelId: channel1.Id, - UserId: postingUserID, - Message: model.NewRandomString(10), - }) - require.NoError(t, err) - - _, err = ss.Post().Save(&model.Post{ - ChannelId: channel1.Id, - UserId: postingUserID, - Message: model.NewRandomString(10), - RootId: rootPost1.Id, - }) - require.NoError(t, err) - - rootPost2, err := ss.Post().Save(&model.Post{ - ChannelId: channel2.Id, - UserId: postingUserID, - Message: model.NewRandomString(10), - }) - require.NoError(t, err) - _, err = ss.Post().Save(&model.Post{ - ChannelId: channel2.Id, - UserId: postingUserID, - Message: model.NewRandomString(10), - RootId: rootPost2.Id, - }) - require.NoError(t, err) - - t.Run("should return memberships for user", func(t *testing.T) { - memA1, cleanupA1 := createThreadMembership(userAID, rootPost1.Id) - defer cleanupA1() - memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id) - defer cleanupA2() - - membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id) - require.NoError(t, err) - - require.Len(t, membershipsA, 2) - require.ElementsMatch(t, []*model.ThreadMembership{memA1, memA2}, membershipsA) - }) - - t.Run("should delete memberships for user for channel", func(t *testing.T) { - _, cleanupA1 := createThreadMembership(userAID, rootPost1.Id) - defer cleanupA1() - memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id) - defer cleanupA2() - - ss.Thread().DeleteMembershipsForChannel(userAID, channel1.Id) - membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id) - require.NoError(t, err) - - require.Len(t, membershipsA, 1) - require.ElementsMatch(t, []*model.ThreadMembership{memA2}, membershipsA) - }) - - t.Run("deleting memberships for channel for userA should not affect userB", func(t *testing.T) { - _, cleanupA1 := createThreadMembership(userAID, rootPost1.Id) - defer cleanupA1() - _, cleanupA2 := createThreadMembership(userAID, rootPost2.Id) - defer cleanupA2() - memB1, cleanupB2 := createThreadMembership(userBID, rootPost1.Id) - defer cleanupB2() - - membershipsB, err := ss.Thread().GetMembershipsForUser(userBID, team.Id) - require.NoError(t, err) - - require.Len(t, membershipsB, 1) - require.ElementsMatch(t, []*model.ThreadMembership{memB1}, membershipsB) - }) -} diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 3dc9a94c19..b52293e013 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -9112,22 +9112,6 @@ func (s *TimerLayerThreadStore) DeleteMembershipForUser(userId string, postID st return err } -func (s *TimerLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error { - start := time.Now() - - err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID) - - elapsed := float64(time.Since(start)) / float64(time.Second) - if s.Root.Metrics != nil { - success := "false" - if err == nil { - success = "true" - } - s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteMembershipsForChannel", success, elapsed) - } - return err -} - func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) { start := time.Now() diff --git a/server/channels/testlib/helper.go b/server/channels/testlib/helper.go index f6a1b22531..f74a562568 100644 --- a/server/channels/testlib/helper.go +++ b/server/channels/testlib/helper.go @@ -331,7 +331,7 @@ func (h *MainHelper) SetReplicationLagForTesting(seconds int) error { func (h *MainHelper) execOnEachReplica(query string, args ...any) error { for _, replica := range h.SQLStore.ReplicaXs { - _, err := replica.Load().Exec(query, args...) + _, err := replica.Exec(query, args...) if err != nil { return err } diff --git a/server/channels/utils/license.go b/server/channels/utils/license.go index 43f1f8a0ba..b937662f35 100644 --- a/server/channels/utils/license.go +++ b/server/channels/utils/license.go @@ -210,6 +210,7 @@ func GetSanitizedClientLicense(l map[string]string) map[string]string { delete(sanitizedLicense, "StartsAt") delete(sanitizedLicense, "ExpiresAt") delete(sanitizedLicense, "SkuName") + delete(sanitizedLicense, "SkuShortName") return sanitizedLicense } diff --git a/server/i18n/en.json b/server/i18n/en.json index 40f626291f..598462a448 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -1777,10 +1777,6 @@ "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_no_organization_name_provided_for_self_hosted_onboarding", - "translation": "Error no organization name provided for self hosted onboarding." - }, { "id": "api.error_set_first_admin_complete_setup", "translation": "Error trying to save first admin complete setup in the store." diff --git a/server/model/config.go b/server/model/config.go index af4341bdfa..f278c97cdf 100644 --- a/server/model/config.go +++ b/server/model/config.go @@ -1173,7 +1173,6 @@ type SqlSettings struct { DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"` MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"` ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none - ReplicaMonitorIntervalSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"` } func (s *SqlSettings) SetDefaults(isUpdate bool) { @@ -1238,10 +1237,6 @@ func (s *SqlSettings) SetDefaults(isUpdate bool) { if s.ReplicaLagSettings == nil { s.ReplicaLagSettings = []*ReplicaLagSettings{} } - - if s.ReplicaMonitorIntervalSeconds == nil { - s.ReplicaMonitorIntervalSeconds = NewInt(5) - } } type LogSettings struct { diff --git a/server/model/onboarding.go b/server/model/onboarding.go index 0fe5e91ffa..797bea7c1d 100644 --- a/server/model/onboarding.go +++ b/server/model/onboarding.go @@ -10,7 +10,6 @@ import ( // CompleteOnboardingRequest describes parameters of the requested plugin. type CompleteOnboardingRequest struct { - Organization string `json:"organization"` // Organization is the name of the organization InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed } diff --git a/server/model/system.go b/server/model/system.go index 24b4fce9c9..fbc2aaa684 100644 --- a/server/model/system.go +++ b/server/model/system.go @@ -16,7 +16,6 @@ const ( SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey" SystemPostActionCookieSecretKey = "PostActionCookieSecret" SystemInstallationDateKey = "InstallationDate" - SystemOrganizationName = "OrganizationName" SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp" SystemClusterEncryptionKey = "ClusterEncryptionKey" SystemUpgradedFromTeId = "UpgradedFromTE" diff --git a/server/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index 4fdbdf51ec..d4da4770bc 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -522,7 +522,6 @@ func (ts *TelemetryService) trackConfig() { "query_timeout": *cfg.SqlSettings.QueryTimeout, "disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch, "migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds, - "replica_monitor_interval_seconds": *cfg.SqlSettings.ReplicaMonitorIntervalSeconds, }) ts.SendTelemetry(TrackConfigLog, map[string]any{ diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index 6ac09280ab..cb22542fa4 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -14,7 +14,7 @@ import {Preferences} from 'mattermost-redux/constants'; import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general'; import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels'; import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; import {ChannelTypes} from 'mattermost-redux/action_types'; @@ -367,19 +367,11 @@ export async function redirectUserToDefaultTeam() { return; } - // if the user is the first admin - const isUserFirstAdmin = isFirstAdmin(state); - const locale = getCurrentLocale(state); const teamId = LocalStorageStore.getPreviousTeamId(user.id); let myTeams = getMyTeams(state); if (myTeams.length === 0) { - if (isUserFirstAdmin) { - getHistory().push('/preparing-workspace'); - return; - } - getHistory().push('/select_team'); return; } diff --git a/webapp/channels/src/components/do_verify_email/do_verify_email.tsx b/webapp/channels/src/components/do_verify_email/do_verify_email.tsx index 0e81ead2c7..7b03d9e7d5 100644 --- a/webapp/channels/src/components/do_verify_email/do_verify_email.tsx +++ b/webapp/channels/src/components/do_verify_email/do_verify_email.tsx @@ -6,6 +6,7 @@ import {useIntl} from 'react-intl'; import {useSelector, useDispatch} from 'react-redux'; import {useLocation, useHistory} from 'react-router-dom'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg'; @@ -14,6 +15,7 @@ import LoadingScreen from 'components/loading_screen'; import {clearErrors, logError} from 'mattermost-redux/actions/errors'; import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users'; +import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {DispatchFunc} from 'mattermost-redux/types/actions'; @@ -38,6 +40,7 @@ const DoVerifyEmail = () => { const token = params.get('token') ?? ''; const loggedIn = Boolean(useSelector(getCurrentUserId)); + const useCaseOnboarding = useSelector(getUseCaseOnboarding); const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING); const [serverError, setServerError] = useState(''); @@ -49,11 +52,16 @@ const DoVerifyEmail = () => { const handleRedirect = () => { if (loggedIn) { - // need info about whether admin or not, - // and whether admin has already completed - // first time onboarding. Instead of fetching and orchestrating that here, - // let the default root component handle it. - history.push('/'); + if (useCaseOnboarding) { + // need info about whether admin or not, + // and whether admin has already completed + // first time onboarding. Instead of fetching and orchestrating that here, + // let the default root component handle it. + history.push('/'); + return; + } + + redirectUserToDefaultTeam(); return; } diff --git a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts index 5a2ac01c35..a59ff532cc 100644 --- a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts +++ b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/index.ts @@ -8,6 +8,7 @@ import {withRouter} from 'react-router-dom'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {GenericAction} from 'mattermost-redux/types/actions'; import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams'; +import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins'; @@ -31,6 +32,7 @@ function mapStateToProps(state: GlobalState) { teamUrl: getCurrentRelativeTeamUrl(state), pluginMenuItems: getUserGuideDropdownPluginMenuItems(state), isFirstAdmin: isFirstAdmin(state), + useCaseOnboarding: getUseCaseOnboarding(state), }; } diff --git a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx index aa1ac2e833..effe92c1ad 100644 --- a/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx +++ b/webapp/channels/src/components/global_header/center_controls/user_guide_dropdown/user_guide_dropdown.test.tsx @@ -34,6 +34,7 @@ describe('components/channel_header/components/UserGuideDropdown', () => { }, pluginMenuItems: [], isFirstAdmin: false, + useCaseOnboarding: false, }; test('should match snapshot', () => { diff --git a/webapp/channels/src/components/login/login.test.tsx b/webapp/channels/src/components/login/login.test.tsx index 512c98856f..b1116f4d2c 100644 --- a/webapp/channels/src/components/login/login.test.tsx +++ b/webapp/channels/src/components/login/login.test.tsx @@ -288,18 +288,4 @@ describe('components/login/Login', () => { expect(externalLoginButton.props().label).toEqual('OpenID 2'); expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'}); }); - - it('should redirect on login', () => { - mockState.entities.users.currentUserId = 'user1'; - LocalStorageStore.setWasLoggedIn(true); - mockConfig.EnableSignInWithEmail = 'true'; - const redirectPath = '/boards/team/teamID/boardID'; - mockLocation.search = '?redirect_to=' + redirectPath; - mount( - - - , - ); - expect(mockHistoryPush).toHaveBeenCalledWith(redirectPath); - }); }); diff --git a/webapp/channels/src/components/login/login.tsx b/webapp/channels/src/components/login/login.tsx index edde751e63..bed62d7eed 100644 --- a/webapp/channels/src/components/login/login.tsx +++ b/webapp/channels/src/components/login/login.tsx @@ -13,7 +13,7 @@ import {UserProfile} from '@mattermost/types/users'; import {Client4} from 'mattermost-redux/client'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {isSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -104,6 +104,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => { const currentUser = useSelector(getCurrentUser); const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined)); const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? '')); + const useCaseOnboarding = useSelector(getUseCaseOnboarding); const isCloud = useSelector(isCurrentLicenseCloud); const graphQLEnabled = useSelector(isGraphQLEnabled); @@ -141,9 +142,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => { const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableSignUpWithSaml; const showSignup = enableOpenServer && (enableExternalSignup || enableSignUpWithEmail || enableLdap); - const query = new URLSearchParams(search); - const redirectTo = query.get('redirect_to'); - const getExternalLoginOptions = () => { const externalLoginOptions: ExternalLoginButtonType[] = []; @@ -375,10 +373,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => { useEffect(() => { if (currentUser) { - if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) { - history.push(redirectTo); - return; - } redirectUserToDefaultTeam(); return; } @@ -622,6 +616,9 @@ const Login = ({onCustomizeHeader}: LoginProps) => { dispatch(setNeedsLoggedInLimitReachedCheck(true)); } + const query = new URLSearchParams(search); + const redirectTo = query.get('redirect_to'); + setCSRFFromCookie(); // Record a successful login to local storage. If an unintentional logout occurs, e.g. @@ -634,12 +631,14 @@ const Login = ({onCustomizeHeader}: LoginProps) => { } else if (experimentalPrimaryTeamMember.team_id) { // Only set experimental team if user is on that team history.push(`/${ExperimentalPrimaryTeam}`); - } else { + } else if (useCaseOnboarding) { // need info about whether admin or not, // and whether admin has already completed // first time onboarding. Instead of fetching and orchestrating that here, // let the default root component handle it. history.push('/'); + } else { + redirectUserToDefaultTeam(); } }; diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap deleted file mode 100644 index 4aa2442f0a..0000000000 --- a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`InviteMembers component should match snapshot 1`] = ` -
-
-
-
-
-
- Previous step -
-

- - Invite your team members - -

-

- - Collaboration is tough by yourself. Invite a few team members using the invitation link below. - -

-
- -
-
- -
-
-
-
-
-
-`; diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap deleted file mode 100644 index 0610646391..0000000000 --- a/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = ` -
- -
-`; diff --git a/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap b/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap deleted file mode 100644 index cec545b0bd..0000000000 --- a/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/preparing-workspace/organization_status should match snapshot 1`] = ` -
-`; diff --git a/webapp/channels/src/components/preparing_workspace/index.tsx b/webapp/channels/src/components/preparing_workspace/index.tsx index c454fd28ac..a3ab4aa606 100644 --- a/webapp/channels/src/components/preparing_workspace/index.tsx +++ b/webapp/channels/src/components/preparing_workspace/index.tsx @@ -5,7 +5,7 @@ import {connect} from 'react-redux'; import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux'; import {Action} from 'mattermost-redux/types/actions'; -import {checkIfTeamExists, createTeam, updateTeam} from 'mattermost-redux/actions/teams'; +import {checkIfTeamExists, createTeam} from 'mattermost-redux/actions/teams'; import {getProfiles} from 'mattermost-redux/actions/users'; import PreparingWorkspace, {Actions} from './preparing_workspace'; @@ -13,7 +13,6 @@ import PreparingWorkspace, {Actions} from './preparing_workspace'; function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators, Actions>({ - updateTeam, createTeam, getProfiles, checkIfTeamExists, diff --git a/webapp/channels/src/components/preparing_workspace/invite_members.scss b/webapp/channels/src/components/preparing_workspace/invite_members.scss deleted file mode 100644 index dc914d42b1..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members.scss +++ /dev/null @@ -1,51 +0,0 @@ -@import 'utils/mixins'; - -.InviteMembers-body { - display: flex; - // page width - channels preview width - progress dots width - people overlap width - max-width: calc(100vw - 600px - 120px - 30px); - - .UsersEmailsInput { - max-width: 420px; - } -} - -.InviteMembers { - &__submit { - display: flex; - align-items: center; - justify-content: flex-start; - } -} - -@include simple-in-and-out-before("InviteMembers"); - -.ChannelsPreview--enter-from-after { - &-enter { - transform: translateX(-100vw); - } - - &-enter-active { - transform: translateX(0); - transition: transform 300ms ease-in-out; - } - - &-enter-done { - transform: translateX(0); - } -} - -.ChannelsPreview--exit-to-after { - &-exit { - transform: translateX(0); - } - - &-exit-active { - transform: translateX(-100vw); - transition: transform 300ms ease-in-out; - } - - &-exit-done { - transform: translateX(-100vw); - } -} diff --git a/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx b/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx deleted file mode 100644 index 54fe45f374..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {ComponentProps} from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; -import {withIntl} from 'tests/helpers/intl-test-helper'; - -import InviteMembers from './invite_members'; - -describe('InviteMembers component', () => { - let defaultProps: ComponentProps; - - beforeEach(() => { - defaultProps = { - disableEdits: false, - browserSiteUrl: 'https://my-org.mattermost.com', - formUrl: 'https://my-org.mattermost.com/signup', - teamInviteId: '1234', - className: 'test-class', - configSiteUrl: 'https://my-org.mattermost.com/config', - onPageView: jest.fn(), - previous:
{'Previous step'}
, - next: jest.fn(), - show: true, - transitionDirection: 'forward', - }; - }); - - it('should match snapshot', () => { - const component = withIntl(); - const {container} = render(component); - expect(container).toMatchSnapshot(); - }); - - it('renders invite URL', () => { - const component = withIntl(); - render(component); - const inviteLink = screen.getByTestId('shareLinkInput'); - expect(inviteLink).toHaveAttribute( - 'value', - 'https://my-org.mattermost.com/config/signup_user_complete/?id=1234', - ); - }); - - it('renders submit button with correct text', () => { - const component = withIntl(); - render(component); - const button = screen.getByRole('button', {name: 'Finish setup'}); - expect(button).toBeInTheDocument(); - }); - - it('button is disabled when disableEdits is true', () => { - const component = withIntl( - , - ); - render(component); - const button = screen.getByRole('button', {name: 'Finish setup'}); - expect(button).toBeDisabled(); - }); - - it('invokes next prop on button click', () => { - const component = withIntl(); - render(component); - const button = screen.getByRole('button', {name: 'Finish setup'}); - fireEvent.click(button); - expect(defaultProps.next).toHaveBeenCalled(); - }); -}); diff --git a/webapp/channels/src/components/preparing_workspace/invite_members.tsx b/webapp/channels/src/components/preparing_workspace/invite_members.tsx deleted file mode 100644 index a018c2a446..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useMemo, useEffect} from 'react'; -import {CSSTransition} from 'react-transition-group'; -import {FormattedMessage} from 'react-intl'; - -import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps'; - -import Title from './title'; -import Description from './description'; -import PageBody from './page_body'; -import SingleColumnLayout from './single_column_layout'; - -import InviteMembersLink from './invite_members_link'; -import PageLine from './page_line'; -import './invite_members.scss'; - -type Props = PreparingWorkspacePageProps & { - disableEdits: boolean; - className?: string; - teamInviteId?: string; - formUrl: Form['url']; - configSiteUrl?: string; - browserSiteUrl: string; -} - -const InviteMembers = (props: Props) => { - let className = 'InviteMembers-body'; - if (props.className) { - className += ' ' + props.className; - } - - useEffect(props.onPageView, []); - - const inviteURL = useMemo(() => { - let urlBase = ''; - if (props.configSiteUrl && !props.configSiteUrl.includes('localhost')) { - urlBase = props.configSiteUrl; - } else if (props.formUrl && !props.formUrl.includes('localhost')) { - urlBase = props.formUrl; - } else { - urlBase = props.browserSiteUrl; - } - return `${urlBase}/signup_user_complete/?id=${props.teamInviteId}`; - }, [props.teamInviteId, props.configSiteUrl, props.browserSiteUrl, props.formUrl]); - - const description = ( - - ); - - const inviteInteraction = ; - - return ( - -
- - - {props.previous} - - <FormattedMessage - id={'onboarding_wizard.invite_members.title'} - defaultMessage='Invite your team members' - /> - - - {description} - - - {inviteInteraction} - -
- -
- -
-
-
- ); -}; - -export default InviteMembers; diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx b/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx deleted file mode 100644 index 26b28e9b6f..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx +++ /dev/null @@ -1,838 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {SVGProps} from 'react'; - -const InviteMembersIllustration = (props: SVGProps) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default InviteMembersIllustration; diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_link.scss b/webapp/channels/src/components/preparing_workspace/invite_members_link.scss deleted file mode 100644 index 09b229f264..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members_link.scss +++ /dev/null @@ -1,51 +0,0 @@ -.InviteMembersLink { - display: flex; - - &__input { - height: 48px; - flex-grow: 1; - padding: 12px 14px; - border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.2); - border-right: 0; - border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.2); - border-left: 1px solid rgba(var(--center-channel-color-rgb), 0.2); - background: rgba(var(--center-channel-color-rgb), 0.04); - border-radius: 4px 0 0 4px; - color: rgba(var(--center-channel-color-rgb), 0.56); - font-size: 16px; - } - - &__button { - display: flex; - width: 180px; - max-width: 382px; - height: 48px; - flex-grow: 0; - align-items: center; - justify-content: center; - border: 1px solid var(--button-bg); - background: var(--center-channel-bg); - border-radius: 0 4px 4px 0; - color: var(--button-bg); - font-size: 16px; - font-weight: 600; - - &:hover { - background: rgba(var(--button-bg-rgb), 0.08); - } - - &:active { - background: rgba(var(--button-bg-rgb), 0.08); - } - - span { - display: inline-block; - height: 24px; - margin-right: 9px; - } - - svg { - fill: var(--button-bg); - } - } -} diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx b/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx deleted file mode 100644 index d74b81d493..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; -import {trackEvent} from 'actions/telemetry_actions'; -import InviteMembersLink from './invite_members_link'; -import {withIntl} from 'tests/helpers/intl-test-helper'; - -jest.mock('actions/telemetry_actions', () => ({ - trackEvent: jest.fn(), -})); - -describe('components/preparing-workspace/invite_members_link', () => { - const inviteURL = 'https://invite-url.mattermost.com'; - - it('should match snapshot', () => { - const component = withIntl(); - - const {container} = render(component); - expect(container).toMatchSnapshot(); - }); - - it('renders an input field with the invite URL', () => { - const component = withIntl(); - render(component); - const input = screen.getByDisplayValue(inviteURL); - expect(input).toBeInTheDocument(); - }); - - it('renders a button to copy the invite URL', () => { - const component = withIntl(); - render(component); - const button = screen.getByRole('button', {name: /copy link/i}); - expect(button).toBeInTheDocument(); - }); - - it('calls the trackEvent function when the copy button is clicked', () => { - const component = withIntl(); - render(component); - const button = screen.getByRole('button', {name: /copy link/i}); - fireEvent.click(button); - expect(trackEvent).toHaveBeenCalledWith( - 'first_admin_setup', - 'admin_setup_click_copy_invite_link', - ); - }); - - it('changes the button text to "Link Copied" when the URL is copied', () => { - const component = withIntl(); - render(component); - const button = screen.getByRole('button', {name: /copy link/i}); - const originalText = 'Copy Link'; - const linkCopiedText = 'Link Copied'; - expect(button).toHaveTextContent(originalText); - - fireEvent.click(button); - - expect(button).toHaveTextContent(linkCopiedText); - }); -}); diff --git a/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx b/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx deleted file mode 100644 index f6491809ca..0000000000 --- a/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; - -import useCopyText from 'components/common/hooks/useCopyText'; -import {trackEvent} from 'actions/telemetry_actions'; - -import './invite_members_link.scss'; - -type Props = { - inviteURL: string; -} - -const InviteMembersLink = (props: Props) => { - const copyText = useCopyText({ - trackCallback: () => trackEvent('first_admin_setup', 'admin_setup_click_copy_invite_link'), - text: props.inviteURL, - }); - const intl = useIntl(); - - return ( -
- - -
- ); -}; - -export default InviteMembersLink; diff --git a/webapp/channels/src/components/preparing_workspace/mixins.scss b/webapp/channels/src/components/preparing_workspace/mixins.scss deleted file mode 100644 index b3ca03bce8..0000000000 --- a/webapp/channels/src/components/preparing_workspace/mixins.scss +++ /dev/null @@ -1,12 +0,0 @@ -@mixin input { - width: 452px; - padding: 12px 16px; - border: 2px solid rgba(var(--center-channel-color-rgb), 0.16); - border-radius: 4px; - font-size: 16px; - - &:active, - &:focus { - border: 2px solid var(--button-bg); - } -} diff --git a/webapp/channels/src/components/preparing_workspace/organization.scss b/webapp/channels/src/components/preparing_workspace/organization.scss deleted file mode 100644 index c063010404..0000000000 --- a/webapp/channels/src/components/preparing_workspace/organization.scss +++ /dev/null @@ -1,63 +0,0 @@ -@import 'utils/variables'; -@import 'utils/mixins'; -@import './mixins'; - -.Organization-body { - display: flex; -} - -.Organization-form-wrapper { - position: relative; -} - -.Organization-left-col { - width: 210px; - min-width: 210px; -} - -.Organization-right-col { - display: flex; - flex-direction: column; - justify-content: center; -} - -.Organization { - &__input { - @include input; - } - - &__status { - display: flex; - align-items: center; - color: rgba(var(--center-channel-color-rgb), 0.72); - font-size: 12px; - - &--error { - margin-top: 8px; - color: var(--dnd-indicator); - } - } - - &__progress-path { - position: absolute; - top: -25px; - left: -55px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-end; - text-align: center; - } - - &__content { - margin-left: 200px; - } -} - -@media screen and (max-width: 700px) { - .Organization-left-col { - display: none; - } -} - -@include simple-in-and-out("Organization"); diff --git a/webapp/channels/src/components/preparing_workspace/organization.tsx b/webapp/channels/src/components/preparing_workspace/organization.tsx deleted file mode 100644 index 684c6dc4d9..0000000000 --- a/webapp/channels/src/components/preparing_workspace/organization.tsx +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useState, useEffect, useRef, ChangeEvent} from 'react'; -import {CSSTransition} from 'react-transition-group'; -import {FormattedMessage, useIntl} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; - -import debounce from 'lodash/debounce'; - -import OrganizationSVG from 'components/common/svg_images_components/organization-building_svg'; -import QuickInput from 'components/quick_input'; - -import {trackEvent} from 'actions/telemetry_actions'; - -import {getTeams} from 'mattermost-redux/actions/teams'; -import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; -import {Team} from '@mattermost/types/teams'; - -import {teamNameToUrl} from 'utils/url'; -import Constants from 'utils/constants'; - -import OrganizationStatus, {TeamApiError} from './organization_status'; -import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps} from './steps'; -import PageLine from './page_line'; -import Title from './title'; -import Description from './description'; -import PageBody from './page_body'; - -import './organization.scss'; - -type Props = PreparingWorkspacePageProps & { - organization: Form['organization']; - setOrganization: (organization: Form['organization']) => void; - className?: string; - createTeam: (OrganizationName: string) => Promise<{error: string | null; newTeam: Team | null}>; - updateTeam: (teamToUpdate: Team) => Promise<{error: string | null; updatedTeam: Team | null}>; - setInviteId: (inviteId: string) => void; -} - -const reportValidationError = debounce(() => { - trackEvent('first_admin_setup', 'validate_organization_error'); -}, 700, {leading: false}); - -const Organization = (props: Props) => { - const {formatMessage} = useIntl(); - const dispatch = useDispatch(); - - const [triedNext, setTriedNext] = useState(false); - const inputRef = useRef(); - const validation = teamNameToUrl(props.organization || ''); - const teamApiError = useRef(null); - - useEffect(props.onPageView, []); - - const teams = useSelector(getActiveTeamsList); - useEffect(() => { - if (!teams) { - dispatch(getTeams(0, 60)); - } - }, [teams]); - - const setApiCallError = () => { - teamApiError.current = TeamApiError; - }; - - const updateTeamNameFromOrgName = async () => { - if (!inputRef.current?.value) { - return; - } - const name = inputRef.current?.value.trim(); - - const currentTeam = teams[0]; - - if (currentTeam && name && name !== currentTeam.display_name) { - const {error} = await props.updateTeam({...currentTeam, display_name: name}); - if (error !== null) { - setApiCallError(); - } - } - }; - - const createTeamFromOrgName = async () => { - if (!inputRef.current?.value) { - return; - } - const name = inputRef.current?.value.trim(); - - if (name) { - const {error, newTeam} = await props.createTeam(name); - if (error !== null || newTeam === null) { - props.setInviteId(''); - setApiCallError(); - return; - } - props.setInviteId(newTeam.invite_id); - } - }; - - const handleOnChange = (e: ChangeEvent) => { - props.setOrganization(e.target.value); - teamApiError.current = null; - }; - - const onNext = (e?: React.KeyboardEvent | React.MouseEvent) => { - if (e && (e as React.KeyboardEvent).key) { - if ((e as React.KeyboardEvent).key !== Constants.KeyCodes.ENTER[0]) { - return; - } - } - if (!triedNext) { - setTriedNext(true); - } - - // if there is already a team, maybe because a page reload, then just update the teamname - const thereIsAlreadyATeam = teams.length > 0; - teamApiError.current = null; - - if (!validation.error && !thereIsAlreadyATeam) { - createTeamFromOrgName(); - } else if (!validation.error && thereIsAlreadyATeam) { - updateTeamNameFromOrgName(); - } - - if (validation.error || teamApiError.current) { - reportValidationError(); - return; - } - props.next?.(); - }; - - let className = 'Organization-body'; - if (props.className) { - className += ' ' + props.className; - } - return ( - -
-
-
-
- - -
-
- {props.previous} - - <FormattedMessage - id={'onboarding_wizard.organization.title'} - defaultMessage='What’s the name of your organization?' - /> - - - - - - handleOnChange(e)} - onKeyUp={onNext} - autoFocus={true} - ref={inputRef as unknown as any} - /> - {triedNext ? : null} - - -
-
-
-
-
- ); -}; -export default Organization; diff --git a/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx b/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx deleted file mode 100644 index e7d65bfd6b..0000000000 --- a/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {render} from '@testing-library/react'; -import {BadUrlReasons} from 'utils/url'; -import OrganizationStatus, {TeamApiError} from './organization_status'; -import {withIntl} from 'tests/helpers/intl-test-helper'; - -describe('components/preparing-workspace/organization_status', () => { - const defaultProps = { - error: null, - }; - - it('should match snapshot', () => { - const {container} = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('should render no error message when error prop is null', () => { - const {queryByText, container} = render(); - expect((container.getElementsByClassName('Organization__status').length)).toBe(1); - expect(queryByText(/empty/i)).not.toBeInTheDocument(); - expect(queryByText(/team api error/i)).not.toBeInTheDocument(); - expect(queryByText(/length/i)).not.toBeInTheDocument(); - expect(queryByText(/reserved/i)).not.toBeInTheDocument(); - }); - - it('should render an error message for an empty organization name', () => { - const component = withIntl(); - const {getByText} = render(component); - expect(getByText(/You must enter an organization name/i)).toBeInTheDocument(); - }); - - it('should render an error message for a team API error', () => { - const component = withIntl(); - const {getByText} = render(component); - expect(getByText(/There was an error, please try again/i)).toBeInTheDocument(); - }); - - it('should render an error message for an organization name with invalid length', () => { - const component = withIntl(); - const {getByText} = render(component); - expect(getByText(/Organization name must be between 2 and 64 characters/i)).toBeInTheDocument(); - }); -}); diff --git a/webapp/channels/src/components/preparing_workspace/organization_status.tsx b/webapp/channels/src/components/preparing_workspace/organization_status.tsx deleted file mode 100644 index d695a2ad26..0000000000 --- a/webapp/channels/src/components/preparing_workspace/organization_status.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import {BadUrlReasons, UrlValidationCheck} from 'utils/url'; -import Constants, {DocLinks} from 'utils/constants'; -import ExternalLink from 'components/external_link'; - -export const TeamApiError = 'team_api_error'; - -const OrganizationStatus = (props: {error: (UrlValidationCheck['error'] | typeof TeamApiError | null)}): JSX.Element => { - let children = null; - let className = 'Organization__status'; - if (props.error) { - className += ' Organization__status--error'; - switch (props.error) { - case BadUrlReasons.Empty: - children = ( - - ); - break; - case TeamApiError: - children = ( - - ); - break; - case BadUrlReasons.Length: - children = ( - - ); - break; - case BadUrlReasons.Reserved: - children = ( - ( - - {chunks} - - ), - }} - /> - ); - break; - default: - children = ( - - ); - break; - } - } - return
{children}
; -}; - -export default OrganizationStatus; diff --git a/webapp/channels/src/components/preparing_workspace/page_line.scss b/webapp/channels/src/components/preparing_workspace/page_line.scss deleted file mode 100644 index 12801e1f67..0000000000 --- a/webapp/channels/src/components/preparing_workspace/page_line.scss +++ /dev/null @@ -1,10 +0,0 @@ -.PageLine { - position: relative; - left: 100px; - width: 1px; - background-color: rgba(var(--center-channel-color-rgb), 0.24); - - &--no-left { - left: initial; - } -} diff --git a/webapp/channels/src/components/preparing_workspace/page_line.tsx b/webapp/channels/src/components/preparing_workspace/page_line.tsx deleted file mode 100644 index ebbb9ee024..0000000000 --- a/webapp/channels/src/components/preparing_workspace/page_line.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import './page_line.scss'; - -type Props = { - style?: Record; - noLeft?: boolean; -} -const PageLine = (props: Props) => { - let className = 'PageLine'; - if (props.noLeft) { - className += ' PageLine--no-left'; - } - const styles: Record = {}; - if (props?.style) { - Object.assign(styles, props.style); - } - if (!styles.height) { - styles.height = '100vh'; - } - if ((!props.style?.height && styles.height === '100vh') && !styles.marginTop) { - styles.marginTop = '50px'; - } - return ( -
- ); -}; - -export default PageLine; diff --git a/webapp/channels/src/components/preparing_workspace/plugins.scss b/webapp/channels/src/components/preparing_workspace/plugins.scss index 0a5465564e..fa74dc5718 100644 --- a/webapp/channels/src/components/preparing_workspace/plugins.scss +++ b/webapp/channels/src/components/preparing_workspace/plugins.scss @@ -4,9 +4,6 @@ margin-top: 24px; } -.plugins-skip-btn { - margin-left: 8px; -} // preempt cards wrapping @media screen and (max-width: 900px) { .Plugins-body { diff --git a/webapp/channels/src/components/preparing_workspace/plugins.tsx b/webapp/channels/src/components/preparing_workspace/plugins.tsx index caf04e794e..b3b1168015 100644 --- a/webapp/channels/src/components/preparing_workspace/plugins.tsx +++ b/webapp/channels/src/components/preparing_workspace/plugins.tsx @@ -21,16 +21,15 @@ import {Animations, mapAnimationReasonToClass, Form, PreparingWorkspacePageProps import Title from './title'; import Description from './description'; import PageBody from './page_body'; + import SingleColumnLayout from './single_column_layout'; -import PageLine from './page_line'; import './plugins.scss'; type Props = PreparingWorkspacePageProps & { options: Form['plugins']; setOption: (option: keyof Form['plugins']) => void; className?: string; - isSelfHosted: boolean; } const Plugins = (props: Props) => { const {formatMessage} = useIntl(); @@ -45,34 +44,6 @@ const Plugins = (props: Props) => { if (props.className) { className += ' ' + props.className; } - - let title = ( - - ); - let description = ( - - ); - if (props.isSelfHosted) { - title = ( - - ); - description = ( - - ); - } - return ( { >
- {props.previous} - {title} - {!props.isSelfHosted && ( - <div className='subtitle'> - <CelebrateSVG/> - <FormattedMessage - id={'onboarding_wizard.cloud_plugins.subtitle'} - defaultMessage='(almost there!)' - /> - </div> - - )} + <FormattedMessage + id={'onboarding_wizard.plugins.title'} + defaultMessage='Welcome to Mattermost!' + /> + <div className='subtitle'> + <CelebrateSVG/> + <FormattedMessage + id={'onboarding_wizard.plugins.subtitle'} + defaultMessage='(almost there!)' + /> + </div> - {description} + + + { />
-
diff --git a/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss b/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss index 99187c301b..c91dd0a1fe 100644 --- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss +++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss @@ -63,21 +63,6 @@ .primary-button { @include primary-button; @include button-medium; - - box-sizing: border-box; - border: 2px solid var(--button-bg); - } - - .primary-button[disabled] { - box-sizing: border-box; - border: 2px solid rgba(var(--center-channel-color-rgb), 0.01); - } - - .link-style { - @include link; - - background: transparent; - font-size: 14px; } .child-page { @@ -85,43 +70,6 @@ position: absolute; height: 100vh; } - - &__invite-members-illustration { - position: absolute; - top: 25%; - right: -651px; - animation-duration: 0.3s; - animation-fill-mode: forwards; - animation-timing-function: ease-in-out; - } -} - -.enter { - animation-name: slideInRight; -} - -.exit { - animation-name: slideOutRight; -} - -@keyframes slideInRight { - from { - right: -651px; - } - - to { - right: 0; - } -} - -@keyframes slideOutRight { - from { - right: 0; - } - - to { - right: -651px; - } } .PreparingWorkspacePageContainer { diff --git a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx index 268e21c55e..d554090d68 100644 --- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx +++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx @@ -1,24 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useState, useCallback, useEffect, useRef, useMemo} from 'react'; +import React, {useState, useCallback, useEffect, useRef} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {RouterProps} from 'react-router-dom'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {useIntl} from 'react-intl'; import {GeneralTypes} from 'mattermost-redux/action_types'; import {General} from 'mattermost-redux/constants'; import {getFirstAdminSetupComplete as getFirstAdminSetupCompleteAction} from 'mattermost-redux/actions/general'; import {ActionResult} from 'mattermost-redux/types/actions'; import {Team} from '@mattermost/types/teams'; +import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeam, getMyTeams} from 'mattermost-redux/selectors/entities/teams'; -import {getFirstAdminSetupComplete, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getFirstAdminSetupComplete, getConfig} from 'mattermost-redux/selectors/entities/general'; import {Client4} from 'mattermost-redux/client'; import Constants from 'utils/constants'; -import {getSiteURL, teamNameToUrl} from 'utils/url'; -import {makeNewTeam} from 'utils/team_utils'; import {pageVisited, trackEvent} from 'actions/telemetry_actions'; @@ -36,14 +35,10 @@ import { mapStepToPageView, mapStepToSubmitFail, PLUGIN_NAME_TO_ID_MAP, - mapStepToPrevious, } from './steps'; -import Organization from './organization'; import Plugins from './plugins'; import Progress from './progress'; -import InviteMembers from './invite_members'; -import InviteMembersIllustration from './invite_members_illustration'; import LaunchingWorkspace, {START_TRANSITIONING_OUT} from './launching_workspace'; import './preparing_workspace.scss'; @@ -63,7 +58,6 @@ const WAIT_FOR_REDIRECT_TIME = 2000 - START_TRANSITIONING_OUT; export type Actions = { createTeam: (team: Team) => ActionResult; - updateTeam: (team: Team) => ActionResult; checkIfTeamExists: (teamName: string) => ActionResult; getProfiles: (page: number, perPage: number, options: Record) => ActionResult; } @@ -87,16 +81,12 @@ function makeSubmitFail(step: WizardStep) { } const trackSubmitFail = { - [WizardSteps.Organization]: makeSubmitFail(WizardSteps.Organization), [WizardSteps.Plugins]: makeSubmitFail(WizardSteps.Plugins), - [WizardSteps.InviteMembers]: makeSubmitFail(WizardSteps.InviteMembers), [WizardSteps.LaunchingWorkspace]: makeSubmitFail(WizardSteps.LaunchingWorkspace), }; const onPageViews = { - [WizardSteps.Organization]: makeOnPageView(WizardSteps.Organization), [WizardSteps.Plugins]: makeOnPageView(WizardSteps.Plugins), - [WizardSteps.InviteMembers]: makeOnPageView(WizardSteps.InviteMembers), [WizardSteps.LaunchingWorkspace]: makeOnPageView(WizardSteps.LaunchingWorkspace), }; @@ -108,35 +98,28 @@ const PreparingWorkspace = (props: Props) => { defaultMessage: 'Something went wrong. Please try again.', }); const isUserFirstAdmin = useSelector(isFirstAdmin); + const useCaseOnboarding = useSelector(getUseCaseOnboarding); const currentTeam = useSelector(getCurrentTeam); const myTeams = useSelector(getMyTeams); // In cloud instances created from portal, // new admin user has a team in myTeams but not in currentTeam. - let team = currentTeam || myTeams?.[0]; + const team = currentTeam || myTeams?.[0]; const config = useSelector(getConfig); const pluginsEnabled = config.PluginsEnabled === 'true'; const showOnMountTimeout = useRef(); - const configSiteUrl = config.SiteURL; - const isSelfHosted = useSelector(getLicense).Cloud !== 'true'; const stepOrder = [ - isSelfHosted && WizardSteps.Organization, pluginsEnabled && WizardSteps.Plugins, - isSelfHosted && WizardSteps.InviteMembers, WizardSteps.LaunchingWorkspace, ].filter((x) => Boolean(x)) as WizardStep[]; - // first steporder that is not false - const firstShowablePage = stepOrder[0]; - const firstAdminSetupComplete = useSelector(getFirstAdminSetupComplete); const [[mostRecentStep, currentStep], setStepHistory] = useState<[WizardStep, WizardStep]>([stepOrder[0], stepOrder[0]]); const [submissionState, setSubmissionState] = useState(SubmissionStates.Presubmit); - const browserSiteUrl = useMemo(getSiteURL, []); const [form, setForm] = useState({ ...emptyForm, }); @@ -205,44 +188,13 @@ const PreparingWorkspace = (props: Props) => { trackSubmitFail[redirectTo](); }, []); - const createTeam = async (OrganizationName: string): Promise<{error: string | null; newTeam: Team | null}> => { - const data = await props.actions.createTeam(makeNewTeam(OrganizationName, teamNameToUrl(OrganizationName || '').url)); - if (data.error) { - return {error: genericSubmitError, newTeam: null}; - } - return {error: null, newTeam: data.data}; - }; - - const updateTeam = async (teamToUpdate: Team): Promise<{error: string | null; updatedTeam: Team | null}> => { - const data = await props.actions.updateTeam(teamToUpdate); - if (data.error) { - return {error: genericSubmitError, updatedTeam: null}; - } - return {error: null, updatedTeam: data.data}; - }; - const sendForm = async () => { const sendFormStart = Date.now(); setSubmissionState(SubmissionStates.Submitting); - if (form.organization && !isSelfHosted) { - try { - const {error, newTeam} = await createTeam(form.organization); - if (error !== null) { - redirectWithError(WizardSteps.Organization, genericSubmitError); - return; - } - team = newTeam as Team; - } catch (e) { - redirectWithError(WizardSteps.Organization, genericSubmitError); - return; - } - } - // send plugins const {skipped: skippedPlugins, ...pluginChoices} = form.plugins; let pluginsToSetup: string[] = []; - if (!skippedPlugins) { pluginsToSetup = Object.entries(pluginChoices).reduce( (acc: string[], [k, v]): string[] => (v ? [...acc, PLUGIN_NAME_TO_ID_MAP[k as keyof Omit]] : acc), [], @@ -252,10 +204,8 @@ const PreparingWorkspace = (props: Props) => { // This endpoint sets setup complete state, so we need to make this request // even if admin skipped submitting plugins. const completeSetupRequest = { - organization: form.organization, install_plugins: pluginsToSetup, }; - try { await Client4.completeSetup(completeSetupRequest); dispatch({type: GeneralTypes.FIRST_ADMIN_COMPLETE_SETUP_RECEIVED, data: true}); @@ -271,7 +221,6 @@ const PreparingWorkspace = (props: Props) => { const sendFormEnd = Date.now(); const timeToWait = WAIT_FOR_REDIRECT_TIME - (sendFormEnd - sendFormStart); - if (timeToWait > 0) { setTimeout(goToChannels, timeToWait); } else { @@ -287,8 +236,7 @@ const PreparingWorkspace = (props: Props) => { }, [submissionState]); const adminRevisitedPage = firstAdminSetupComplete && submissionState === SubmissionStates.Presubmit; - const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage; - + const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage || !useCaseOnboarding; useEffect(() => { if (shouldRedirect) { props.history.push('/'); @@ -308,24 +256,6 @@ const PreparingWorkspace = (props: Props) => { return stepIndex > currentStepIndex ? Animations.Reasons.ExitToBefore : Animations.Reasons.ExitToAfter; }; - const goPrevious = useCallback((e?: React.KeyboardEvent | React.MouseEvent) => { - if (e && (e as React.KeyboardEvent).key) { - const key = (e as React.KeyboardEvent).key; - if (key !== Constants.KeyCodes.ENTER[0] && key !== Constants.KeyCodes.SPACE[0]) { - return; - } - } - if (submissionState !== SubmissionStates.Presubmit && submissionState !== SubmissionStates.SubmitFail) { - return; - } - const stepIndex = stepOrder.indexOf(currentStep); - if (stepIndex <= 0) { - return; - } - trackEvent('first_admin_setup', mapStepToPrevious(currentStep)); - setStepHistory([currentStep, stepOrder[stepIndex - 1]]); - }, [currentStep]); - const skipPlugins = useCallback((skipped: boolean) => { if (skipped === form.plugins.skipped) { return; @@ -339,46 +269,6 @@ const PreparingWorkspace = (props: Props) => { }); }, [form]); - const skipTeamMembers = useCallback((skipped: boolean) => { - if (skipped === form.teamMembers.skipped) { - return; - } - setForm({ - ...form, - teamMembers: { - ...form.teamMembers, - skipped, - }, - }); - }, [form]); - - const getInviteMembersAnimationClass = useCallback(() => { - if (currentStep === WizardSteps.InviteMembers) { - return 'enter'; - } else if (mostRecentStep === WizardSteps.InviteMembers) { - return 'exit'; - } - return ''; - }, [currentStep]); - - let previous: React.ReactNode = ( -
- - -
- ); - if (currentStep === firstShowablePage) { - previous = null; - } - return (
{submissionState === SubmissionStates.SubmitFail && submitError && ( @@ -401,49 +291,17 @@ const PreparingWorkspace = (props: Props) => { transitionSpeed={Animations.PAGE_SLIDE} />
- { - setForm({ - ...form, - organization, - }); - }} - setInviteId={(inviteId: string) => { - setForm({ - ...form, - teamMembers: { - ...form.teamMembers, - inviteId, - }, - }); - }} - className='child-page' - createTeam={createTeam} - updateTeam={updateTeam} - /> - { const pluginChoices = {...form.plugins}; delete pluginChoices.skipped; - if (!isSelfHosted) { - setSubmissionState(SubmissionStates.UserRequested); - } + setSubmissionState(SubmissionStates.UserRequested); makeNext(WizardSteps.Plugins)(pluginChoices); skipPlugins(false); }} skip={() => { - if (!isSelfHosted) { - setSubmissionState(SubmissionStates.UserRequested); - } + setSubmissionState(SubmissionStates.UserRequested); makeNext(WizardSteps.Plugins, true)(); skipPlugins(true); }} @@ -461,40 +319,12 @@ const PreparingWorkspace = (props: Props) => { transitionDirection={getTransitionDirection(WizardSteps.Plugins)} className='child-page' /> - { - skipTeamMembers(false); - const inviteMembersTracking = { - inviteCount: form.teamMembers.invites.length, - }; - setSubmissionState(SubmissionStates.UserRequested); - makeNext(WizardSteps.InviteMembers)(inviteMembersTracking); - }} - skip={() => { - skipTeamMembers(true); - setSubmissionState(SubmissionStates.UserRequested); - makeNext(WizardSteps.InviteMembers, true)(); - }} - previous={previous} - show={shouldShowPage(WizardSteps.InviteMembers)} - transitionDirection={getTransitionDirection(WizardSteps.InviteMembers)} - disableEdits={submissionState !== SubmissionStates.Presubmit && submissionState !== SubmissionStates.SubmitFail} - className='child-page' - teamInviteId={team?.invite_id || form.teamMembers.inviteId} - configSiteUrl={configSiteUrl} - formUrl={form.url} - browserSiteUrl={browserSiteUrl} - />
-
- -
); }; diff --git a/webapp/channels/src/components/preparing_workspace/single_column_layout.scss b/webapp/channels/src/components/preparing_workspace/single_column_layout.scss index afff27dcfe..357faabdf4 100644 --- a/webapp/channels/src/components/preparing_workspace/single_column_layout.scss +++ b/webapp/channels/src/components/preparing_workspace/single_column_layout.scss @@ -4,4 +4,5 @@ height: 100vh; flex-direction: column; align-items: flex-start; + justify-content: center; } diff --git a/webapp/channels/src/components/preparing_workspace/steps.ts b/webapp/channels/src/components/preparing_workspace/steps.ts index cbb78da5b6..ed52d984af 100644 --- a/webapp/channels/src/components/preparing_workspace/steps.ts +++ b/webapp/channels/src/components/preparing_workspace/steps.ts @@ -4,9 +4,7 @@ import deepFreeze from 'mattermost-redux/utils/deep_freeze'; export const WizardSteps = { - Organization: 'Organization', Plugins: 'Plugins', - InviteMembers: 'InviteMembers', LaunchingWorkspace: 'LaunchingWorkspace', } as const; @@ -22,12 +20,8 @@ export const Animations = { export function mapStepToNextName(step: WizardStep): string { switch (step) { - case WizardSteps.Organization: - return 'admin_onboarding_next_organization'; case WizardSteps.Plugins: return 'admin_onboarding_next_plugins'; - case WizardSteps.InviteMembers: - return 'admin_onboarding_next_invite_members'; case WizardSteps.LaunchingWorkspace: return 'admin_onboarding_next_transitioning_out'; default: @@ -37,12 +31,8 @@ export function mapStepToNextName(step: WizardStep): string { export function mapStepToPrevious(step: WizardStep): string { switch (step) { - case WizardSteps.Organization: - return 'admin_onboarding_previous_organization'; case WizardSteps.Plugins: return 'admin_onboarding_previous_plugins'; - case WizardSteps.InviteMembers: - return 'admin_onboarding_previous_invite_members'; case WizardSteps.LaunchingWorkspace: return 'admin_onboarding_previous_transitioning_out'; default: @@ -52,12 +42,8 @@ export function mapStepToPrevious(step: WizardStep): string { export function mapStepToPageView(step: WizardStep): string { switch (step) { - case WizardSteps.Organization: - return 'pageview_admin_onboarding_organization'; case WizardSteps.Plugins: return 'pageview_admin_onboarding_plugins'; - case WizardSteps.InviteMembers: - return 'pageview_admin_onboarding_invite_members'; case WizardSteps.LaunchingWorkspace: return 'pageview_admin_onboarding_transitioning_out'; default: @@ -67,12 +53,8 @@ export function mapStepToPageView(step: WizardStep): string { export function mapStepToSubmitFail(step: WizardStep): string { switch (step) { - case WizardSteps.Organization: - return 'admin_onboarding_organization_submit_fail'; case WizardSteps.Plugins: return 'admin_onboarding_plugins_submit_fail'; - case WizardSteps.InviteMembers: - return 'admin_onboarding_invite_members_submit_fail'; case WizardSteps.LaunchingWorkspace: return 'admin_onboarding_transitioning_out_submit_fail'; default: @@ -82,12 +64,8 @@ export function mapStepToSubmitFail(step: WizardStep): string { export function mapStepToSkipName(step: WizardStep): string { switch (step) { - case WizardSteps.Organization: - return 'admin_onboarding_skip_organization'; case WizardSteps.Plugins: return 'admin_onboarding_skip_plugins'; - case WizardSteps.InviteMembers: - return 'admin_onboarding_skip_invite_members'; case WizardSteps.LaunchingWorkspace: return 'admin_onboarding_skip_transitioning_out'; default: @@ -150,14 +128,12 @@ export type Form = { skipped: boolean; }; teamMembers: { - inviteId: string; invites: string[]; skipped: boolean; }; } export const emptyForm = deepFreeze({ - organization: '', inferredProtocol: null, urlSkipped: false, useCase: { @@ -180,7 +156,6 @@ export const emptyForm = deepFreeze({ skipped: false, }, teamMembers: { - inviteId: '', invites: [], skipped: false, }, @@ -190,7 +165,7 @@ export type PreparingWorkspacePageProps = { transitionDirection: AnimationReason; next?: () => void; skip?: () => void; - previous?: React.ReactNode; + previous?: JSX.Element; show: boolean; onPageView: () => void; } diff --git a/webapp/channels/src/components/root/root.tsx b/webapp/channels/src/components/root/root.tsx index 73f78f6af6..eb41c2d0fe 100644 --- a/webapp/channels/src/components/root/root.tsx +++ b/webapp/channels/src/components/root/root.tsx @@ -10,7 +10,7 @@ import classNames from 'classnames'; import {Client4} from 'mattermost-redux/client'; import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder'; import {General} from 'mattermost-redux/constants'; -import {Theme} from 'mattermost-redux/selectors/entities/preferences'; +import {Theme, getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getCurrentUser, isCurrentUserSystemAdmin, checkIsFirstAdmin} from 'mattermost-redux/selectors/entities/users'; import {setUrl} from 'mattermost-redux/actions/general'; @@ -89,8 +89,6 @@ import {ActionResult} from 'mattermost-redux/types/actions'; import WelcomePostRenderer from 'components/welcome_post_renderer'; -import {getMyTeams} from 'mattermost-redux/selectors/entities/teams'; - import {applyLuxonDefaults} from './effects'; import RootProvider from './root_provider'; @@ -360,8 +358,8 @@ export default class Root extends React.PureComponent { return; } - const myTeams = getMyTeams(storeState); - if (myTeams.length > 0) { + const useCaseOnboarding = getUseCaseOnboarding(storeState); + if (!useCaseOnboarding) { GlobalActions.redirectUserToDefaultTeam(); return; } diff --git a/webapp/channels/src/components/root/root_redirect/index.ts b/webapp/channels/src/components/root/root_redirect/index.ts index eca15abc20..7575f7c5a4 100644 --- a/webapp/channels/src/components/root/root_redirect/index.ts +++ b/webapp/channels/src/components/root/root_redirect/index.ts @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general'; import {getCurrentUserId, isCurrentUserSystemAdmin, isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; +import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; import {GenericAction} from 'mattermost-redux/types/actions'; import {GlobalState} from 'types/store'; @@ -13,7 +14,11 @@ import {GlobalState} from 'types/store'; import RootRedirect, {Props} from './root_redirect'; function mapStateToProps(state: GlobalState) { - const isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state); + const useCaseOnboarding = getUseCaseOnboarding(state); + let isElegibleForFirstAdmingOnboarding = useCaseOnboarding; + if (isElegibleForFirstAdmingOnboarding) { + isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state); + } return { currentUserId: getCurrentUserId(state), isElegibleForFirstAdmingOnboarding, diff --git a/webapp/channels/src/components/signup/signup.test.tsx b/webapp/channels/src/components/signup/signup.test.tsx index 900e8de8b7..dcc56032a0 100644 --- a/webapp/channels/src/components/signup/signup.test.tsx +++ b/webapp/channels/src/components/signup/signup.test.tsx @@ -7,6 +7,8 @@ import {IntlProvider} from 'react-intl'; import {BrowserRouter} from 'react-router-dom'; import {act, screen} from '@testing-library/react'; +import * as global_actions from 'actions/global_actions'; + import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import Signup from 'components/signup/signup'; @@ -195,6 +197,9 @@ describe('components/signup/Signup', () => { mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser mockResolvedValueOnce({error: {server_error_id: 'api.user.login.not_verified.app_error'}}); // loginById + const mockRedirectUserToDefaultTeam = jest.fn(); + jest.spyOn(global_actions, 'redirectUserToDefaultTeam').mockImplementation(mockRedirectUserToDefaultTeam); + const wrapper = mountWithIntl( @@ -223,6 +228,7 @@ describe('components/signup/Signup', () => { expect(wrapper.find('#input_name').first().props().disabled).toEqual(true); expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true); + expect(mockRedirectUserToDefaultTeam).not.toHaveBeenCalled(); expect(mockHistoryPush).toHaveBeenCalledWith('/should_verify_email?email=jdoe%40mm.com&teamname=teamName'); }); @@ -232,6 +238,9 @@ describe('components/signup/Signup', () => { mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser mockResolvedValueOnce({}); // loginById + const mockRedirectUserToDefaultTeam = jest.fn(); + jest.spyOn(global_actions, 'redirectUserToDefaultTeam').mockImplementation(mockRedirectUserToDefaultTeam); + const wrapper = mountWithIntl( @@ -259,6 +268,8 @@ describe('components/signup/Signup', () => { expect(wrapper.find(Input).first().props().disabled).toEqual(true); expect(wrapper.find('#input_name').first().props().disabled).toEqual(true); expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true); + + expect(mockRedirectUserToDefaultTeam).toHaveBeenCalled(); }); it('should add user to team and redirect when team invite valid and logged in', async () => { diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index 0407a74895..454d5cd8ae 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -17,7 +17,7 @@ import {getTeamInviteInfo} from 'mattermost-redux/actions/teams'; import {createUser, loadMe, loadMeREST} from 'mattermost-redux/actions/users'; import {DispatchFunc} from 'mattermost-redux/types/actions'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {isEmail} from 'mattermost-redux/utils/helpers'; @@ -25,6 +25,7 @@ import {GlobalState} from 'types/store'; import {getGlobalItem} from 'selectors/storage'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; import {removeGlobalItem, setGlobalItem} from 'actions/storage'; import {addUserToTeamFromInvite} from 'actions/team_actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; @@ -103,6 +104,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { } = config; const {IsLicensed, Cloud} = useSelector(getLicense); const loggedIn = Boolean(useSelector(getCurrentUserId)); + const useCaseOnboarding = useSelector(getUseCaseOnboarding); const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined)); const graphQLEnabled = useSelector(isGraphQLEnabled); @@ -308,7 +310,15 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { } else if (inviteId) { getInviteInfo(inviteId); } else if (loggedIn) { - history.push('/'); + if (useCaseOnboarding) { + // need info about whether admin or not, + // and whether admin has already completed + // first tiem onboarding. Instead of fetching and orchestrating that here, + // let the default root component handle it. + history.push('/'); + } else { + redirectUserToDefaultTeam(); + } } } @@ -451,12 +461,14 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { if (redirectTo) { history.push(redirectTo); - } else { + } else if (useCaseOnboarding) { // need info about whether admin or not, // and whether admin has already completed // first tiem onboarding. Instead of fetching and orchestrating that here, // let the default root component handle it. history.push('/'); + } else { + redirectUserToDefaultTeam(); } }; diff --git a/webapp/channels/src/components/terms_of_service/index.ts b/webapp/channels/src/components/terms_of_service/index.ts index c22fba415f..95faca09a1 100644 --- a/webapp/channels/src/components/terms_of_service/index.ts +++ b/webapp/channels/src/components/terms_of_service/index.ts @@ -6,6 +6,7 @@ import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux'; import {getTermsOfService, updateMyTermsOfServiceStatus} from 'mattermost-redux/actions/users'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; import {GlobalState} from '@mattermost/types/store'; import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions'; @@ -25,7 +26,9 @@ type Actions = { function mapStateToProps(state: GlobalState) { const config = getConfig(state); + const useCaseOnboarding = getUseCaseOnboarding(state); return { + useCaseOnboarding, termsEnabled: config.EnableCustomTermsOfService === 'true', emojiMap: getEmojiMap(state), }; diff --git a/webapp/channels/src/components/terms_of_service/terms_of_service.test.tsx b/webapp/channels/src/components/terms_of_service/terms_of_service.test.tsx index 5c9f58faab..d05c91cd84 100644 --- a/webapp/channels/src/components/terms_of_service/terms_of_service.test.tsx +++ b/webapp/channels/src/components/terms_of_service/terms_of_service.test.tsx @@ -27,6 +27,7 @@ describe('components/terms_of_service/TermsOfService', () => { location: {search: ''}, termsEnabled: true, emojiMap: {} as EmojiMap, + useCaseOnboarding: false, }; test('should match snapshot', () => { diff --git a/webapp/channels/src/components/terms_of_service/terms_of_service.tsx b/webapp/channels/src/components/terms_of_service/terms_of_service.tsx index f885830f9c..992086d561 100644 --- a/webapp/channels/src/components/terms_of_service/terms_of_service.tsx +++ b/webapp/channels/src/components/terms_of_service/terms_of_service.tsx @@ -38,6 +38,7 @@ export interface TermsOfServiceProps { ) => {data: UpdateMyTermsOfServiceStatusResponse}; }; emojiMap: EmojiMap; + useCaseOnboarding: boolean; } interface TermsOfServiceState { @@ -110,12 +111,14 @@ export default class TermsOfService extends React.PureComponent { const [integrations, setIntegrations] = useState(); - const marketplacePlugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins); - const loadedPlugins = useSelector((state: GlobalState) => state.plugins.plugins); + const plugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins); const [illustrationDetails, setIllustrationDetails] = useState(() => { const defaultIllustration = getTemplateDefaultIllustration(template); @@ -131,14 +130,13 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { const intg = availableIntegrations?. flatMap((integration) => { - return marketplacePlugins.reduce((acc: Integration[], curr) => { + return plugins.reduce((acc: Integration[], curr) => { if (curr.manifest.id === integration.id) { - const installed = Boolean(loadedPlugins[integration.id]); acc.push({ ...integration, name: curr.manifest.name, icon: curr.icon_data, - installed, + installed: curr.installed_version !== '', }); return acc; @@ -151,7 +149,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { if (intg?.length) { setIntegrations(intg); } - }, [marketplacePlugins, availableIntegrations, loadedPlugins, pluginsEnabled]); + }, [plugins, availableIntegrations, pluginsEnabled]); // building accordion items const accordionItemsData: AccordionItemType[] = []; @@ -206,7 +204,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { )], }); } - if (pluginsEnabled && integrations?.length) { + if (integrations?.length && pluginsEnabled) { accordionItemsData.push({ id: 'integrations', icon: , @@ -305,7 +303,6 @@ const StyledPreview = styled(Preview)` width: 387px; height: 416px; padding-right: 32px; - margin-top: 17px; } strong { diff --git a/webapp/channels/src/components/work_templates/components/preview/accordion.tsx b/webapp/channels/src/components/work_templates/components/preview/accordion.tsx index 02be467269..41394b9dc4 100644 --- a/webapp/channels/src/components/work_templates/components/preview/accordion.tsx +++ b/webapp/channels/src/components/work_templates/components/preview/accordion.tsx @@ -12,7 +12,6 @@ const Accordion = styled(LibAccordion)` .accordion-card { margin-bottom: 8px; border-radius: 4px; - border: 1px solid transparent; color: var(--center-channel-color); .accordion-card-header { @@ -47,7 +46,7 @@ const Accordion = styled(LibAccordion)` } &.active { - border-color: var(--denim-button-bg); + border: 1px solid var(--denim-button-bg); .accordion-card-header { color: var(--denim-button-bg); diff --git a/webapp/channels/src/components/work_templates/components/preview/section.tsx b/webapp/channels/src/components/work_templates/components/preview/section.tsx index 3514da9b09..00ee7e8f13 100644 --- a/webapp/channels/src/components/work_templates/components/preview/section.tsx +++ b/webapp/channels/src/components/work_templates/components/preview/section.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {ReactNode, useCallback, useEffect, useState} from 'react'; +import React, {ReactNode, useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; import classnames from 'classnames'; import styled from 'styled-components'; @@ -110,28 +110,6 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps id: 'work_templates.preview.integrations.admin_install.notify', defaultMessage: 'Notify admin to install integrations.', }); - - const makeIntegrationSubtext = useCallback((integration: IntegrationPreviewSectionItemsProps) => { - if (integration.installed) { - return formatMessage({ - id: 'work_templates.preview.integrations.already_installed', - defaultMessage: 'Already installed', - }); - } - - if (!pluginInstallationPossible) { - return formatMessage({ - id: 'work_templates.preview.integrations.app_install', - defaultMessage: 'App Install', - }); - } - - return formatMessage({ - id: 'work_templates.preview.integrations.to_be_installed', - defaultMessage: 'To be installed', - }); - }, [pluginInstallationPossible, formatMessage]); - return (
@@ -141,17 +119,15 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps key={item.id} className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})} > -
+
- {item.name}
- - {makeIntegrationSubtext(item)} - + {item.name}
{item.installed && -
} +
} + {!item.installed &&
}
); })}
@@ -229,7 +205,6 @@ const StyledPreviewSection = styled(PreviewSection)` &-item { display: flex; - align-items: center; width: 128px; height: 48px; flex-basis: 45%; @@ -240,7 +215,7 @@ const StyledPreviewSection = styled(PreviewSection)` opacity: 65%; } - &__illustration { + &__icon { display: flex; width: 24px; height: 24px; @@ -252,30 +227,22 @@ const StyledPreviewSection = styled(PreviewSection)` width: 100%; height: 100%; } + + &_blue { + color: var(--denim-button-bg); + } } &__name { flex-grow: 2; + margin-top: 8px; color: var(--center-channel-text); font-family: 'Open Sans'; font-size: 11px; font-style: normal; font-weight: 600; letter-spacing: 0.02em; - line-height: 16px; - &-sub { - color: rgba(var(--center-channel-color-rgb), 0.72); - font-weight: 400; - font-size: 10px; - } - } - - &__icon { - align-self: flex-start; - - &_blue { - color: var(--denim-button-bg); - } + line-height: 22px; } } } @@ -297,8 +264,8 @@ const StyledPreviewSection = styled(PreviewSection)` } .icon-check-circle::before { - margin-top: 2px; - margin-right: 2px; + margin-top: 8px; + margin-right: 8px; } .icon-download-outline::before { diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c5af857223..21b4595f82 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4318,26 +4318,10 @@ "notify_here.question": "By using **@here** you are about to send notifications to up to **{totalMembers} other people**. Are you sure you want to do this?", "notify_here.question_timezone": "By using **@here** you are about to send notifications to up to **{totalMembers} other people** in **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Are you sure you want to do this?", "numMembers": "{num, number} {num, plural, one {member} other {members}}", - "onboarding_wizard.cloud_plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.", - "onboarding_wizard.cloud_plugins.subtitle": "(almost there!)", - "onboarding_wizard.cloud_plugins.title": "Welcome to Mattermost!", - "onboarding_wizard.invite_members.copied_link": "Link Copied", - "onboarding_wizard.invite_members.copy_link": "Copy Link", - "onboarding_wizard.invite_members.copy_link_input": "team invite link", - "onboarding_wizard.invite_members.description_link": "Collaboration is tough by yourself. Invite a few team members using the invitation link below.", - "onboarding_wizard.invite_members.next_link": "Finish setup", - "onboarding_wizard.invite_members.title": "Invite your team members", "onboarding_wizard.launching_workspace.description": "It’ll be ready in a moment", "onboarding_wizard.launching_workspace.title": "Launching your workspace now", "onboarding_wizard.next": "Continue", - "onboarding_wizard.organization.description": "We’ll use this to help personalize your workspace.", - "onboarding_wizard.organization.empty": "You must enter an organization name", - "onboarding_wizard.organization.length": "Organization name must be between {min} and {max} characters", - "onboarding_wizard.organization.other": "Invalid organization name: {reason}", - "onboarding_wizard.organization.placeholder": "Organization name", - "onboarding_wizard.organization.reserved": "Organization name may not start with a reserved word.", - "onboarding_wizard.organization.team_api_error": "There was an error, please try again.", - "onboarding_wizard.organization.title": "What’s the name of your organization?", + "onboarding_wizard.plugins.description": "Mattermost is better when integrated with the tools your team uses for collaboration. Popular tools are below, select the ones your team uses and we'll add them to your workspace. Additional set up may be needed later.", "onboarding_wizard.plugins.github": "GitHub", "onboarding_wizard.plugins.github.tooltip": "Subscribe to repositories, stay up to date with reviews, assignments", "onboarding_wizard.plugins.gitlab": "GitLab", @@ -4345,14 +4329,13 @@ "onboarding_wizard.plugins.jira": "Jira", "onboarding_wizard.plugins.jira.tooltip": "Create Jira tickets from messages in Mattermost, get notified of important updates in Jira", "onboarding_wizard.plugins.marketplace": "More tools can be added once your workspace is set up. To see all available integrations, visit the Marketplace.", + "onboarding_wizard.plugins.subtitle": "(almost there!)", + "onboarding_wizard.plugins.title": "Welcome to Mattermost!", "onboarding_wizard.plugins.todo": "To do", "onboarding_wizard.plugins.todo.tooltip": "A plugin to track Todo issues in a list and send you daily reminders about your Todo list", "onboarding_wizard.plugins.zoom": "Zoom", "onboarding_wizard.plugins.zoom.tooltip": "Start Zoom audio and video conferencing calls in Mattermost with a single click", - "onboarding_wizard.previous": "Previous", - "onboarding_wizard.self_hosted_plugins.description": "Choose the tools you work with, and we'll add them to your workspace. Additional set up may be needed later.", - "onboarding_wizard.self_hosted_plugins.title": "What tools do you use?", - "onboarding_wizard.skip-button": "Skip", + "onboarding_wizard.skip": "Skip for now", "onboarding_wizard.submit_error.generic": "Something went wrong. Please try again.", "onboardingTask.checklist.completed_subtitle": "We hope Mattermost is more familiar now.", "onboardingTask.checklist.completed_title": "Well done. You’ve completed all of the tasks!", @@ -5784,9 +5767,6 @@ "work_templates.preview.integrations.admin_install.multiple_plugin": "Integrations will not be added until admin installs them.", "work_templates.preview.integrations.admin_install.notify": "Notify admin to install integrations", "work_templates.preview.integrations.admin_install.single_plugin": "{plugin} will not be added until admin installs it.", - "work_templates.preview.integrations.already_installed": "Already installed", - "work_templates.preview.integrations.app_install": "App Install", - "work_templates.preview.integrations.to_be_installed": "To be installed", "work_templates.preview.modal_cancel_button": "Back", "work_templates.preview.modal_next_button": "Next", "work_templates.preview.modal_title": "Preview {useCase}", diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts index f31c122014..f5afb85aa2 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts @@ -4,18 +4,6 @@ import {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; -import {ServerError} from '@mattermost/types/errors'; -import { - Channel, - ChannelNotifyProps, - ChannelMembership, - ChannelModerationPatch, - ChannelsWithTotalCount, - ChannelSearchOpts, - ServerChannel, -} from '@mattermost/types/channels'; -import {PreferenceType} from '@mattermost/types/preferences'; - import {ChannelTypes, PreferenceTypes, UserTypes} from 'mattermost-redux/action_types'; import {Client4} from 'mattermost-redux/client'; @@ -31,12 +19,18 @@ import { getRedirectChannelNameForTeam, isManuallyUnread, } from 'mattermost-redux/selectors/entities/channels'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getConfig, getServerVersion} from 'mattermost-redux/selectors/entities/general'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {ActionFunc, ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; -import {getChannelByName} from 'mattermost-redux/utils/channel_utils'; +import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils'; + +import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; + +import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch, ChannelsWithTotalCount, ChannelSearchOpts} from '@mattermost/types/channels'; + +import {PreferenceType} from '@mattermost/types/preferences'; import {General, Preferences} from '../constants'; @@ -468,33 +462,52 @@ export function getChannelTimezones(channelId: string): ActionFunc { }; } -export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc<{channels: ServerChannel[]; channelMembers: ChannelMembership[]}> { +export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + dispatch({ + type: ChannelTypes.CHANNELS_REQUEST, + data: null, + }); + let channels; let channelMembers; + const state = getState(); + const shouldFetchArchived = isMinimumServerVersion(getServerVersion(state), 5, 21); try { [channels, channelMembers] = await Promise.all([ - Client4.getMyChannels(teamId), + Client4.getMyChannels(teamId, shouldFetchArchived), Client4.getMyChannelMembers(teamId), ]); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); + dispatch({type: ChannelTypes.CHANNELS_FAILURE, error}); dispatch(logError(error)); - return {error: error as ServerError}; + return {error}; } + const {currentUserId} = state.entities.users; + const {currentChannelId} = state.entities.channels; + dispatch(batchActions([ { type: ChannelTypes.RECEIVED_CHANNELS, teamId, data: channels, + currentChannelId, + }, + { + type: ChannelTypes.CHANNELS_SUCCESS, }, { type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, data: channelMembers, + sync: !shouldFetchArchived, + channels, + remove: getChannelsIdForTeam(state, teamId), + currentUserId, + currentChannelId, }, ])); - const roles = new Set(); for (const member of channelMembers) { for (const role of member.roles.split(' ')) { @@ -505,7 +518,7 @@ export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc<{chann dispatch(loadRolesIfNeeded(roles)); } - return {data: {channels, channelMembers}}; + return {data: {channels, members: channelMembers}}; }; } diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts index b2d3c1a672..2ef11c99d3 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts @@ -245,6 +245,10 @@ export function isCustomGroupsEnabled(state: GlobalState): boolean { return getConfig(state).EnableCustomGroups === 'true'; } +export function getUseCaseOnboarding(state: GlobalState): boolean { + return getFeatureFlagValue(state, 'UseCaseOnboarding') === 'true' && getLicense(state)?.Cloud === 'true'; +} + export function insightsAreEnabled(state: GlobalState): boolean { const isConfiguredForFeature = getConfig(state).InsightsEnabled === 'true'; const featureIsEnabled = getFeatureFlagValue(state, 'InsightsEnabled') === 'true'; diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 7f1dce9244..62fc2e8fdd 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -1099,7 +1099,6 @@ export const DocLinks = { ONBOARD_LDAP: 'https://docs.mattermost.com/onboard/ad-ldap.html', ONBOARD_SSO: 'https://docs.mattermost.com/onboard/sso-saml.html', TRUE_UP_REVIEW: 'https://mattermost.com/pl/true-up-documentation', - ABOUT_TEAMS: 'https://docs.mattermost.com/welcome/about-teams.html#team-url', }; export const LicenseLinks = { diff --git a/webapp/platform/types/src/setup.ts b/webapp/platform/types/src/setup.ts index 085527a434..980aa05dce 100644 --- a/webapp/platform/types/src/setup.ts +++ b/webapp/platform/types/src/setup.ts @@ -2,6 +2,5 @@ // See LICENSE.txt for license information. export type CompleteOnboardingRequest = { - organization: string; install_plugins: string[]; }