From 2ef9c7a6341a4b5a7a17cf3faa4eb7a0359b319a Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Tue, 11 Apr 2023 14:56:10 +0530 Subject: [PATCH 01/69] e2e: fix yearly subscription test --- .../enterprise/cloud/billing/yearly_subscription_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js index 0eacc1350c..3c4b443381 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/yearly_subscription_spec.js @@ -142,7 +142,7 @@ describe('System Console - Subscriptions section', () => { cy.get('.RHS').find('button').should('be.enabled'); // # Change the user seats field to a value smaller than the current number of users - const lessThanUserCount = count - 5; + const lessThanUserCount = 1; cy.get('#input_UserSeats').clear().type(lessThanUserCount); // * Ensure that the yearly, monthly, and yearly saving prices match the new user seats value entered From b6f77e8bdc7ab7d9515ead77b7e39e55aa2c5aba Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Tue, 11 Apr 2023 18:57:06 +0530 Subject: [PATCH 02/69] e2e: fix more channels tests --- .../channels/channel/more_channels_spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index 95e31a75d4..326a21b1e3 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -65,7 +65,7 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').wait(TIMEOUTS.HALF_SEC); + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -113,7 +113,7 @@ describe('Channels', () => { cy.findByText('Archived Channels').should('be.visible').click(); // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', 'Show: Archived Channels'); + cy.wrap(el).should('contain', 'Channel Type: Archived'); }); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); @@ -196,7 +196,7 @@ describe('Channels', () => { // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', 'Show: Public Channels'); + cy.wrap(el).should('contain', 'Channel Type: Public'); }); // * Users should be able to type and search @@ -207,12 +207,12 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').click().within((el) => { + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').click().within((el) => { // # Click on archived channels item cy.findByText('Archived Channels').should('be.visible').click(); // * Modal should show the archived channels list - cy.wrap(el).should('contain', 'Show: Archived Channels'); + cy.wrap(el).should('contain', 'Channel Type: Archived'); }).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').clear(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); @@ -250,7 +250,7 @@ function verifyMoreChannelsModal(isEnabled) { // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Show: Public Channels'); + cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Channel Type: Public'); } else { cy.get('#channelsMoreDropdown').should('not.exist'); } From c120e729310c946f011ebd91b12c92d102acae32 Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Tue, 18 Apr 2023 17:51:24 +0530 Subject: [PATCH 03/69] chore: revert channels changes --- .../channels/channel/more_channels_spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index 326a21b1e3..2e983dfcfc 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -65,7 +65,7 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').wait(TIMEOUTS.HALF_SEC); + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -113,7 +113,7 @@ describe('Channels', () => { cy.findByText('Archived Channels').should('be.visible').click(); // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', 'Channel Type: Archived'); + cy.wrap(el).should('contain', 'Show: Archived Channels'); }); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); @@ -196,7 +196,7 @@ describe('Channels', () => { // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', 'Channel Type: Public'); + cy.wrap(el).should('contain', 'Show: Public Channels'); }); // * Users should be able to type and search @@ -207,12 +207,12 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').click().within((el) => { + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').click().within((el) => { // # Click on archived channels item cy.findByText('Archived Channels').should('be.visible').click(); // * Modal should show the archived channels list - cy.wrap(el).should('contain', 'Channel Type: Archived'); + cy.wrap(el).should('contain', 'Show: Archived Channels'); }).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').clear(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); @@ -250,9 +250,9 @@ function verifyMoreChannelsModal(isEnabled) { // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Channel Type: Public'); + cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Show: Public Channels'); } else { cy.get('#channelsMoreDropdown').should('not.exist'); } }); -} +} \ No newline at end of file From 89026c106171088eba29b85a1e3aefb98be6cf27 Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Tue, 18 Apr 2023 17:52:00 +0530 Subject: [PATCH 04/69] chore: revert channels changes --- .../tests/integration/channels/channel/more_channels_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index 2e983dfcfc..95e31a75d4 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -255,4 +255,4 @@ function verifyMoreChannelsModal(isEnabled) { cy.get('#channelsMoreDropdown').should('not.exist'); } }); -} \ No newline at end of file +} From c207b4ce0febe757c1659f2899a2f49dfcaa6b1f Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Tue, 18 Apr 2023 18:17:43 +0530 Subject: [PATCH 05/69] chore: fix channels test --- .../channels/channel/archived_channels_1_spec.js | 10 +++++----- .../channels/channel/more_channels_spec.js | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index 1880dafb8a..6b9301814b 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -97,7 +97,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Click on dropdown - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText('Channel Type: Public').should('be.visible').click(); // # Click archived channels cy.findByText('Archived Channels').click(); @@ -145,7 +145,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Public channel list opens by default - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText('Channel Type: Public').should('be.visible').click(); // # Click on archived channels cy.findByText('Archived Channels').click(); @@ -198,7 +198,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Public channels are shown by default - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText('Channel Type: Public').should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -252,7 +252,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Show public channels is visible by default - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText('Channel Type: Public').should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -286,7 +286,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens and lands on public channels cy.get('#moreChannelsModal').should('be.visible').within(() => { - cy.findByText('Show: Public Channels').should('be.visible').click(); + cy.findByText('Channel Type: Public').should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index 95e31a75d4..326a21b1e3 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -65,7 +65,7 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').wait(TIMEOUTS.HALF_SEC); + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -113,7 +113,7 @@ describe('Channels', () => { cy.findByText('Archived Channels').should('be.visible').click(); // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', 'Show: Archived Channels'); + cy.wrap(el).should('contain', 'Channel Type: Archived'); }); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); @@ -196,7 +196,7 @@ describe('Channels', () => { // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', 'Show: Public Channels'); + cy.wrap(el).should('contain', 'Channel Type: Public'); }); // * Users should be able to type and search @@ -207,12 +207,12 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Show: Public Channels').click().within((el) => { + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').click().within((el) => { // # Click on archived channels item cy.findByText('Archived Channels').should('be.visible').click(); // * Modal should show the archived channels list - cy.wrap(el).should('contain', 'Show: Archived Channels'); + cy.wrap(el).should('contain', 'Channel Type: Archived'); }).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').clear(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); @@ -250,7 +250,7 @@ function verifyMoreChannelsModal(isEnabled) { // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Show: Public Channels'); + cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Channel Type: Public'); } else { cy.get('#channelsMoreDropdown').should('not.exist'); } From 1bbfdefad73d12b44c6b1d14d219696c54c184c8 Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Wed, 19 Apr 2023 16:20:17 +0530 Subject: [PATCH 06/69] MM-49603 : Remove fetching of deleted channels on page load (#22981) - channel request types removed from fetchMyChannelsAndMembersREST - removed isMinimumServerVersion check from that above action call, which is adding the query to include the archived channels --- .../mattermost-redux/src/actions/channels.ts | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) 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 f5afb85aa2..f31c122014 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts @@ -4,6 +4,18 @@ 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'; @@ -19,18 +31,12 @@ import { getRedirectChannelNameForTeam, isManuallyUnread, } from 'mattermost-redux/selectors/entities/channels'; -import {getConfig, getServerVersion} from 'mattermost-redux/selectors/entities/general'; +import {getConfig} 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 {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 {getChannelByName} from 'mattermost-redux/utils/channel_utils'; import {General, Preferences} from '../constants'; @@ -462,52 +468,33 @@ export function getChannelTimezones(channelId: string): ActionFunc { }; } -export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc { +export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc<{channels: ServerChannel[]; channelMembers: ChannelMembership[]}> { 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, shouldFetchArchived), + Client4.getMyChannels(teamId), Client4.getMyChannelMembers(teamId), ]); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); - dispatch({type: ChannelTypes.CHANNELS_FAILURE, error}); dispatch(logError(error)); - return {error}; + return {error: error as ServerError}; } - 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(' ')) { @@ -518,7 +505,7 @@ export function fetchMyChannelsAndMembersREST(teamId: string): ActionFunc { dispatch(loadRolesIfNeeded(roles)); } - return {data: {channels, members: channelMembers}}; + return {data: {channels, channelMembers}}; }; } From bab2620b59c2f210d3078bcfddb20f999956b720 Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Wed, 19 Apr 2023 16:21:10 +0530 Subject: [PATCH 07/69] MM-50123 : Identify causes of removal of channel and channel members re fetching on team switch (#22984) --- .../channels/src/actions/channel_actions.ts | 21 ++++++++----------- .../team_controller/actions/index.ts | 17 +-------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/webapp/channels/src/actions/channel_actions.ts b/webapp/channels/src/actions/channel_actions.ts index b3c9ffb156..c800d0fda1 100644 --- a/webapp/channels/src/actions/channel_actions.ts +++ b/webapp/channels/src/actions/channel_actions.ts @@ -254,25 +254,22 @@ export function fetchChannelsAndMembers(teamId: Team['id'] = ''): ActionFunc<{ch teamId, data: channels, }); - actions.push({ - type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, - data: channelMembers, - }); - actions.push({ - type: RoleTypes.RECEIVED_ROLES, - data: roles, - }); } else { actions.push({ type: ChannelTypes.RECEIVED_ALL_CHANNELS, data: channels, }); - actions.push({ - type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, - data: channelMembers, - }); } + actions.push({ + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, + data: channelMembers, + }); + actions.push({ + type: RoleTypes.RECEIVED_ROLES, + data: roles, + }); + await dispatch(batchActions(actions)); return {data: {channels, channelMembers, roles}}; diff --git a/webapp/channels/src/components/team_controller/actions/index.ts b/webapp/channels/src/components/team_controller/actions/index.ts index 38977f2678..e1717d0dbc 100644 --- a/webapp/channels/src/components/team_controller/actions/index.ts +++ b/webapp/channels/src/components/team_controller/actions/index.ts @@ -4,10 +4,9 @@ import {ActionFunc} from 'mattermost-redux/types/actions'; import {getTeamByName, selectTeam} from 'mattermost-redux/actions/teams'; import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers'; -import {fetchMyChannelsAndMembersREST} from 'mattermost-redux/actions/channels'; import {getGroups, getAllGroupsAssociatedToChannelsInTeam, getAllGroupsAssociatedToTeam, getGroupsByUserIdPaginated} from 'mattermost-redux/actions/groups'; import {logError} from 'mattermost-redux/actions/errors'; -import {isCustomGroupsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; @@ -15,7 +14,6 @@ import {isSuccess} from 'types/actions'; import {loadStatusesForChannelAndSidebar} from 'actions/status_actions'; import {addUserToTeam} from 'actions/team_actions'; -import {fetchChannelsAndMembers} from 'actions/channel_actions'; import LocalStorageStore from 'stores/local_storage_store'; @@ -30,19 +28,6 @@ export function initializeTeam(team: Team): ActionFunc { const currentUser = getCurrentUser(state); LocalStorageStore.setPreviousTeamId(currentUser.id, team.id); - const graphQLEnabled = isGraphQLEnabled(state); - try { - if (graphQLEnabled) { - await dispatch(fetchChannelsAndMembers(team.id)); - } else { - await dispatch(fetchMyChannelsAndMembersREST(team.id)); - } - } catch (error) { - forceLogoutIfNecessary(error as ServerError, dispatch, getState); - dispatch(logError(error as ServerError)); - return {error: error as ServerError}; - } - dispatch(loadStatusesForChannelAndSidebar()); const license = getLicense(state); From 7216dcf367d16f8e1a30d22e027c8ce2f17e47c2 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Wed, 19 Apr 2023 17:03:18 +0530 Subject: [PATCH 08/69] MM-50427: Make MM survive DB replica outage (#22888) We monitor the health of DB replicas, and on a fatal error, take them out of the pool. On a separate goroutine, we keep pinging the unhealthy replicas, and on getting a good response back, we add them back to the pool. https://mattermost.atlassian.net/browse/MM-50427 ```release-note Mattermost is now resilient against DB replica outages and will dynamically choose a replica if it's alive. Also added a config parameter ReplicaMonitorIntervalSeconds whose default value is 5. This controls how frequently unhealthy replicas will be monitored for liveness check. ``` Co-authored-by: Mattermost Build --- .../boards/services/store/sqlstore/migrate.go | 5 +- server/channels/einterfaces/metrics.go | 1 + .../einterfaces/mocks/MetricsInterface.go | 5 + .../channels/store/sqlstore/sqlx_wrapper.go | 73 ++++-- .../store/sqlstore/sqlx_wrapper_test.go | 11 +- server/channels/store/sqlstore/store.go | 225 ++++++++++++------ server/channels/store/sqlstore/store_test.go | 18 +- server/channels/store/store.go | 3 - .../channels/store/storetest/mocks/Store.go | 16 -- server/channels/store/storetest/settings.go | 1 + server/channels/testlib/helper.go | 2 +- server/model/config.go | 5 + .../platform/services/telemetry/telemetry.go | 1 + 13 files changed, 245 insertions(+), 121 deletions(-) diff --git a/server/boards/services/store/sqlstore/migrate.go b/server/boards/services/store/sqlstore/migrate.go index 1c876a5168..63b4665490 100644 --- a/server/boards/services/store/sqlstore/migrate.go +++ b/server/boards/services/store/sqlstore/migrate.go @@ -70,7 +70,10 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) { } *settings.DriverName = s.dbType - db := sqlstore.SetupConnection("master", connectionString, &settings) + db, err := sqlstore.SetupConnection("master", connectionString, &settings, sqlstore.DBPingAttempts) + if err != nil { + return nil, err + } return db, nil } diff --git a/server/channels/einterfaces/metrics.go b/server/channels/einterfaces/metrics.go index 06f44f7b66..c44af2a3c5 100644 --- a/server/channels/einterfaces/metrics.go +++ b/server/channels/einterfaces/metrics.go @@ -13,6 +13,7 @@ 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 0d6f799ee5..06f568546a 100644 --- a/server/channels/einterfaces/mocks/MetricsInterface.go +++ b/server/channels/einterfaces/mocks/MetricsInterface.go @@ -319,6 +319,11 @@ 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/sqlstore/sqlx_wrapper.go b/server/channels/store/sqlstore/sqlx_wrapper.go index 0dab579512..e8d771cada 100644 --- a/server/channels/store/sqlstore/sqlx_wrapper.go +++ b/server/channels/store/sqlstore/sqlx_wrapper.go @@ -6,9 +6,12 @@ package sqlstore import ( "context" "database/sql" + "errors" + "net" "regexp" "strconv" "strings" + "sync/atomic" "time" "unicode" @@ -66,14 +69,18 @@ type sqlxDBWrapper struct { *sqlx.DB queryTimeout time.Duration trace bool + isOnline *atomic.Bool } func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper { - return &sqlxDBWrapper{ + w := &sqlxDBWrapper{ DB: db, queryTimeout: timeout, trace: trace, + isOnline: &atomic.Bool{}, } + w.isOnline.Store(true) + return w } func (w *sqlxDBWrapper) Stats() sql.DBStats { @@ -83,19 +90,19 @@ func (w *sqlxDBWrapper) Stats() sql.DBStats { func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) { tx, err := w.DB.Beginx() if err != nil { - return nil, err + return nil, w.checkErr(err) } - return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil + return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil } func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) { tx, err := w.DB.BeginTxx(context.Background(), opts) if err != nil { - return nil, err + return nil, w.checkErr(err) } - return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil + return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil } func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error { @@ -109,7 +116,7 @@ func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error { }(time.Now()) } - return w.DB.GetContext(ctx, dest, query, args...) + return w.checkErr(w.DB.GetContext(ctx, dest, query, args...)) } func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error { @@ -134,7 +141,7 @@ func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) { }(time.Now()) } - return w.DB.NamedExecContext(ctx, query, arg) + return w.checkErrWithResult(w.DB.NamedExecContext(ctx, query, arg)) } func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) { @@ -161,7 +168,7 @@ func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er }(time.Now()) } - return w.DB.ExecContext(context.Background(), query, args...) + return w.checkErrWithResult(w.DB.ExecContext(context.Background(), query, args...)) } // ExecRaw is like Exec but without any rebinding of params. You need to pass @@ -176,7 +183,7 @@ func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) { }(time.Now()) } - return w.DB.ExecContext(ctx, query, args...) + return w.checkErrWithResult(w.DB.ExecContext(ctx, query, args...)) } func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { @@ -192,7 +199,7 @@ func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { }(time.Now()) } - return w.DB.NamedQueryContext(ctx, query, arg) + return w.checkErrWithRows(w.DB.NamedQueryContext(ctx, query, arg)) } func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row { @@ -220,7 +227,7 @@ func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) { }(time.Now()) } - return w.DB.QueryxContext(ctx, query, args) + return w.checkErrWithRows(w.DB.QueryxContext(ctx, query, args)) } func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error { @@ -238,7 +245,7 @@ func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, a }(time.Now()) } - return w.DB.SelectContext(ctx, dest, query, args...) + return w.checkErr(w.DB.SelectContext(ctx, dest, query, args...)) } func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error { @@ -254,13 +261,15 @@ type sqlxTxWrapper struct { *sqlx.Tx queryTimeout time.Duration trace bool + dbw *sqlxDBWrapper } -func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper { +func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool, dbw *sqlxDBWrapper) *sqlxTxWrapper { return &sqlxTxWrapper{ Tx: tx, queryTimeout: timeout, trace: trace, + dbw: dbw, } } @@ -275,7 +284,7 @@ func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error { }(time.Now()) } - return w.Tx.GetContext(ctx, dest, query, args...) + return w.dbw.checkErr(w.Tx.GetContext(ctx, dest, query, args...)) } func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error { @@ -284,13 +293,13 @@ func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error { return err } - return w.Get(dest, query, args...) + return w.dbw.checkErr(w.Get(dest, query, args...)) } func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) { query = w.Tx.Rebind(query) - return w.ExecRaw(query, args...) + return w.dbw.checkErrWithResult(w.ExecRaw(query, args...)) } func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) { @@ -302,7 +311,7 @@ func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er }(time.Now()) } - return w.Tx.ExecContext(context.Background(), query, args...) + return w.dbw.checkErrWithResult(w.Tx.ExecContext(context.Background(), query, args...)) } func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) { @@ -326,7 +335,7 @@ func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) { }(time.Now()) } - return w.Tx.ExecContext(ctx, query, args...) + return w.dbw.checkErrWithResult(w.Tx.ExecContext(ctx, query, args...)) } func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) { @@ -342,7 +351,7 @@ func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) { }(time.Now()) } - return w.Tx.NamedExecContext(ctx, query, arg) + return w.dbw.checkErrWithResult(w.Tx.NamedExecContext(ctx, query, arg)) } func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { @@ -386,7 +395,7 @@ func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) { } } - return res.rows, res.err + return res.rows, w.dbw.checkErr(res.err) } func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row { @@ -414,7 +423,7 @@ func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) { }(time.Now()) } - return w.Tx.QueryxContext(ctx, query, args) + return w.dbw.checkErrWithRows(w.Tx.QueryxContext(ctx, query, args)) } func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error { @@ -428,7 +437,7 @@ func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error { }(time.Now()) } - return w.Tx.SelectContext(ctx, dest, query, args...) + return w.dbw.checkErr(w.Tx.SelectContext(ctx, dest, query, args...)) } func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error { @@ -459,3 +468,23 @@ 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 07c6391767..c03d228935 100644 --- a/server/channels/store/sqlstore/sqlx_wrapper_test.go +++ b/server/channels/store/sqlstore/sqlx_wrapper_test.go @@ -6,6 +6,7 @@ package sqlstore import ( "context" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -28,12 +29,14 @@ func TestSqlX(t *testing.T) { } *settings.QueryTimeout = 1 store := &SqlStore{ - rrCounter: 0, - srCounter: 0, - settings: settings, + rrCounter: 0, + srCounter: 0, + settings: settings, + quitMonitor: make(chan struct{}), + wgMonitor: &sync.WaitGroup{}, } - store.initConnection() + require.NoError(t, store.initConnection()) defer store.Close() diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index d39f92661c..acd02b0853 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 = 18 + DBPingAttempts = 5 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 []*sqlxDBWrapper + ReplicaXs []*atomic.Pointer[sqlxDBWrapper] - searchReplicaXs []*sqlxDBWrapper + searchReplicaXs []*atomic.Pointer[sqlxDBWrapper] replicaLagHandles []*dbsql.DB stores SqlStoreStores @@ -138,17 +138,28 @@ 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, + rrCounter: 0, + srCounter: 0, + settings: &settings, + metrics: metrics, + quitMonitor: make(chan struct{}), + wgMonitor: &sync.WaitGroup{}, } - store.initConnection() + err := store.initConnection() + if err != nil { + mlog.Fatal("Error setting up connections", mlog.Err(err)) + } + + store.wgMonitor.Add(1) + go store.monitorReplicas() ver, err := store.GetDbVersion(true) if err != nil { @@ -230,29 +241,28 @@ 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) *dbsql.DB { +func SetupConnection(connType string, dataSource string, settings *model.SqlSettings, attempts int) (*dbsql.DB, error) { db, err := dbsql.Open(*settings.DriverName, dataSource) if err != nil { - mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err)) + return nil, errors.Wrap(err, "failed to open SQL connection") } - for i := 0; i < DBPingAttempts; i++ { + for i := 0; i < attempts; 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 { - 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) + if err != nil { + if i == attempts-1 { + return nil, err } + 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) { @@ -272,7 +282,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 + return db, nil } func (ss *SqlStore) SetContext(context context.Context) { @@ -285,7 +295,7 @@ func (ss *SqlStore) Context() context.Context { func noOpMapper(s string) string { return s } -func (ss *SqlStore) initConnection() { +func (ss *SqlStore) initConnection() error { dataSource := *ss.settings.DataSource if ss.DriverName() == model.DatabaseDriverMysql { // TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout @@ -294,11 +304,14 @@ func (ss *SqlStore) initConnection() { var err error dataSource, err = ResetReadTimeout(dataSource) if err != nil { - mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource)) + return errors.Wrap(err, "failed to reset read timeout from datasource") } } - handle := SetupConnection("master", dataSource, ss.settings) + handle, err := SetupConnection("master", dataSource, ss.settings, DBPingAttempts) + if err != nil { + return err + } ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), time.Duration(*ss.settings.QueryTimeout)*time.Second, *ss.settings.Trace) @@ -310,34 +323,32 @@ func (ss *SqlStore) initConnection() { } if len(ss.settings.DataSourceReplicas) > 0 { - ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas)) + ss.ReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceReplicas)) for i, replica := range ss.settings.DataSourceReplicas { - 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.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 } + ss.setDB(ss.ReplicaXs[i], handle, "replica-"+strconv.Itoa(i)) } } if len(ss.settings.DataSourceSearchReplicas) > 0 { - ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas)) + ss.searchReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceSearchReplicas)) for i, replica := range ss.settings.DataSourceSearchReplicas { - 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.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 } + ss.setDB(ss.searchReplicaXs[i], handle, "searchreplica-"+strconv.Itoa(i)) } } @@ -347,9 +358,14 @@ func (ss *SqlStore) initConnection() { if src.DataSource == nil { continue } - ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings) + 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 + } } } + return nil } func (ss *SqlStore) DriverName() string { @@ -455,8 +471,15 @@ func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper { return ss.GetReplicaX() } - rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs)) - return ss.searchReplicaXs[rrNum] + 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() } func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper { @@ -464,23 +487,64 @@ func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper { return ss.GetMasterX() } - 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, + 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() } } - dbs := make([]*sql.DB, len(ss.ReplicaXs)) - for i, rx := range ss.ReplicaXs { - dbs[i] = rx.DB.DB - } + // If all replicas are down, then go with master. + return ss.GetMasterX() +} - return dbs +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)) + } + } + } +} + +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) + } } func (ss *SqlStore) GetInternalReplicaDB() *sql.DB { @@ -489,7 +553,7 @@ func (ss *SqlStore) GetInternalReplicaDB() *sql.DB { } rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs)) - return ss.ReplicaXs[rrNum].DB.DB + return ss.ReplicaXs[rrNum].Load().DB.DB } func (ss *SqlStore) TotalMasterDbConnections() int { @@ -541,7 +605,10 @@ func (ss *SqlStore) TotalReadDbConnections() int { count := 0 for _, db := range ss.ReplicaXs { - count = count + db.Stats().OpenConnections + if !db.Load().Online() { + continue + } + count = count + db.Load().Stats().OpenConnections } return count @@ -554,7 +621,10 @@ func (ss *SqlStore) TotalSearchDbConnections() int { count := 0 for _, db := range ss.searchReplicaXs { - count = count + db.Stats().OpenConnections + if !db.Load().Online() { + continue + } + count = count + db.Load().Stats().OpenConnections } return count @@ -782,9 +852,14 @@ func IsUniqueConstraintError(err error, indexName []string) bool { } func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper { - all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1) - copy(all, ss.ReplicaXs) - all[len(ss.ReplicaXs)] = ss.masterX + 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) return all } @@ -807,11 +882,24 @@ 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 { - replica.Close() + if replica.Load().Online() { + replica.Load().Close() + } } for _, replica := range ss.searchReplicaXs { + if replica.Load().Online() { + replica.Load().Close() + } + } + + for _, replica := range ss.replicaLagHandles { replica.Close() } } @@ -1132,7 +1220,10 @@ func (ss *SqlStore) migrate(direction migrationDirection) error { if err != nil { return err } - db := SetupConnection("master", dataSource, ss.settings) + db, err2 := SetupConnection("master", dataSource, ss.settings, DBPingAttempts) + if err2 != nil { + return err2 + } 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 c218fa205d..699ee53e98 100644 --- a/server/channels/store/sqlstore/store_test.go +++ b/server/channels/store/sqlstore/store_test.go @@ -761,13 +761,15 @@ func TestReplicaLagQuery(t *testing.T) { mockMetrics.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master") store := &SqlStore{ - rrCounter: 0, - srCounter: 0, - settings: settings, - metrics: mockMetrics, + rrCounter: 0, + srCounter: 0, + settings: settings, + metrics: mockMetrics, + quitMonitor: make(chan struct{}), + wgMonitor: &sync.WaitGroup{}, } - store.initConnection() + require.NoError(t, store.initConnection()) store.stores.post = newSqlPostStore(store, mockMetrics) err = store.migrate(migrationsDirectionUp) require.NoError(t, err) @@ -839,9 +841,11 @@ func TestMySQLReadTimeout(t *testing.T) { settings.DataSource = &dataSource store := &SqlStore{ - settings: settings, + settings: settings, + quitMonitor: make(chan struct{}), + wgMonitor: &sync.WaitGroup{}, } - store.initConnection() + require.NoError(t, store.initConnection()) defer store.Close() _, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`) diff --git a/server/channels/store/store.go b/server/channels/store/store.go index 7da24fd24c..20af689736 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -72,10 +72,7 @@ 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 diff --git a/server/channels/store/storetest/mocks/Store.go b/server/channels/store/storetest/mocks/Store.go index bb06fb9005..bca15c95e0 100644 --- a/server/channels/store/storetest/mocks/Store.go +++ b/server/channels/store/storetest/mocks/Store.go @@ -346,22 +346,6 @@ 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/settings.go b/server/channels/store/storetest/settings.go index a1253f28bb..0104b950bb 100644 --- a/server/channels/store/storetest/settings.go +++ b/server/channels/store/storetest/settings.go @@ -261,6 +261,7 @@ 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/testlib/helper.go b/server/channels/testlib/helper.go index f74a562568..f6a1b22531 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.Exec(query, args...) + _, err := replica.Load().Exec(query, args...) if err != nil { return err } diff --git a/server/model/config.go b/server/model/config.go index f278c97cdf..af4341bdfa 100644 --- a/server/model/config.go +++ b/server/model/config.go @@ -1173,6 +1173,7 @@ 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) { @@ -1237,6 +1238,10 @@ 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/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index d4da4770bc..4fdbdf51ec 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -522,6 +522,7 @@ 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{ From e1bad44e85a68f128b8ef4336468e34f95ca34d5 Mon Sep 17 00:00:00 2001 From: Kyriakos Z <3829551+koox00@users.noreply.github.com> Date: Wed, 19 Apr 2023 15:20:34 +0300 Subject: [PATCH 09/69] MM-45009: Delete ThreadMemberships from "left" channels (#22559) * MM-50550: Filter out threads from "left" channels v2 Currently leaving a channel doesn't affect the thread memberships of that user/channel combination. This PR aims to filter out all threads from those channels for the user. Adds a DeleteAt column in the ThreadMemberships table, and filter out all thread memberships that are "deleted". Each time a user leaves a channel all thread memberships are going to be marked as deleted, and when a user joins a channel again all those existing thread memberships will be re-instantiated. Adds a migration to mark all existing thread memberships as deleted depending on whether there exists a channel membership for that channel/user. * Added migration files into list * Fixes tests * Fixes case where DeleteAt would be null * Guard thread API endpoints with appropriate perms * Deletes ThreadMembership rows upon leaving channel * Minor style changes * Use NoTranslation error * Refactors tests * Adds API tests to assert permissions on Team * Adds tests, and fixes migrations * Fixes test description * Fix test * Removes check on DM/GMs * Change the MySQL query in the migration --------- Co-authored-by: Mattermost Build --- 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/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 ++ .../opentracinglayer/opentracinglayer.go | 18 +++ .../channels/store/retrylayer/retrylayer.go | 21 ++++ .../channels/store/sqlstore/thread_store.go | 40 +++++- server/channels/store/store.go | 1 + .../store/storetest/mocks/ThreadStore.go | 14 +++ .../channels/store/storetest/thread_store.go | 119 ++++++++++++++++++ .../channels/store/timerlayer/timerlayer.go | 16 +++ 16 files changed, 426 insertions(+), 38 deletions(-) create mode 100644 server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql create mode 100644 server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql create mode 100644 server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql create mode 100644 server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index c259922e82..2269efe88c 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -3106,6 +3106,10 @@ 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) @@ -3136,6 +3140,10 @@ 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, @@ -3213,6 +3221,10 @@ 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 { @@ -3279,6 +3291,10 @@ 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 { @@ -3338,6 +3354,10 @@ 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 8d9673ed9e..0f9ce87d33 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -6360,6 +6360,15 @@ 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) { @@ -6855,52 +6864,64 @@ 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) - // 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}) + 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 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 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), + }, }, - }, - }) - 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 + 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) }) - 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) + 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) - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.PostPriority = true - cfg.FeatureFlags.PostPriority = true + _, resp, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), false) + require.Error(t, err) + CheckForbiddenStatus(t, resp) }) - - 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) { @@ -7072,6 +7093,23 @@ 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 09fb2ce1cc..162254b84b 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -2518,6 +2518,9 @@ 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 eaf1171a6c..2b427ba806 100644 --- a/server/channels/app/channel_test.go +++ b/server/channels/app/channel_test.go @@ -609,6 +609,85 @@ 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/db/migrations/migrations.list b/server/channels/db/migrations/migrations.list index 6a7d33d5c6..47f5bf333b 100644 --- a/server/channels/db/migrations/migrations.list +++ b/server/channels/db/migrations/migrations.list @@ -212,6 +212,8 @@ 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 @@ -424,3 +426,5 @@ 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 new file mode 100644 index 0000000000..4743bd6462 --- /dev/null +++ b/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql @@ -0,0 +1 @@ +-- 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 new file mode 100644 index 0000000000..90644be3f3 --- /dev/null +++ b/server/channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000000..4743bd6462 --- /dev/null +++ b/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql @@ -0,0 +1 @@ +-- 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 new file mode 100644 index 0000000000..0ec82905bc --- /dev/null +++ b/server/channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql @@ -0,0 +1,12 @@ +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/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 941704a2f4..66a5051260 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -10123,6 +10123,24 @@ 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 91a3209c44..b39c79ab9b 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -11563,6 +11563,27 @@ 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/thread_store.go b/server/channels/store/sqlstore/thread_store.go index b731b0b71c..66ce1f42a1 100644 --- a/server/channels/store/sqlstore/thread_store.go +++ b/server/channels/store/sqlstore/thread_store.go @@ -688,6 +688,28 @@ 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"). @@ -712,7 +734,14 @@ func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model. memberships := []*model.ThreadMembership{} query := s.getQueryBuilder(). - Select("ThreadMemberships.*"). + Select( + "ThreadMemberships.PostId", + "ThreadMemberships.UserId", + "ThreadMemberships.Following", + "ThreadMemberships.LastUpdated", + "ThreadMemberships.LastViewed", + "ThreadMemberships.UnreadMentions", + ). Join("Threads ON Threads.PostId = ThreadMemberships.PostId"). From("ThreadMemberships"). Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}). @@ -732,7 +761,14 @@ 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("*"). + Select( + "PostId", + "UserId", + "Following", + "LastUpdated", + "LastViewed", + "UnreadMentions", + ). From("ThreadMemberships"). Where(sq.And{ sq.Eq{"PostId": postId}, diff --git a/server/channels/store/store.go b/server/channels/store/store.go index 20af689736..cd813239d4 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -344,6 +344,7 @@ 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/mocks/ThreadStore.go b/server/channels/store/storetest/mocks/ThreadStore.go index 60b9211db2..661194a935 100644 --- a/server/channels/store/storetest/mocks/ThreadStore.go +++ b/server/channels/store/storetest/mocks/ThreadStore.go @@ -29,6 +29,20 @@ 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/thread_store.go b/server/channels/store/storetest/thread_store.go index 4cd64c8f1e..efbc74d3ac 100644 --- a/server/channels/store/storetest/thread_store.go +++ b/server/channels/store/storetest/thread_store.go @@ -29,6 +29,7 @@ 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) { @@ -1914,3 +1915,121 @@ 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 b52293e013..3dc9a94c19 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -9112,6 +9112,22 @@ 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() From b073ab2e3ad734196316ec2338961a2fd572adca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20V=C3=A9lez=20Vidal?= Date: Wed, 19 Apr 2023 15:31:47 +0200 Subject: [PATCH 10/69] MM 50960 - Add company name and invite members pages to preparing-workspace for self-hosted (#22838) * MM-50960 - store system organization name * restore the preparing workspace plugins and invite screens * add back the page lines for the design * add lines back and organize styles * set back documentation to monorepo style and disable board as a product * fix organization link and style skip button * create team on organization name screen continue button click * make sure there are not already created team and if so just update team name * update the team display name if team has already been created * cover error scenarios during team creation * add pr feedback and add a couple of unit tests * fix translation server error; make sure only update display name if it has changed in the form * temp advances * rewrite unit tests using react-testing library; fix unit tests * fix translations * make sure the launching workspace finish in cloud installations * remove redundant validation * fix unit tests * remove unintended config value left after merge conflict --- .../support/server/default_config.ts | 1 - server/channels/api4/system_test.go | 1 + server/channels/app/onboarding.go | 18 + server/channels/app/onboarding_test.go | 30 + server/i18n/en.json | 4 + server/model/onboarding.go | 1 + server/model/system.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 - .../channels/src/components/login/login.tsx | 7 +- .../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 +- webapp/channels/src/i18n/en.json | 25 +- .../src/selectors/entities/preferences.ts | 4 - webapp/channels/src/utils/constants.tsx | 1 + webapp/platform/types/src/setup.ts | 1 + 47 files changed, 2248 insertions(+), 107 deletions(-) create mode 100644 server/channels/app/onboarding_test.go create mode 100644 webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap create mode 100644 webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap create mode 100644 webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members.scss create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members.test.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_link.scss create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/invite_members_link.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/mixins.scss create mode 100644 webapp/channels/src/components/preparing_workspace/organization.scss create mode 100644 webapp/channels/src/components/preparing_workspace/organization.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/organization_status.test.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/organization_status.tsx create mode 100644 webapp/channels/src/components/preparing_workspace/page_line.scss create mode 100644 webapp/channels/src/components/preparing_workspace/page_line.tsx diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts index 2d8cb12c58..3e8e5ed881 100644 --- a/e2e-tests/playwright/support/server/default_config.ts +++ b/e2e-tests/playwright/support/server/default_config.ts @@ -665,7 +665,6 @@ const defaultServerConfig: AdminConfig = { BoardsFeatureFlags: '', BoardsDataRetention: false, NormalizeLdapDNs: false, - UseCaseOnboarding: true, GraphQL: false, InsightsEnabled: true, CommandPalette: false, diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index 5921e32802..25574e4400 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -892,6 +892,7 @@ 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/app/onboarding.go b/server/channels/app/onboarding.go index 2dd85749d9..3b76aefe53 100644 --- a/server/channels/app/onboarding.go +++ b/server/channels/app/onboarding.go @@ -28,6 +28,24 @@ 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 new file mode 100644 index 0000000000..cf8462cf28 --- /dev/null +++ b/server/channels/app/onboarding_test.go @@ -0,0 +1,30 @@ +// 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/i18n/en.json b/server/i18n/en.json index 598462a448..40f626291f 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -1777,6 +1777,10 @@ "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/onboarding.go b/server/model/onboarding.go index 797bea7c1d..0fe5e91ffa 100644 --- a/server/model/onboarding.go +++ b/server/model/onboarding.go @@ -10,6 +10,7 @@ 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 fbc2aaa684..24b4fce9c9 100644 --- a/server/model/system.go +++ b/server/model/system.go @@ -16,6 +16,7 @@ const ( SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey" SystemPostActionCookieSecretKey = "PostActionCookieSecret" SystemInstallationDateKey = "InstallationDate" + SystemOrganizationName = "OrganizationName" SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp" SystemClusterEncryptionKey = "ClusterEncryptionKey" SystemUpgradedFromTeId = "UpgradedFromTE" diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index cb22542fa4..6ac09280ab 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} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUser, getCurrentUserId, isFirstAdmin} 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,11 +367,19 @@ 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 7b03d9e7d5..0e81ead2c7 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,7 +6,6 @@ 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'; @@ -15,7 +14,6 @@ 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'; @@ -40,7 +38,6 @@ 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(''); @@ -52,16 +49,11 @@ const DoVerifyEmail = () => { const handleRedirect = () => { if (loggedIn) { - 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(); + // 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; } 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 a59ff532cc..5a2ac01c35 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,7 +8,6 @@ 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'; @@ -32,7 +31,6 @@ 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 effe92c1ad..aa1ac2e833 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,7 +34,6 @@ describe('components/channel_header/components/UserGuideDropdown', () => { }, pluginMenuItems: [], isFirstAdmin: false, - useCaseOnboarding: false, }; test('should match snapshot', () => { diff --git a/webapp/channels/src/components/login/login.tsx b/webapp/channels/src/components/login/login.tsx index bed62d7eed..c0e154e561 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 {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {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,7 +104,6 @@ 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); @@ -631,14 +630,12 @@ const Login = ({onCustomizeHeader}: LoginProps) => { } else if (experimentalPrimaryTeamMember.team_id) { // Only set experimental team if user is on that team history.push(`/${ExperimentalPrimaryTeam}`); - } else if (useCaseOnboarding) { + } else { // 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 new file mode 100644 index 0000000000..4aa2442f0a --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members.test.tsx.snap @@ -0,0 +1,80 @@ +// 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 new file mode 100644 index 0000000000..0610646391 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/invite_members_link.test.tsx.snap @@ -0,0 +1,29 @@ +// 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 new file mode 100644 index 0000000000..cec545b0bd --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/__snapshots__/organization_status.test.tsx.snap @@ -0,0 +1,7 @@ +// 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 a3ab4aa606..c454fd28ac 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} from 'mattermost-redux/actions/teams'; +import {checkIfTeamExists, createTeam, updateTeam} from 'mattermost-redux/actions/teams'; import {getProfiles} from 'mattermost-redux/actions/users'; import PreparingWorkspace, {Actions} from './preparing_workspace'; @@ -13,6 +13,7 @@ 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 new file mode 100644 index 0000000000..dc914d42b1 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members.scss @@ -0,0 +1,51 @@ +@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 new file mode 100644 index 0000000000..54fe45f374 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members.test.tsx @@ -0,0 +1,71 @@ +// 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 new file mode 100644 index 0000000000..a018c2a446 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members.tsx @@ -0,0 +1,114 @@ +// 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 new file mode 100644 index 0000000000..26b28e9b6f --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members_illustration.tsx @@ -0,0 +1,838 @@ +// 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 new file mode 100644 index 0000000000..09b229f264 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members_link.scss @@ -0,0 +1,51 @@ +.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 new file mode 100644 index 0000000000..d74b81d493 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members_link.test.tsx @@ -0,0 +1,61 @@ +// 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 new file mode 100644 index 0000000000..f6491809ca --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/invite_members_link.tsx @@ -0,0 +1,64 @@ +// 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 new file mode 100644 index 0000000000..b3ca03bce8 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/mixins.scss @@ -0,0 +1,12 @@ +@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 new file mode 100644 index 0000000000..c063010404 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/organization.scss @@ -0,0 +1,63 @@ +@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 new file mode 100644 index 0000000000..684c6dc4d9 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/organization.tsx @@ -0,0 +1,206 @@ +// 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 new file mode 100644 index 0000000000..e7d65bfd6b --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/organization_status.test.tsx @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 0000000000..d695a2ad26 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/organization_status.tsx @@ -0,0 +1,83 @@ +// 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 new file mode 100644 index 0000000000..12801e1f67 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/page_line.scss @@ -0,0 +1,10 @@ +.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 new file mode 100644 index 0000000000..ebbb9ee024 --- /dev/null +++ b/webapp/channels/src/components/preparing_workspace/page_line.tsx @@ -0,0 +1,35 @@ +// 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 fa74dc5718..0a5465564e 100644 --- a/webapp/channels/src/components/preparing_workspace/plugins.scss +++ b/webapp/channels/src/components/preparing_workspace/plugins.scss @@ -4,6 +4,9 @@ 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 b3b1168015..caf04e794e 100644 --- a/webapp/channels/src/components/preparing_workspace/plugins.tsx +++ b/webapp/channels/src/components/preparing_workspace/plugins.tsx @@ -21,15 +21,16 @@ 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(); @@ -44,6 +45,34 @@ const Plugins = (props: Props) => { if (props.className) { className += ' ' + props.className; } + + let title = ( + + ); + let description = ( + + ); + if (props.isSelfHosted) { + title = ( + + ); + description = ( + + ); + } + return ( { >
+ {props.previous} - <FormattedMessage - id={'onboarding_wizard.plugins.title'} - defaultMessage='Welcome to Mattermost!' - /> - <div className='subtitle'> - <CelebrateSVG/> - <FormattedMessage - id={'onboarding_wizard.plugins.subtitle'} - defaultMessage='(almost there!)' - /> - </div> + {title} + {!props.isSelfHosted && ( + <div className='subtitle'> + <CelebrateSVG/> + <FormattedMessage + id={'onboarding_wizard.cloud_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 c91dd0a1fe..99187c301b 100644 --- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss +++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.scss @@ -63,6 +63,21 @@ .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 { @@ -70,6 +85,43 @@ 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 d554090d68..268e21c55e 100644 --- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx +++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx @@ -1,23 +1,24 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useState, useCallback, useEffect, useRef} from 'react'; +import React, {useState, useCallback, useEffect, useRef, useMemo} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {RouterProps} from 'react-router-dom'; -import {useIntl} from 'react-intl'; +import {FormattedMessage, 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} from 'mattermost-redux/selectors/entities/general'; +import {getFirstAdminSetupComplete, getConfig, getLicense} 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'; @@ -35,10 +36,14 @@ 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'; @@ -58,6 +63,7 @@ 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; } @@ -81,12 +87,16 @@ 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), }; @@ -98,28 +108,35 @@ 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. - const team = currentTeam || myTeams?.[0]; + let 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, }); @@ -188,13 +205,44 @@ 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), [], @@ -204,8 +252,10 @@ 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}); @@ -221,6 +271,7 @@ const PreparingWorkspace = (props: Props) => { const sendFormEnd = Date.now(); const timeToWait = WAIT_FOR_REDIRECT_TIME - (sendFormEnd - sendFormStart); + if (timeToWait > 0) { setTimeout(goToChannels, timeToWait); } else { @@ -236,7 +287,8 @@ const PreparingWorkspace = (props: Props) => { }, [submissionState]); const adminRevisitedPage = firstAdminSetupComplete && submissionState === SubmissionStates.Presubmit; - const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage || !useCaseOnboarding; + const shouldRedirect = !isUserFirstAdmin || adminRevisitedPage; + useEffect(() => { if (shouldRedirect) { props.history.push('/'); @@ -256,6 +308,24 @@ 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; @@ -269,6 +339,46 @@ 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 && ( @@ -291,17 +401,49 @@ 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; - setSubmissionState(SubmissionStates.UserRequested); + if (!isSelfHosted) { + setSubmissionState(SubmissionStates.UserRequested); + } makeNext(WizardSteps.Plugins)(pluginChoices); skipPlugins(false); }} skip={() => { - setSubmissionState(SubmissionStates.UserRequested); + if (!isSelfHosted) { + setSubmissionState(SubmissionStates.UserRequested); + } makeNext(WizardSteps.Plugins, true)(); skipPlugins(true); }} @@ -319,12 +461,40 @@ 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 357faabdf4..afff27dcfe 100644 --- a/webapp/channels/src/components/preparing_workspace/single_column_layout.scss +++ b/webapp/channels/src/components/preparing_workspace/single_column_layout.scss @@ -4,5 +4,4 @@ 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 ed52d984af..cbb78da5b6 100644 --- a/webapp/channels/src/components/preparing_workspace/steps.ts +++ b/webapp/channels/src/components/preparing_workspace/steps.ts @@ -4,7 +4,9 @@ import deepFreeze from 'mattermost-redux/utils/deep_freeze'; export const WizardSteps = { + Organization: 'Organization', Plugins: 'Plugins', + InviteMembers: 'InviteMembers', LaunchingWorkspace: 'LaunchingWorkspace', } as const; @@ -20,8 +22,12 @@ 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: @@ -31,8 +37,12 @@ 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: @@ -42,8 +52,12 @@ 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: @@ -53,8 +67,12 @@ 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: @@ -64,8 +82,12 @@ 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: @@ -128,12 +150,14 @@ export type Form = { skipped: boolean; }; teamMembers: { + inviteId: string; invites: string[]; skipped: boolean; }; } export const emptyForm = deepFreeze({ + organization: '', inferredProtocol: null, urlSkipped: false, useCase: { @@ -156,6 +180,7 @@ export const emptyForm = deepFreeze({ skipped: false, }, teamMembers: { + inviteId: '', invites: [], skipped: false, }, @@ -165,7 +190,7 @@ export type PreparingWorkspacePageProps = { transitionDirection: AnimationReason; next?: () => void; skip?: () => void; - previous?: JSX.Element; + previous?: React.ReactNode; show: boolean; onPageView: () => void; } diff --git a/webapp/channels/src/components/root/root.tsx b/webapp/channels/src/components/root/root.tsx index eb41c2d0fe..73f78f6af6 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, getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences'; +import {Theme} 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,6 +89,8 @@ 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'; @@ -358,8 +360,8 @@ export default class Root extends React.PureComponent { return; } - const useCaseOnboarding = getUseCaseOnboarding(storeState); - if (!useCaseOnboarding) { + const myTeams = getMyTeams(storeState); + if (myTeams.length > 0) { 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 7575f7c5a4..eca15abc20 100644 --- a/webapp/channels/src/components/root/root_redirect/index.ts +++ b/webapp/channels/src/components/root/root_redirect/index.ts @@ -6,7 +6,6 @@ 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'; @@ -14,11 +13,7 @@ import {GlobalState} from 'types/store'; import RootRedirect, {Props} from './root_redirect'; function mapStateToProps(state: GlobalState) { - const useCaseOnboarding = getUseCaseOnboarding(state); - let isElegibleForFirstAdmingOnboarding = useCaseOnboarding; - if (isElegibleForFirstAdmingOnboarding) { - isElegibleForFirstAdmingOnboarding = isCurrentUserSystemAdmin(state); - } + const 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 dcc56032a0..900e8de8b7 100644 --- a/webapp/channels/src/components/signup/signup.test.tsx +++ b/webapp/channels/src/components/signup/signup.test.tsx @@ -7,8 +7,6 @@ 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'; @@ -197,9 +195,6 @@ 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( @@ -228,7 +223,6 @@ 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'); }); @@ -238,9 +232,6 @@ 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( @@ -268,8 +259,6 @@ 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 454d5cd8ae..0407a74895 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 {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {isEmail} from 'mattermost-redux/utils/helpers'; @@ -25,7 +25,6 @@ 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'; @@ -104,7 +103,6 @@ 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); @@ -310,15 +308,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { } else if (inviteId) { getInviteInfo(inviteId); } else if (loggedIn) { - 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(); - } + history.push('/'); } } @@ -461,14 +451,12 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { if (redirectTo) { history.push(redirectTo); - } else if (useCaseOnboarding) { + } else { // 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 95faca09a1..c22fba415f 100644 --- a/webapp/channels/src/components/terms_of_service/index.ts +++ b/webapp/channels/src/components/terms_of_service/index.ts @@ -6,7 +6,6 @@ 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'; @@ -26,9 +25,7 @@ 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 d05c91cd84..5c9f58faab 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,7 +27,6 @@ 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 992086d561..f885830f9c 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,7 +38,6 @@ export interface TermsOfServiceProps { ) => {data: UpdateMyTermsOfServiceStatusResponse}; }; emojiMap: EmojiMap; - useCaseOnboarding: boolean; } interface TermsOfServiceState { @@ -111,14 +110,12 @@ export default class TermsOfService extends React.PureComponentstart 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.github": "GitHub", "onboarding_wizard.plugins.github.tooltip": "Subscribe to repositories, stay up to date with reviews, assignments", "onboarding_wizard.plugins.gitlab": "GitLab", @@ -4329,13 +4345,14 @@ "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.skip": "Skip for now", + "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.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!", 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 2ef11c99d3..b2d3c1a672 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,10 +245,6 @@ 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 62fc2e8fdd..7f1dce9244 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -1099,6 +1099,7 @@ 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 980aa05dce..085527a434 100644 --- a/webapp/platform/types/src/setup.ts +++ b/webapp/platform/types/src/setup.ts @@ -2,5 +2,6 @@ // See LICENSE.txt for license information. export type CompleteOnboardingRequest = { + organization: string; install_plugins: string[]; } From a06b297c8f203acf9f192362628f907200c13b7c Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Mon, 17 Apr 2023 10:17:48 -0500 Subject: [PATCH 11/69] add ci steps --- .github/workflows/channels-ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/channels-ci.yml b/.github/workflows/channels-ci.yml index d6a1a6258c..3321fd717f 100644 --- a/.github/workflows/channels-ci.yml +++ b/.github/workflows/channels-ci.yml @@ -83,6 +83,16 @@ 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 + 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 + 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: From a24fd99149b7f5a51e2916bd54ebb86936d2cc5c Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Mon, 17 Apr 2023 10:29:12 -0500 Subject: [PATCH 12/69] test ci extract lint --- webapp/boards/src/components/addContentMenuItem.tsx | 2 +- webapp/playbooks/src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/boards/src/components/addContentMenuItem.tsx b/webapp/boards/src/components/addContentMenuItem.tsx index 1e147b2563..335a6e4c9d 100644 --- a/webapp/boards/src/components/addContentMenuItem.tsx +++ b/webapp/boards/src/components/addContentMenuItem.tsx @@ -42,7 +42,7 @@ const AddContentMenuItem = (props: Props): JSX.Element => { newBlock.boardId = card.boardId const typeName = handler.getDisplayText(intl) - const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type}'}, {type: typeName}) + const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type} __ci-test__'}, {type: typeName}) const afterRedo = async (nb: Block) => { const contentOrder = card.fields.contentOrder.slice() diff --git a/webapp/playbooks/src/index.tsx b/webapp/playbooks/src/index.tsx index d72b244f2f..6e9f4b1bbf 100644 --- a/webapp/playbooks/src/index.tsx +++ b/webapp/playbooks/src/index.tsx @@ -213,7 +213,7 @@ export default class Plugin { const siteStats = await fetchSiteStats(); return { playbook_count: { - name: , + name: , id: 'total_playbooks', icon: 'fa-book', // font-awesome-4.7.0 handler value: siteStats?.total_playbooks, From 25b9c0e3f0314dc35e736350ce7eca2648bdf652 Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Mon, 17 Apr 2023 11:12:40 -0500 Subject: [PATCH 13/69] Revert "test ci extract lint" This reverts commit 4779700a06b3d74099592a247159230b3e418c44. --- webapp/boards/src/components/addContentMenuItem.tsx | 2 +- webapp/playbooks/src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/boards/src/components/addContentMenuItem.tsx b/webapp/boards/src/components/addContentMenuItem.tsx index 335a6e4c9d..1e147b2563 100644 --- a/webapp/boards/src/components/addContentMenuItem.tsx +++ b/webapp/boards/src/components/addContentMenuItem.tsx @@ -42,7 +42,7 @@ const AddContentMenuItem = (props: Props): JSX.Element => { newBlock.boardId = card.boardId const typeName = handler.getDisplayText(intl) - const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type} __ci-test__'}, {type: typeName}) + const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type}'}, {type: typeName}) const afterRedo = async (nb: Block) => { const contentOrder = card.fields.contentOrder.slice() diff --git a/webapp/playbooks/src/index.tsx b/webapp/playbooks/src/index.tsx index 6e9f4b1bbf..d72b244f2f 100644 --- a/webapp/playbooks/src/index.tsx +++ b/webapp/playbooks/src/index.tsx @@ -213,7 +213,7 @@ export default class Plugin { const siteStats = await fetchSiteStats(); return { playbook_count: { - name: , + name: , id: 'total_playbooks', icon: 'fa-book', // font-awesome-4.7.0 handler value: siteStats?.total_playbooks, From afa3a7eb8f415b6c91ae7928bd529c0e2e88eddb Mon Sep 17 00:00:00 2001 From: Caleb Roseland Date: Tue, 18 Apr 2023 11:29:31 -0500 Subject: [PATCH 14/69] step names --- .github/workflows/channels-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/channels-ci.yml b/.github/workflows/channels-ci.yml index 3321fd717f..7d71862ba7 100644 --- a/.github/workflows/channels-ci.yml +++ b/.github/workflows/channels-ci.yml @@ -83,12 +83,12 @@ 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 + - 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 + - name: ci/lint-playbooks working-directory: webapp/playbooks run: | npm run i18n-extract From 5beaee031dc95e479a1131770c10e37b842cbcd5 Mon Sep 17 00:00:00 2001 From: Tanmay Datta Date: Wed, 19 Apr 2023 14:59:49 +0100 Subject: [PATCH 15/69] [MM-51089] Fix sorting value of category in CreateSidebarCategoryForTeamForUser (#22455) Co-authored-by: Mattermost Build --- .../sqlstore/channel_store_categories.go | 2 +- .../storetest/channel_store_categories.go | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/server/channels/store/sqlstore/channel_store_categories.go b/server/channels/store/sqlstore/channel_store_categories.go index 3f2f726a0f..4eca0d5de0 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: model.SidebarCategorySortDefault, + Sorting: newCategory.Sorting, 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/storetest/channel_store_categories.go b/server/channels/store/storetest/channel_store_categories.go index ecd49ef8c2..6ba934f45b 100644 --- a/server/channels/store/storetest/channel_store_categories.go +++ b/server/channels/store/storetest/channel_store_categories.go @@ -672,6 +672,38 @@ 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) { From af37ffac38d37e160b18d3a1f209c3df3d09b25e Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Wed, 19 Apr 2023 20:01:50 +0530 Subject: [PATCH 16/69] Add CODEOWNERS for migrations (#23020) ```release-note NONE ``` --- CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index b0bd218122..4ef5c97c43 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,3 +5,6 @@ /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 From 42c95776cd3eb09951db8cc4dd4e650cbfae50b1 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Wed, 19 Apr 2023 10:45:23 -0600 Subject: [PATCH 17/69] MM-51876 - use redirect if user already logged in (#22997) * use redirect if user already logged in * lint fixes --------- Co-authored-by: Mattermost Build --- .../channels/src/components/login/login.test.tsx | 14 ++++++++++++++ webapp/channels/src/components/login/login.tsx | 10 +++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/webapp/channels/src/components/login/login.test.tsx b/webapp/channels/src/components/login/login.test.tsx index b1116f4d2c..512c98856f 100644 --- a/webapp/channels/src/components/login/login.test.tsx +++ b/webapp/channels/src/components/login/login.test.tsx @@ -288,4 +288,18 @@ 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 c0e154e561..edde751e63 100644 --- a/webapp/channels/src/components/login/login.tsx +++ b/webapp/channels/src/components/login/login.tsx @@ -141,6 +141,9 @@ 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[] = []; @@ -372,6 +375,10 @@ const Login = ({onCustomizeHeader}: LoginProps) => { useEffect(() => { if (currentUser) { + if (redirectTo && redirectTo.match(/^\/([^/]|$)/)) { + history.push(redirectTo); + return; + } redirectUserToDefaultTeam(); return; } @@ -615,9 +622,6 @@ 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. From efa976cbfdbcb11c0589bb8ff524ac01654c5f94 Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Wed, 19 Apr 2023 10:38:56 -0700 Subject: [PATCH 18/69] [MM-52152] Expose license SkuShortName to all users (#22955) --- server/channels/app/license_test.go | 2 -- server/channels/app/platform/license_test.go | 2 -- server/channels/utils/license.go | 1 - 3 files changed, 5 deletions(-) diff --git a/server/channels/app/license_test.go b/server/channels/app/license_test.go index 7b32ee52e6..ea6bbcf7eb 100644 --- a/server/channels/app/license_test.go +++ b/server/channels/app/license_test.go @@ -71,8 +71,6 @@ 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/platform/license_test.go b/server/channels/app/platform/license_test.go index 6682348130..258c2fbe38 100644 --- a/server/channels/app/platform/license_test.go +++ b/server/channels/app/platform/license_test.go @@ -71,8 +71,6 @@ 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/utils/license.go b/server/channels/utils/license.go index b937662f35..43f1f8a0ba 100644 --- a/server/channels/utils/license.go +++ b/server/channels/utils/license.go @@ -210,7 +210,6 @@ func GetSanitizedClientLicense(l map[string]string) map[string]string { delete(sanitizedLicense, "StartsAt") delete(sanitizedLicense, "ExpiresAt") delete(sanitizedLicense, "SkuName") - delete(sanitizedLicense, "SkuShortName") return sanitizedLicense } From 7c60637cabbf7968d8a934da62561ae7de92cfbb Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Wed, 19 Apr 2023 14:52:21 -0700 Subject: [PATCH 19/69] [MM-51805 + MM-51806 + MM-51041] Work template: multiple UI fixes (#22862) --- .../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 | 3 + 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/webapp/channels/src/components/work_templates/components/customize.tsx b/webapp/channels/src/components/work_templates/components/customize.tsx index 6d5639308c..07569122e9 100644 --- a/webapp/channels/src/components/work_templates/components/customize.tsx +++ b/webapp/channels/src/components/work_templates/components/customize.tsx @@ -165,7 +165,8 @@ const StyledCustomized = styled(Customize)` border-radius: 4px; border: 1px solid rgba(var(--center-channel-text-rgb), 0.16); &:focus { - border: 2px solid var(--button-bg); + border: 1px solid var(--button-bg); + box-shadow: inset 0 0 0 1px var(--button-bg); } } diff --git a/webapp/channels/src/components/work_templates/components/preview.tsx b/webapp/channels/src/components/work_templates/components/preview.tsx index 9e7ab6438e..14ea11265d 100644 --- a/webapp/channels/src/components/work_templates/components/preview.tsx +++ b/webapp/channels/src/components/work_templates/components/preview.tsx @@ -51,7 +51,8 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { const [integrations, setIntegrations] = useState(); - const plugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins); + const marketplacePlugins: MarketplacePlugin[] = useSelector((state: GlobalState) => state.views.marketplace.plugins); + const loadedPlugins = useSelector((state: GlobalState) => state.plugins.plugins); const [illustrationDetails, setIllustrationDetails] = useState(() => { const defaultIllustration = getTemplateDefaultIllustration(template); @@ -130,13 +131,14 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { const intg = availableIntegrations?. flatMap((integration) => { - return plugins.reduce((acc: Integration[], curr) => { + return marketplacePlugins.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: curr.installed_version !== '', + installed, }); return acc; @@ -149,7 +151,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { if (intg?.length) { setIntegrations(intg); } - }, [plugins, availableIntegrations, pluginsEnabled]); + }, [marketplacePlugins, availableIntegrations, loadedPlugins, pluginsEnabled]); // building accordion items const accordionItemsData: AccordionItemType[] = []; @@ -204,7 +206,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { )], }); } - if (integrations?.length && pluginsEnabled) { + if (pluginsEnabled && integrations?.length) { accordionItemsData.push({ id: 'integrations', icon: , @@ -303,6 +305,7 @@ 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 41394b9dc4..02be467269 100644 --- a/webapp/channels/src/components/work_templates/components/preview/accordion.tsx +++ b/webapp/channels/src/components/work_templates/components/preview/accordion.tsx @@ -12,6 +12,7 @@ const Accordion = styled(LibAccordion)` .accordion-card { margin-bottom: 8px; border-radius: 4px; + border: 1px solid transparent; color: var(--center-channel-color); .accordion-card-header { @@ -46,7 +47,7 @@ const Accordion = styled(LibAccordion)` } &.active { - border: 1px solid var(--denim-button-bg); + border-color: 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 00ee7e8f13..3514da9b09 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, useEffect, useState} from 'react'; +import React, {ReactNode, useCallback, useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; import classnames from 'classnames'; import styled from 'styled-components'; @@ -110,6 +110,28 @@ 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 (
@@ -119,15 +141,17 @@ const IntegrationsPreview = ({items, categoryId}: IntegrationPreviewSectionProps key={item.id} className={classnames('preview-integrations-plugins-item', {'preview-integrations-plugins-item__readonly': !item.installed && !pluginInstallationPossible})} > -
+
- {item.name} + {item.name}
+ + {makeIntegrationSubtext(item)} +
{item.installed && -
} - {!item.installed &&
} +
}
); })}
@@ -205,6 +229,7 @@ const StyledPreviewSection = styled(PreviewSection)` &-item { display: flex; + align-items: center; width: 128px; height: 48px; flex-basis: 45%; @@ -215,7 +240,7 @@ const StyledPreviewSection = styled(PreviewSection)` opacity: 65%; } - &__icon { + &__illustration { display: flex; width: 24px; height: 24px; @@ -227,22 +252,30 @@ 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: 22px; + 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); + } } } } @@ -264,8 +297,8 @@ const StyledPreviewSection = styled(PreviewSection)` } .icon-check-circle::before { - margin-top: 8px; - margin-right: 8px; + margin-top: 2px; + margin-right: 2px; } .icon-download-outline::before { diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 88de5a0b7c..c5af857223 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -5784,6 +5784,9 @@ "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}", From 1f697ba5cd76b17aec5a8ed45642e469b6c4692e Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Thu, 20 Apr 2023 08:31:09 +0530 Subject: [PATCH 20/69] Revert "MM-50123 : Identify causes of removal of channel and channel members re fetching on team switch (#22984)" (#23038) This reverts commit 5d5c1d90bfe1cebe117b5221487cf73b0dc1ad4c. --- .../channels/src/actions/channel_actions.ts | 21 +++++++++++-------- .../team_controller/actions/index.ts | 17 ++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/webapp/channels/src/actions/channel_actions.ts b/webapp/channels/src/actions/channel_actions.ts index c800d0fda1..b3c9ffb156 100644 --- a/webapp/channels/src/actions/channel_actions.ts +++ b/webapp/channels/src/actions/channel_actions.ts @@ -254,22 +254,25 @@ export function fetchChannelsAndMembers(teamId: Team['id'] = ''): ActionFunc<{ch teamId, data: channels, }); + actions.push({ + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, + data: channelMembers, + }); + actions.push({ + type: RoleTypes.RECEIVED_ROLES, + data: roles, + }); } else { actions.push({ type: ChannelTypes.RECEIVED_ALL_CHANNELS, data: channels, }); + actions.push({ + type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, + data: channelMembers, + }); } - actions.push({ - type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS, - data: channelMembers, - }); - actions.push({ - type: RoleTypes.RECEIVED_ROLES, - data: roles, - }); - await dispatch(batchActions(actions)); return {data: {channels, channelMembers, roles}}; diff --git a/webapp/channels/src/components/team_controller/actions/index.ts b/webapp/channels/src/components/team_controller/actions/index.ts index e1717d0dbc..38977f2678 100644 --- a/webapp/channels/src/components/team_controller/actions/index.ts +++ b/webapp/channels/src/components/team_controller/actions/index.ts @@ -4,9 +4,10 @@ import {ActionFunc} from 'mattermost-redux/types/actions'; import {getTeamByName, selectTeam} from 'mattermost-redux/actions/teams'; import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers'; +import {fetchMyChannelsAndMembersREST} from 'mattermost-redux/actions/channels'; import {getGroups, getAllGroupsAssociatedToChannelsInTeam, getAllGroupsAssociatedToTeam, getGroupsByUserIdPaginated} from 'mattermost-redux/actions/groups'; import {logError} from 'mattermost-redux/actions/errors'; -import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {isCustomGroupsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; @@ -14,6 +15,7 @@ import {isSuccess} from 'types/actions'; import {loadStatusesForChannelAndSidebar} from 'actions/status_actions'; import {addUserToTeam} from 'actions/team_actions'; +import {fetchChannelsAndMembers} from 'actions/channel_actions'; import LocalStorageStore from 'stores/local_storage_store'; @@ -28,6 +30,19 @@ export function initializeTeam(team: Team): ActionFunc { const currentUser = getCurrentUser(state); LocalStorageStore.setPreviousTeamId(currentUser.id, team.id); + const graphQLEnabled = isGraphQLEnabled(state); + try { + if (graphQLEnabled) { + await dispatch(fetchChannelsAndMembers(team.id)); + } else { + await dispatch(fetchMyChannelsAndMembersREST(team.id)); + } + } catch (error) { + forceLogoutIfNecessary(error as ServerError, dispatch, getState); + dispatch(logError(error as ServerError)); + return {error: error as ServerError}; + } + dispatch(loadStatusesForChannelAndSidebar()); const license = getLicense(state); From f0a54f680465f6b6117b660ec595ef6fc0f4adbd Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Thu, 20 Apr 2023 13:45:09 +0530 Subject: [PATCH 21/69] chore: review comment --- .../channel/archived_channels_1_spec.js | 15 +++++++++------ .../channels/channel/more_channels_spec.js | 17 +++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index 6b9301814b..579ed06b0f 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -15,7 +15,10 @@ import {getRandomId} from '../../../utils'; describe('Leave an archived channel', () => { let testTeam; let offTopicUrl; - + const channelType = { + public : 'Channel Type: Public', + archived : 'Channel Type: Archived', + } before(() => { cy.apiUpdateConfig({ TeamSettings: { @@ -97,7 +100,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('#moreChannelsModal').should('be.visible').within(() => { // # Click on dropdown - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Click archived channels cy.findByText('Archived Channels').click(); @@ -145,7 +148,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Public channel list opens by default - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Click on archived channels cy.findByText('Archived Channels').click(); @@ -198,7 +201,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Public channels are shown by default - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -252,7 +255,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens cy.get('.more-modal').should('be.visible').within(() => { // # Show public channels is visible by default - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); @@ -286,7 +289,7 @@ describe('Leave an archived channel', () => { // # More channels modal opens and lands on public channels cy.get('#moreChannelsModal').should('be.visible').within(() => { - cy.findByText('Channel Type: Public').should('be.visible').click(); + cy.findByText(channelType.public).should('be.visible').click(); // # Go to archived channels cy.findByText('Archived Channels').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index 326a21b1e3..e1a496781a 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -14,6 +14,11 @@ import * as TIMEOUTS from '../../../fixtures/timeouts'; import {createPrivateChannel} from '../enterprise/elasticsearch_autocomplete/helpers'; +const channelType = { + public : 'Channel Type: Public', + archived : 'Channel Type: Archived', +} + describe('Channels', () => { let testUser; let otherUser; @@ -65,7 +70,7 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').wait(TIMEOUTS.HALF_SEC); + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => { @@ -113,7 +118,7 @@ describe('Channels', () => { cy.findByText('Archived Channels').should('be.visible').click(); // * Channel test should be visible as an archived channel in the list - cy.wrap(el).should('contain', 'Channel Type: Archived'); + cy.wrap(el).should('contain', channelType.archived); }); cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC); @@ -196,7 +201,7 @@ describe('Channels', () => { // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { - cy.wrap(el).should('contain', 'Channel Type: Public'); + cy.wrap(el).should('contain', channelType.public); }); // * Users should be able to type and search @@ -207,12 +212,12 @@ describe('Channels', () => { cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Users should be able to switch to "Archived Channels" list - cy.get('#channelsMoreDropdown').should('be.visible').and('contain', 'Channel Type: Public').click().within((el) => { + cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).click().within((el) => { // # Click on archived channels item cy.findByText('Archived Channels').should('be.visible').click(); // * Modal should show the archived channels list - cy.wrap(el).should('contain', 'Channel Type: Archived'); + cy.wrap(el).should('contain', channelType.archived); }).wait(TIMEOUTS.HALF_SEC); cy.get('#searchChannelsTextbox').clear(); cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2); @@ -250,7 +255,7 @@ function verifyMoreChannelsModal(isEnabled) { // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { if (isEnabled) { - cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', 'Channel Type: Public'); + cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', channelType.public); } else { cy.get('#channelsMoreDropdown').should('not.exist'); } From 3055a5e935eb5e5605a56b1200fd77daab9d05cc Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Thu, 20 Apr 2023 13:56:37 +0530 Subject: [PATCH 22/69] 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[]; } From 7c78fe558971cbf9cdfe95ac9e16f1422675ad2d Mon Sep 17 00:00:00 2001 From: Conor Macpherson Date: Thu, 20 Apr 2023 14:20:12 -0400 Subject: [PATCH 23/69] Fix style issues with workspace deletion modal. --- .../billing/delete_workspace/delete_workspace_modal.scss | 7 ++++++- .../billing/delete_workspace/delete_workspace_modal.tsx | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss index 2bcbbebd2a..d93d35824c 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss @@ -12,6 +12,10 @@ } } + &__Icon { + padding-top: 8px; + } + &__Title { color: var(--sys-denim-center-channel-text); font-family: Metropolis; @@ -22,10 +26,11 @@ &__Usage { text-align: left; + color: var(--sys-center-channel-text); &-Highlighted { color: black; - font-weight: 700; + font-weight: bold; } } diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx index 5fc7c7c2a7..aaff7fab71 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx @@ -186,8 +186,8 @@ export default function DeleteWorkspaceModal(props: Props) { className='DeleteWorkspaceModal' onExited={handleClickCancel} > -
- +
+
Date: Thu, 20 Apr 2023 14:34:58 -0400 Subject: [PATCH 24/69] lint. --- .../billing/delete_workspace/delete_workspace_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx index aaff7fab71..16ca7b506f 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx @@ -186,7 +186,7 @@ export default function DeleteWorkspaceModal(props: Props) { className='DeleteWorkspaceModal' onExited={handleClickCancel} > -
+
From fd1f62f9d17ae044907cff12ccc01d2b049b475e Mon Sep 17 00:00:00 2001 From: Conor Macpherson Date: Thu, 20 Apr 2023 15:21:02 -0400 Subject: [PATCH 25/69] lint. --- .../billing/delete_workspace/delete_workspace_modal.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss index d93d35824c..7afd7e64a2 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss @@ -25,8 +25,8 @@ } &__Usage { - text-align: left; color: var(--sys-center-channel-text); + text-align: left; &-Highlighted { color: black; From 35a16855170108f79ec3f74b52703e13eba25a5a Mon Sep 17 00:00:00 2001 From: Conor Macpherson Date: Thu, 20 Apr 2023 15:52:03 -0400 Subject: [PATCH 26/69] change font color from sys-center-console-text to center-console-text. --- .../billing/delete_workspace/delete_workspace_modal.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss index 7afd7e64a2..aee1487a3d 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss @@ -25,7 +25,7 @@ } &__Usage { - color: var(--sys-center-channel-text); + color: var(--center-channel-text); text-align: left; &-Highlighted { @@ -35,6 +35,7 @@ } &__Warning { + color: var(--center-channel-text); text-align: left; } From 26ee59e3c2fdb8bcd87a563f80b337fe5f9e1b58 Mon Sep 17 00:00:00 2001 From: Conor Macpherson Date: Thu, 20 Apr 2023 15:54:38 -0400 Subject: [PATCH 27/69] Change to -center-channel-color after seeing warning not to use -center-channel-text. --- .../billing/delete_workspace/delete_workspace_modal.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss index aee1487a3d..514dd90e0a 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss @@ -25,7 +25,7 @@ } &__Usage { - color: var(--center-channel-text); + color: var(--center-channel-color); text-align: left; &-Highlighted { @@ -35,7 +35,7 @@ } &__Warning { - color: var(--center-channel-text); + color: var(--center-channel-color); text-align: left; } From 344e882f042612a37c73ee610805b188df029bcf Mon Sep 17 00:00:00 2001 From: Vishal Date: Fri, 21 Apr 2023 11:55:04 +0530 Subject: [PATCH 28/69] Replace string concatenation with StringBuilder (#23021) --- server/platform/shared/markdown/inlines.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/platform/shared/markdown/inlines.go b/server/platform/shared/markdown/inlines.go index 43dee3bd32..973ae5ed21 100644 --- a/server/platform/shared/markdown/inlines.go +++ b/server/platform/shared/markdown/inlines.go @@ -628,7 +628,7 @@ func MergeInlineText(inlines []Inline) []Inline { } func Unescape(markdown string) string { - ret := "" + var ret strings.Builder position := 0 for position < len(markdown) { @@ -637,27 +637,27 @@ func Unescape(markdown string) string { switch c { case '\\': if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) { - ret += string(markdown[position+1]) + ret.WriteByte(markdown[position+1]) position += 2 } else { - ret += `\` + ret.WriteString(`\`) position++ } case '&': position++ if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 { - ret += "&" + ret.WriteString("&") } else if s := CharacterReference(markdown[position : position+semicolon]); s != "" { position += semicolon + 1 - ret += s + ret.WriteString(s) } else { - ret += "&" + ret.WriteString("&") } default: - ret += string(c) + ret.WriteRune(c) position += cSize } } - return ret + return ret.String() } From 041cbe2d24e22ae415511016fe4501d0341f02ca Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Fri, 21 Apr 2023 10:44:52 +0200 Subject: [PATCH 29/69] Updates the check for the schema_migrations information (#23032) The old check was using `sq.Eq`, which replaces its values with strings, and for the case of `table_schema` we want to call a function. --- server/boards/services/store/sqlstore/schema_table_migration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/boards/services/store/sqlstore/schema_table_migration.go b/server/boards/services/store/sqlstore/schema_table_migration.go index cdb0f4d628..8ec5f154a9 100644 --- a/server/boards/services/store/sqlstore/schema_table_migration.go +++ b/server/boards/services/store/sqlstore/schema_table_migration.go @@ -126,7 +126,7 @@ func (s *SQLStore) isSchemaMigrationNeeded() (bool, error) { case model.MysqlDBType: query = query.Where(sq.Eq{"TABLE_SCHEMA": s.schemaName}) case model.PostgresDBType: - query = query.Where(sq.Eq{"TABLE_SCHEMA": "current_schema()"}) + query = query.Where("table_schema = current_schema()") } rows, err := query.Query() From 67735be261d74bb89fa4fd53aced4db35f6d421e Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Fri, 21 Apr 2023 22:23:56 +0530 Subject: [PATCH 30/69] MM-52216: Trim errors (#23040) https://mattermost.atlassian.net/browse/MM-52216 ```release-note NONE ``` --- server/channels/store/sqlstore/channel_store.go | 14 +++++++------- server/channels/store/sqlstore/file_info_store.go | 2 +- server/channels/store/sqlstore/post_store.go | 2 +- server/channels/store/sqlstore/utils.go | 11 +++++++++++ server/model/utils.go | 8 +++++++- server/model/utils_test.go | 7 +++++++ 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index 2f8e91b9dc..d240ec9dcd 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -3073,7 +3073,7 @@ func (s SqlChannelStore) Autocomplete(userID, term string, includeDeleted, isGue channels := model.ChannelListWithTeamData{} err = s.GetReplicaX().Select(&channels, sql, args...) if err != nil { - return nil, errors.Wrapf(err, "could not find channel with term=%s", term) + return nil, errors.Wrapf(err, "could not find channel with term=%s", trimInput(term)) } return channels, nil } @@ -3186,7 +3186,7 @@ func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID strin // query the database err = s.GetReplicaX().Select(&channels, sql, args...) if err != nil { - return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term) + return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", trimInput(term)) } directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userID, term) @@ -3242,7 +3242,7 @@ func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string // query the channel list from the database using SQLX channels := model.ChannelList{} if err := s.GetReplicaX().Select(&channels, sql, args...); err != nil { - return nil, errors.Wrapf(err, "failed to find Channels with term='%s' (%s %% %v)", term, sql, args) + return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", trimInput(term)) } return channels, nil @@ -3461,7 +3461,7 @@ func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearch } channels := model.ChannelListWithTeamData{} if err2 := s.GetReplicaX().Select(&channels, queryString, args...); err2 != nil { - return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term) + return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", trimInput(term)) } var totalCount int64 @@ -3474,7 +3474,7 @@ func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearch return nil, 0, errors.Wrap(err, "channel_tosql") } if err2 := s.GetReplicaX().Get(&totalCount, queryString, args...); err2 != nil { - return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", term) + return nil, 0, errors.Wrapf(err2, "failed to find Channels with term='%s'", trimInput(term)) } } else { totalCount = int64(len(channels)) @@ -3651,7 +3651,7 @@ func (s SqlChannelStore) performSearch(searchQuery sq.SelectBuilder, term string channels := model.ChannelList{} err = s.GetReplicaX().Select(&channels, sql, args...) if err != nil { - return channels, errors.Wrapf(err, "failed to find Channels with term='%s'", term) + return channels, errors.Wrapf(err, "failed to find Channels with term='%s'", trimInput(term)) } return channels, nil @@ -3744,7 +3744,7 @@ func (s SqlChannelStore) SearchGroupChannels(userId, term string) (model.Channel groupChannels := model.ChannelList{} if err := s.GetReplicaX().Select(&groupChannels, sql, params...); err != nil { - return nil, errors.Wrapf(err, "failed to find Channels with term='%s' and userId=%s", term, userId) + return nil, errors.Wrapf(err, "failed to find Channels with term='%s' and userId=%s", trimInput(term), userId) } return groupChannels, nil } diff --git a/server/channels/store/sqlstore/file_info_store.go b/server/channels/store/sqlstore/file_info_store.go index 0e804605c9..52c5164136 100644 --- a/server/channels/store/sqlstore/file_info_store.go +++ b/server/channels/store/sqlstore/file_info_store.go @@ -681,7 +681,7 @@ func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, team items := []fileInfoWithChannelID{} err = fs.GetSearchReplicaX().Select(&items, queryString, args...) if err != nil { - mlog.Warn("Query error searching files.", mlog.Err(err)) + mlog.Warn("Query error searching files.", mlog.String("error", trimInput(err.Error()))) // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. } else { for _, item := range items { diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go index e60583fe75..ad95fce3a9 100644 --- a/server/channels/store/sqlstore/post_store.go +++ b/server/channels/store/sqlstore/post_store.go @@ -2075,7 +2075,7 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search var posts []*model.Post if err := s.GetSearchReplicaX().Select(&posts, searchQuery, searchQueryArgs...); err != nil { - mlog.Warn("Query error searching posts.", mlog.Err(err)) + mlog.Warn("Query error searching posts.", mlog.String("error", trimInput(err.Error()))) // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. } else { for _, p := range posts { diff --git a/server/channels/store/sqlstore/utils.go b/server/channels/store/sqlstore/utils.go index 69d21ab824..753d5d3933 100644 --- a/server/channels/store/sqlstore/utils.go +++ b/server/channels/store/sqlstore/utils.go @@ -233,3 +233,14 @@ func SanitizeDataSource(driverName, dataSource string) (string, error) { return "", errors.New("invalid drivername. Not postgres or mysql.") } } + +const maxTokenSize = 50 + +// trimInput limits the string to a max size to prevent clogging up disk space +// while logging +func trimInput(input string) string { + if len(input) > maxTokenSize { + input = input[:maxTokenSize] + "..." + } + return input +} diff --git a/server/model/utils.go b/server/model/utils.go index a46bddabae..956aa8caf3 100644 --- a/server/model/utils.go +++ b/server/model/utils.go @@ -251,6 +251,8 @@ type AppError struct { wrapped error } +const maxErrorLength = 1024 + func (er *AppError) Error() string { var sb strings.Builder @@ -276,7 +278,11 @@ func (er *AppError) Error() string { sb.WriteString(err.Error()) } - return sb.String() + res := sb.String() + if len(res) > maxErrorLength { + res = res[:maxErrorLength] + "..." + } + return res } func (er *AppError) Translate(T i18n.TranslateFunc) { diff --git a/server/model/utils_test.go b/server/model/utils_test.go index 606477d750..7b5e099e96 100644 --- a/server/model/utils_test.go +++ b/server/model/utils_test.go @@ -116,6 +116,13 @@ func TestAppErrorRender(t *testing.T) { aerr := NewAppError("here", "message", nil, "details", http.StatusTeapot).Wrap(fmt.Errorf("my error (%w)", fmt.Errorf("inner error"))) assert.EqualError(t, aerr, "here: message, details, my error (inner error)") }) + + t.Run("MaxLength", func(t *testing.T) { + str := strings.Repeat("error", 65536) + msg := "msg" + aerr := NewAppError("id", msg, nil, str, http.StatusTeapot).Wrap(errors.New(str)) + assert.Len(t, aerr.Error(), maxErrorLength+len(msg)) + }) } func TestAppErrorSerialize(t *testing.T) { From 54db770811ea141af8cc62bdee6cd4e82f013986 Mon Sep 17 00:00:00 2001 From: Nathaniel Allred Date: Fri, 21 Apr 2023 12:31:27 -0500 Subject: [PATCH 31/69] Mm 51788 non-admins do not trigger a request to fetch stripe customer in cloud instances (#22821) * do not query for customer if not a cloud admin --- .../payment_announcement_bar/index.test.tsx | 97 +++++++++++++++++++ .../payment_announcement_bar/index.ts | 53 ---------- .../payment_announcement_bar/index.tsx | 89 +++++++++++++++++ .../payment_announcement_bar.tsx | 96 ------------------ webapp/channels/src/utils/constants.tsx | 1 + 5 files changed, 187 insertions(+), 149 deletions(-) create mode 100644 webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx delete mode 100644 webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts create mode 100644 webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx delete mode 100644 webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx new file mode 100644 index 0000000000..8e610d2d24 --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx @@ -0,0 +1,97 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {screen} from '@testing-library/react'; +import {renderWithIntlAndStore} from 'tests/react_testing_utils'; +import * as cloudActions from 'mattermost-redux/actions/cloud'; + +import {CloudProducts} from 'utils/constants'; + +import PaymentAnnouncementBar from './'; + +jest.mock('mattermost-redux/actions/cloud', () => { + const original = jest.requireActual('mattermost-redux/actions/cloud'); + return { + ...original, + __esModule: true, + + // just testing that it fired, not that the result updated or anything like that + getCloudCustomer: jest.fn(() => ({type: 'bogus'})), + }; +}); + +describe('PaymentAnnouncementBar', () => { + const happyPathStore = { + entities: { + users: { + currentUserId: 'me', + profiles: { + me: { + roles: 'system_admin', + }, + }, + }, + general: { + license: { + Cloud: 'true', + }, + }, + cloud: { + subscription: { + product_id: 'prod_something', + last_invoice: { + status: 'failed', + }, + }, + customer: { + payment_method: { + exp_month: 12, + exp_year: (new Date()).getFullYear() + 1, + }, + }, + products: { + prod_something: { + id: 'prod_something', + sku: CloudProducts.PROFESSIONAL, + }, + }, + }, + }, + views: { + announcementBar: { + announcementBarState: { + announcementBarCount: 1, + }, + }, + }, + }; + + it('when most recent payment failed, shows that', () => { + renderWithIntlAndStore(, happyPathStore); + screen.getByText('Your most recent payment failed'); + }); + + it('when card is expired, shows that', () => { + const store = JSON.parse(JSON.stringify(happyPathStore)); + store.entities.cloud.customer.payment_method.exp_year = (new Date()).getFullYear() - 1; + store.entities.cloud.subscription.last_invoice.status = 'success'; + renderWithIntlAndStore(, store); + screen.getByText('Your credit card has expired', {exact: false}); + }); + + it('when needed, fetches, customer', () => { + const store = JSON.parse(JSON.stringify(happyPathStore)); + store.entities.cloud.customer = null; + store.entities.cloud.subscription.last_invoice.status = 'success'; + renderWithIntlAndStore(, store); + expect(cloudActions.getCloudCustomer).toHaveBeenCalled(); + }); + + it('when not an admin, does not fetch customer', () => { + const store = JSON.parse(JSON.stringify(happyPathStore)); + store.entities.users.profiles.me.roles = ''; + renderWithIntlAndStore(, store); + expect(cloudActions.getCloudCustomer).not.toHaveBeenCalled(); + }); +}); diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts deleted file mode 100644 index 86e3bd5f05..0000000000 --- a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {connect} from 'react-redux'; -import {bindActionCreators, Dispatch} from 'redux'; - -import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; -import {GenericAction} from 'mattermost-redux/types/actions'; -import {getCloudSubscription, getCloudCustomer} from 'mattermost-redux/actions/cloud'; - -import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; -import { - getCloudSubscription as selectCloudSubscription, - getCloudCustomer as selectCloudCustomer, - getSubscriptionProduct, -} from 'mattermost-redux/selectors/entities/cloud'; -import {CloudProducts} from 'utils/constants'; - -import {openModal} from 'actions/views/modals'; - -import {GlobalState} from 'types/store'; - -import PaymentAnnouncementBar from './payment_announcement_bar'; - -function mapStateToProps(state: GlobalState) { - const subscription = selectCloudSubscription(state); - const customer = selectCloudCustomer(state); - const subscriptionProduct = getSubscriptionProduct(state); - return { - userIsAdmin: isCurrentUserSystemAdmin(state), - isCloud: getLicense(state).Cloud === 'true', - subscription, - customer, - isStarterFree: subscriptionProduct?.sku === CloudProducts.STARTER, - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators( - { - savePreferences, - openModal, - getCloudSubscription, - getCloudCustomer, - }, - dispatch, - ), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PaymentAnnouncementBar); diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx new file mode 100644 index 0000000000..f14153ad8e --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx @@ -0,0 +1,89 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useEffect, useState} from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useSelector, useDispatch} from 'react-redux'; +import {isEmpty} from 'lodash'; + +import {DispatchFunc} from 'mattermost-redux/types/actions'; +import {getCloudCustomer} from 'mattermost-redux/actions/cloud'; +import {getLicense} from 'mattermost-redux/selectors/entities/general'; +import { + getCloudSubscription as selectCloudSubscription, + getCloudCustomer as selectCloudCustomer, + getSubscriptionProduct, +} from 'mattermost-redux/selectors/entities/cloud'; +import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; + +import {getHistory} from 'utils/browser_history'; +import {isCustomerCardExpired} from 'utils/cloud_utils'; +import {AnnouncementBarTypes, CloudProducts, ConsolePages} from 'utils/constants'; +import {t} from 'utils/i18n'; + +import AnnouncementBar from '../default_announcement_bar'; + +export default function PaymentAnnouncementBar() { + const [requestedCustomer, setRequestedCustomer] = useState(false); + const dispatch = useDispatch(); + const subscription = useSelector(selectCloudSubscription); + const customer = useSelector(selectCloudCustomer); + const isStarterFree = useSelector(getSubscriptionProduct)?.sku === CloudProducts.STARTER; + const userIsAdmin = useSelector(isCurrentUserSystemAdmin); + const isCloud = useSelector(getLicense).Cloud === 'true'; + + useEffect(() => { + if (isCloud && !isStarterFree && isEmpty(customer) && userIsAdmin && !requestedCustomer) { + setRequestedCustomer(true); + dispatch(getCloudCustomer()); + } + }, + [isCloud, isStarterFree, customer, userIsAdmin, requestedCustomer]); + + const mostRecentPaymentFailed = subscription?.last_invoice?.status === 'failed'; + + if ( + // Prevents banner flashes if the subscription hasn't been loaded yet + isEmpty(subscription) || + isStarterFree || + !isCloud || + !userIsAdmin || + isEmpty(customer) || + (!isCustomerCardExpired(customer) && !mostRecentPaymentFailed) + ) { + return null; + } + + const updatePaymentInfo = () => { + getHistory().push(ConsolePages.PAYMENT_INFO); + }; + + let message = ( + + ); + + if (mostRecentPaymentFailed) { + message = ( + + ); + } + + return ( + + ); +} diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx deleted file mode 100644 index 5fe7c7fa4b..0000000000 --- a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {isEmpty} from 'lodash'; - -import {CloudCustomer, Subscription} from '@mattermost/types/cloud'; - -import {getHistory} from 'utils/browser_history'; -import {isCustomerCardExpired} from 'utils/cloud_utils'; -import {AnnouncementBarTypes} from 'utils/constants'; -import {t} from 'utils/i18n'; - -import AnnouncementBar from '../default_announcement_bar'; - -type Props = { - userIsAdmin: boolean; - isCloud: boolean; - subscription?: Subscription; - customer?: CloudCustomer; - isStarterFree: boolean; - actions: { - getCloudSubscription: () => void; - getCloudCustomer: () => void; - }; -}; - -class PaymentAnnouncementBar extends React.PureComponent { - async componentDidMount() { - if (isEmpty(this.props.customer)) { - await this.props.actions.getCloudCustomer(); - } - } - - isMostRecentPaymentFailed = () => { - return this.props.subscription?.last_invoice?.status === 'failed'; - }; - - shouldShowBanner = () => { - const {userIsAdmin, isCloud, subscription} = this.props; - - // Prevents banner flashes if the subscription hasn't been loaded yet - if (subscription === null) { - return false; - } - - if (this.props.isStarterFree) { - return false; - } - - if (!isCloud) { - return false; - } - - if (!userIsAdmin) { - return false; - } - - if (!isCustomerCardExpired(this.props.customer) && !this.isMostRecentPaymentFailed()) { - return false; - } - - return true; - }; - - updatePaymentInfo = () => { - getHistory().push('/admin_console/billing/payment_info'); - }; - - render() { - if (isEmpty(this.props.customer) || isEmpty(this.props.subscription)) { - return null; - } - - if (!this.shouldShowBanner()) { - return null; - } - - return ( - - - ); - } -} - -export default PaymentAnnouncementBar; diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 652e102586..97ea8d65d0 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -2021,6 +2021,7 @@ export const ConsolePages = { WEB_SERVER: '/admin_console/environment/web_server', PUSH_NOTIFICATION_CENTER: '/admin_console/environment/push_notification_server', SMTP: '/admin_console/environment/smtp', + PAYMENT_INFO: '/admin_console/billing/payment_info', BILLING_HISTORY: '/admin_console/billing/billing_history', }; From 94de9c8175e68a1b315ba203cc9a43e4e6769d27 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Sat, 22 Apr 2023 10:14:54 +0530 Subject: [PATCH 32/69] MM-52352: Limit channel search results (#23070) https://mattermost.atlassian.net/browse/MM-52352 ```release-note NONE ``` --- .../channels/store/sqlstore/channel_store.go | 3 +- .../channels/store/storetest/channel_store.go | 28 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/server/channels/store/sqlstore/channel_store.go b/server/channels/store/sqlstore/channel_store.go index d240ec9dcd..38c35239c0 100644 --- a/server/channels/store/sqlstore/channel_store.go +++ b/server/channels/store/sqlstore/channel_store.go @@ -3035,7 +3035,8 @@ func (s SqlChannelStore) Autocomplete(userID, term string, includeDeleted, isGue sq.Expr("t.id = tm.TeamId"), sq.Eq{"tm.UserId": userID}, }). - OrderBy("c.DisplayName") + OrderBy("c.DisplayName"). + Limit(model.ChannelSearchDefaultLimit) if !includeDeleted { query = query.Where(sq.And{ diff --git a/server/channels/store/storetest/channel_store.go b/server/channels/store/storetest/channel_store.go index 69b9328ec8..baffe97abd 100644 --- a/server/channels/store/storetest/channel_store.go +++ b/server/channels/store/storetest/channel_store.go @@ -115,7 +115,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetGuestCount", func(t *testing.T) { testGetGuestCount(t, ss) }) t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) - t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, ss) }) + t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, ss, s) }) t.Run("SearchArchivedInTeam", func(t *testing.T) { testChannelStoreSearchArchivedInTeam(t, ss, s) }) t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, ss) }) t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) @@ -5986,7 +5986,7 @@ func testChannelStoreSearchInTeam(t *testing.T, ss store.Store) { } } -func testAutocomplete(t *testing.T, ss store.Store) { +func testAutocomplete(t *testing.T, ss store.Store, s SqlStore) { t1 := &model.Team{ DisplayName: "t1", Name: NewTestId(), @@ -6165,9 +6165,9 @@ func testAutocomplete(t *testing.T, ss store.Store) { } for _, testCase := range testCases { - t.Run("Autocomplete/"+testCase.Description, func(t *testing.T) { - channels, err := ss.Channel().Autocomplete(testCase.UserID, testCase.Term, testCase.IncludeDeleted, testCase.IsGuest) - require.NoError(t, err) + t.Run(testCase.Description, func(t *testing.T) { + channels, err2 := ss.Channel().Autocomplete(testCase.UserID, testCase.Term, testCase.IncludeDeleted, testCase.IsGuest) + require.NoError(t, err2) var gotChannelIds []string var gotTeamNames []string for _, ch := range channels { @@ -6178,6 +6178,24 @@ func testAutocomplete(t *testing.T, ss store.Store) { require.ElementsMatch(t, testCase.ExpectedTeamNames, gotTeamNames, "team names are not as expected") }) } + + t.Run("Limit", func(t *testing.T) { + for i := 0; i < model.ChannelSearchDefaultLimit+10; i++ { + _, err = ss.Channel().Save(&model.Channel{ + TeamId: teamID, + DisplayName: "Channel " + strconv.Itoa(i), + Name: NewTestId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + } + channels, err := ss.Channel().Autocomplete(m1.UserId, "Chann", false, false) + require.NoError(t, err) + assert.Len(t, channels, model.ChannelSearchDefaultLimit) + }) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMasterX().Exec("TRUNCATE Channels") } func testChannelStoreSearchForUserInTeam(t *testing.T, ss store.Store) { From 273f572cbe7d3bb268c926181cfee991260d1544 Mon Sep 17 00:00:00 2001 From: na Date: Mon, 24 Apr 2023 14:37:53 +0700 Subject: [PATCH 33/69] [MM-51502] - Update email field position in profile popover (#23024) Co-authored-by: Nevyana Angelova --- .../profile_popover/profile_popover.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/webapp/channels/src/components/profile_popover/profile_popover.tsx b/webapp/channels/src/components/profile_popover/profile_popover.tsx index f436ac4e6b..de8d5d753e 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover.tsx @@ -524,23 +524,6 @@ class ProfilePopover extends React.PureComponent, ); - const email = this.props.user.email || ''; - if (email && !this.props.user.is_bot && !haveOverrideProp) { - dataContent.push( - , - ); - } if (this.props.user.position && !haveOverrideProp) { const position = (this.props.user?.position || '').substring( 0, @@ -561,6 +544,23 @@ class ProfilePopover extends React.PureComponent, ); + const email = this.props.user.email || ''; + if (email && !this.props.user.is_bot && !haveOverrideProp) { + dataContent.push( + , + ); + } dataContent.push( Date: Mon, 24 Apr 2023 02:04:29 -0600 Subject: [PATCH 34/69] Make channel type filter dropdown a button instead of an anchor (#22827) Automatic Merge --- webapp/channels/src/components/searchable_channel_list.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/channels/src/components/searchable_channel_list.jsx b/webapp/channels/src/components/searchable_channel_list.jsx index bd9d4f0e59..fc66227ba7 100644 --- a/webapp/channels/src/components/searchable_channel_list.jsx +++ b/webapp/channels/src/components/searchable_channel_list.jsx @@ -250,10 +250,10 @@ export default class SearchableChannelList extends React.PureComponent { channelDropdown = (
- + Date: Mon, 24 Apr 2023 12:58:33 +0200 Subject: [PATCH 35/69] MM-50963 - enhance onboarding self-hosted telemetry (#23051) Co-authored-by: Mattermost Build --- .../src/components/preparing_workspace/organization.tsx | 6 +++--- .../channels/src/components/preparing_workspace/plugins.tsx | 2 ++ .../components/preparing_workspace/preparing_workspace.tsx | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/webapp/channels/src/components/preparing_workspace/organization.tsx b/webapp/channels/src/components/preparing_workspace/organization.tsx index 684c6dc4d9..81f5c3f166 100644 --- a/webapp/channels/src/components/preparing_workspace/organization.tsx +++ b/webapp/channels/src/components/preparing_workspace/organization.tsx @@ -38,8 +38,8 @@ type Props = PreparingWorkspacePageProps & { setInviteId: (inviteId: string) => void; } -const reportValidationError = debounce(() => { - trackEvent('first_admin_setup', 'validate_organization_error'); +const reportValidationError = debounce((error: string) => { + trackEvent('first_admin_setup', 'admin_onboarding_organization_submit_fail', {error}); }, 700, {leading: false}); const Organization = (props: Props) => { @@ -123,7 +123,7 @@ const Organization = (props: Props) => { } if (validation.error || teamApiError.current) { - reportValidationError(); + reportValidationError(validation.error ? validation.error : teamApiError.current! as string); return; } props.next?.(); diff --git a/webapp/channels/src/components/preparing_workspace/plugins.tsx b/webapp/channels/src/components/preparing_workspace/plugins.tsx index caf04e794e..93855aa0b0 100644 --- a/webapp/channels/src/components/preparing_workspace/plugins.tsx +++ b/webapp/channels/src/components/preparing_workspace/plugins.tsx @@ -31,6 +31,7 @@ type Props = PreparingWorkspacePageProps & { setOption: (option: keyof Form['plugins']) => void; className?: string; isSelfHosted: boolean; + handleVisitMarketPlaceClick: () => void; } const Plugins = (props: Props) => { const {formatMessage} = useIntl(); @@ -178,6 +179,7 @@ const Plugins = (props: Props) => { {chunks} diff --git a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx index 268e21c55e..4e0d5dc9aa 100644 --- a/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx +++ b/webapp/channels/src/components/preparing_workspace/preparing_workspace.tsx @@ -267,6 +267,7 @@ const PreparingWorkspace = (props: Props) => { const goToChannels = () => { dispatch({type: GeneralTypes.SHOW_LAUNCHING_WORKSPACE, open: true}); props.history.push(`/${team.name}/channels${Constants.DEFAULT_CHANNEL}`); + trackEvent('first_admin_setup', 'admin_setup_complete'); }; const sendFormEnd = Date.now(); @@ -460,6 +461,9 @@ const PreparingWorkspace = (props: Props) => { show={shouldShowPage(WizardSteps.Plugins)} transitionDirection={getTransitionDirection(WizardSteps.Plugins)} className='child-page' + handleVisitMarketPlaceClick={() => { + trackEvent('first_admin_setup', 'click_visit_marketplace_link'); + }} /> Date: Mon, 24 Apr 2023 10:34:31 -0300 Subject: [PATCH 36/69] Absolute paths in Playbooks API Spec (#22989) --- server/playbooks/server/api/api.yaml | 48 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/server/playbooks/server/api/api.yaml b/server/playbooks/server/api/api.yaml index 538c03ca79..bed383530c 100644 --- a/server/playbooks/server/api/api.yaml +++ b/server/playbooks/server/api/api.yaml @@ -10,7 +10,7 @@ info: servers: - url: http://localhost:8065/plugins/playbooks/api/v0 paths: - /runs: + /plugins/playbooks/api/v0/runs: get: summary: List all playbook runs description: Retrieve a paged list of playbook runs, filtered by team, status, owner, name and/or members, and sorted by ID, name, status, creation date, end date, team or owner ID. @@ -198,7 +198,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/dialog: + /plugins/playbooks/api/v0/runs/dialog: post: summary: Create a new playbook run from dialog description: This is an internal endpoint to create a playbook run from the submission of an interactive dialog, filled by a user in the webapp. See [Interactive Dialogs](https://docs.mattermost.com/developer/interactive-dialogs.html) for more information. @@ -276,7 +276,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/owners: + /plugins/playbooks/api/v0/runs/owners: get: summary: Get all owners description: Get the owners of all playbook runs, filtered by team. @@ -314,7 +314,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/channels: + /plugins/playbooks/api/v0/runs/channels: get: summary: Get playbook run channels description: Get all channels associated with a playbook run, filtered by team, status, owner, name and/or members, and sorted by ID, name, status, creation date, end date, team, or owner ID. @@ -413,7 +413,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/checklist-autocomplete: + /plugins/playbooks/api/v0/runs/checklist-autocomplete: get: summary: Get autocomplete data for /playbook check description: This is an internal endpoint used by the autocomplete system to retrieve the data needed to show the list of items that the user can check. @@ -459,7 +459,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/channel/{channel_id}: + /plugins/playbooks/api/v0/runs/channel/{channel_id}: get: summary: Find playbook run by channel ID operationId: getPlaybookRunByChannelId @@ -492,7 +492,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}: + /plugins/playbooks/api/v0/runs/{id}: get: summary: Get a playbook run operationId: getPlaybookRun @@ -565,7 +565,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/metadata: + /plugins/playbooks/api/v0/runs/{id}/metadata: get: summary: Get playbook run metadata operationId: getPlaybookRunMetadata @@ -598,7 +598,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/end: + /plugins/playbooks/api/v0/runs/{id}/end: put: summary: End a playbook run operationId: endPlaybookRun @@ -651,7 +651,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/restart: + /plugins/playbooks/api/v0/runs/{id}/restart: put: summary: Restart a playbook run operationId: restartPlaybookRun @@ -678,7 +678,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/status: + /plugins/playbooks/api/v0/runs/{id}/status: post: summary: Update a playbook run's status operationId: status @@ -728,7 +728,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/finish: + /plugins/playbooks/api/v0/runs/{id}/finish: put: summary: Finish a playbook operationId: finish @@ -755,7 +755,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/owner: + /plugins/playbooks/api/v0/runs/{id}/owner: post: summary: Update playbook run owner operationId: changeOwner @@ -800,7 +800,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/next-stage-dialog: + /plugins/playbooks/api/v0/runs/{id}/next-stage-dialog: post: summary: Go to next stage from dialog description: This is an internal endpoint to go to the next stage via a confirmation dialog, submitted by a user in the webapp. @@ -835,7 +835,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/add: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/add: put: summary: Add an item to a playbook run's checklist description: The most common pattern to add a new item is to only send its title as the request payload. By default, it is an open item, with no assignee and no slash command. @@ -923,7 +923,7 @@ paths: schema: $ref: "#/components/schemas/Error" - /runs/{id}/checklists/{checklist}/reorder: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/reorder: put: summary: Reorder an item in a playbook run's checklist operationId: reoderChecklistItem @@ -978,7 +978,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}: put: summary: Update an item of a playbook run's checklist description: Update the title and the slash command of an item in one of the playbook run's checklists. @@ -1083,7 +1083,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}/state: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/state: put: summary: Update the state of an item operationId: itemSetState @@ -1145,7 +1145,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}/assignee: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/assignee: put: summary: Update the assignee of an item operationId: itemSetAssignee @@ -1202,7 +1202,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/checklists/{checklist}/item/{item}/run: + /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/run: put: summary: Run an item's slash command operationId: itemRun @@ -1249,7 +1249,7 @@ paths: 500: $ref: "#/components/responses/500" - /runs/{id}/timeline/{event_id}/: + /plugins/playbooks/api/v0/runs/{id}/timeline/{event_id}/: delete: summary: Remove a timeline event from the playbook run operationId: removeTimelineEvent @@ -1285,7 +1285,7 @@ paths: 500: $ref: "#/components/responses/500" - /playbooks: + /plugins/playbooks/api/v0/playbooks: get: summary: List all playbooks description: Retrieve a paged list of playbooks, filtered by team, and sorted by title, number of stages or number of steps. @@ -1562,7 +1562,7 @@ paths: 500: $ref: "#/components/responses/500" - /playbooks/{id}: + /plugins/playbooks/api/v0/playbooks/{id}: get: summary: Get a playbook operationId: getPlaybook @@ -1658,7 +1658,7 @@ paths: 500: $ref: "#/components/responses/500" - /playbooks/{id}/autofollows: + /plugins/playbooks/api/v0/playbooks/{id}/autofollows: get: summary: Get the list of followers' user IDs of a playbook operationId: getAutoFollows From 2dc55918c7d3e7f3126d5891267d901c46238eb4 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Mon, 24 Apr 2023 16:37:22 +0300 Subject: [PATCH 37/69] [MM-52287] - Cloud Free should not show the ability to start a trial (#23073) * [MM-52287] - Cloud Free should not show the ability to start a trial * fix logic --- .../admin_console/billing/billing_summary/index.tsx | 12 ++++++++---- .../src/selectors/entities/preferences.ts | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx b/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx index e53bd72570..8f0d5c5c56 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_summary/index.tsx @@ -5,6 +5,7 @@ import React from 'react'; import {useSelector} from 'react-redux'; import {getSubscriptionProduct, checkHadPriorTrial, getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud'; +import {cloudReverseTrial} from 'mattermost-redux/selectors/entities/preferences'; import {CloudProducts} from 'utils/constants'; @@ -27,17 +28,20 @@ type BillingSummaryProps = { const BillingSummary = ({isFreeTrial, daysLeftOnTrial, onUpgradeMattermostCloud}: BillingSummaryProps) => { const subscription = useSelector(getCloudSubscription); const product = useSelector(getSubscriptionProduct); + const reverseTrial = useSelector(cloudReverseTrial); let body = noBillingHistory; const isPreTrial = subscription?.is_free_trial === 'false' && subscription?.trial_end_at === 0; const hasPriorTrial = useSelector(checkHadPriorTrial); - const showTryEnterprise = product?.sku === CloudProducts.STARTER && isPreTrial; - const showUpgradeProfessional = product?.sku === CloudProducts.STARTER && hasPriorTrial; + const isStarterPreTrial = product?.sku === CloudProducts.STARTER && isPreTrial; + const isStarterPostTrial = product?.sku === CloudProducts.STARTER && hasPriorTrial; - if (showTryEnterprise) { + if (isStarterPreTrial && reverseTrial) { + body = ; + } else if (isStarterPreTrial) { body = tryEnterpriseCard; - } else if (showUpgradeProfessional) { + } else if (isStarterPostTrial) { body = ; } else if (isFreeTrial) { body = freeTrial(onUpgradeMattermostCloud, daysLeftOnTrial); 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..b3f5d1f843 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 @@ -300,6 +300,10 @@ export function deprecateCloudFree(state: GlobalState): boolean { return getFeatureFlagValue(state, 'DeprecateCloudFree') === 'true'; } +export function cloudReverseTrial(state: GlobalState): boolean { + return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true'; +} + export function appsSidebarCategoryEnabled(state: GlobalState): boolean { return getFeatureFlagValue(state, 'AppsSidebarCategory') === 'true'; } From bf4555a223d8cc2a839dfe680ac7639664b54689 Mon Sep 17 00:00:00 2001 From: Spiros Economakis <812075+spirosoik@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:36:51 +0300 Subject: [PATCH 38/69] Bump libucrl gnutls to `7.64.0-4+deb10u6` (#23084) The pin to previous version fails with not found. Ticket: https://mattermost.atlassian.net/browse/CLD-5582 --- server/build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build/Dockerfile b/server/build/Dockerfile index 294debc3f9..2db70884b0 100644 --- a/server/build/Dockerfile +++ b/server/build/Dockerfile @@ -58,7 +58,7 @@ RUN apt-get update \ libxext6=2:1.3.3-1+b2 \ libxrender1=1:0.9.10-1 \ libcairo2=1.16.0-4+deb10u1 \ - libcurl3-gnutls=7.64.0-4+deb10u5 \ + libcurl3-gnutls=7.64.0-4+deb10u6 \ libglib2.0-0=2.58.3-2+deb10u3 \ libgsf-1-common=1.14.45-1 \ libgsf-1-114=1.14.45-1 \ From 94cb2867a7ebea1378a87ffb4d2c8574a39d1152 Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Mon, 24 Apr 2023 09:59:01 -0700 Subject: [PATCH 39/69] [MM-52271] Add recommended tag to Work Template integrations (#23016) --- .../worktemplates/generator/worktemplate.tmpl | 1 + .../channels/app/worktemplates/templates.yaml | 12 ++++++-- server/channels/app/worktemplates/types.go | 6 ++-- .../worktemplates/worktemplate_generated.go | 30 ++++++++++++------- server/model/worktemplate.go | 3 +- .../work_templates/components/preview.tsx | 2 +- .../src/components/work_templates/index.tsx | 18 +++++++++-- webapp/platform/types/src/work_templates.ts | 1 + 8 files changed, 55 insertions(+), 18 deletions(-) diff --git a/server/channels/app/worktemplates/generator/worktemplate.tmpl b/server/channels/app/worktemplates/generator/worktemplate.tmpl index 60f40b5301..52d8d0be5f 100644 --- a/server/channels/app/worktemplates/generator/worktemplate.tmpl +++ b/server/channels/app/worktemplates/generator/worktemplate.tmpl @@ -93,6 +93,7 @@ var wt{{.MD5}} = &WorkTemplate{ Illustration: "{{.Playbook.Illustration}}", },{{end}}{{if .Integration}}Integration: &Integration{ ID: "{{.Integration.ID}}", + Recommended: {{.Integration.Recommended}}, },{{end}} }, {{end}} diff --git a/server/channels/app/worktemplates/templates.yaml b/server/channels/app/worktemplates/templates.yaml index 351e034d15..362b014a71 100644 --- a/server/channels/app/worktemplates/templates.yaml +++ b/server/channels/app/worktemplates/templates.yaml @@ -45,8 +45,10 @@ content: illustration: "/static/worktemplates/playbooks/product_release.png" - integration: id: jira + recommended: true - integration: id: github + recommended: true --- id: 'product_teams/goals_and_okrs:v1' category: product_teams @@ -86,7 +88,7 @@ content: channel: channel-1674845108569 - integration: id: zoom - + recommended: true --- id: 'product_teams/bug_bash:v1' category: product_teams @@ -120,6 +122,7 @@ content: playbook: playbook-1674844017943 - integration: id: jira + recommended: true --- id: 'product_teams/sprint_planning:v1' category: product_teams @@ -153,6 +156,7 @@ content: channel: channel-1674850783500 - integration: id: zoom + recommended: true --- id: 'product_teams/product_roadmap:v1' category: product_teams @@ -282,6 +286,7 @@ content: channel: channel-1674845108569 - integration: id: zoom + recommended: true --- id: 'companywide/create_project:v1' category: companywide @@ -316,10 +321,13 @@ content: channel: channel-1674851940114 - integration: id: jira + recommended: true - integration: id: github + recommended: true - integration: id: zoom + recommended: true --- ###################### # Leadership @@ -356,4 +364,4 @@ content: channel: channel-1674845108569 - integration: id: zoom - + recommended: true diff --git a/server/channels/app/worktemplates/types.go b/server/channels/app/worktemplates/types.go index a75db605aa..83649a4571 100644 --- a/server/channels/app/worktemplates/types.go +++ b/server/channels/app/worktemplates/types.go @@ -108,7 +108,8 @@ func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemp if content.Integration != nil { mwt.Content = append(mwt.Content, model.WorkTemplateContent{ Integration: &model.WorkTemplateIntegration{ - ID: content.Integration.ID, + ID: content.Integration.ID, + Recommended: content.Integration.Recommended, }, }) } @@ -320,7 +321,8 @@ func (p *Playbook) Validate() error { } type Integration struct { - ID string `yaml:"id"` + ID string `yaml:"id"` + Recommended bool `yaml:"recommended"` } func (i *Integration) Validate() error { diff --git a/server/channels/app/worktemplates/worktemplate_generated.go b/server/channels/app/worktemplates/worktemplate_generated.go index a201d7c5c3..f7e3a3e16f 100644 --- a/server/channels/app/worktemplates/worktemplate_generated.go +++ b/server/channels/app/worktemplates/worktemplate_generated.go @@ -148,12 +148,14 @@ var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "jira", + ID: "jira", + Recommended: true, }, }, { Integration: &Integration{ - ID: "github", + ID: "github", + Recommended: true, }, }, }, @@ -214,7 +216,8 @@ var wt5baa68055bf9ea423273662e01ccc575 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -265,7 +268,8 @@ var wtfeb56bc6a8f277c47b503bd1c92d830e = &WorkTemplate{ }, { Integration: &Integration{ - ID: "jira", + ID: "jira", + Recommended: true, }, }, }, @@ -317,7 +321,8 @@ var wt8d2ef53deac5517eb349dc5de6150196 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -518,7 +523,8 @@ var wtf7b846d35810f8272eeb9a1a562025b5 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -570,17 +576,20 @@ var wtb9ab412890c2410c7b49eec8f12e7edc = &WorkTemplate{ }, { Integration: &Integration{ - ID: "jira", + ID: "jira", + Recommended: true, }, }, { Integration: &Integration{ - ID: "github", + ID: "github", + Recommended: true, }, }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, @@ -632,7 +641,8 @@ var wt32ab773bfe021e3d4913931041552559 = &WorkTemplate{ }, { Integration: &Integration{ - ID: "zoom", + ID: "zoom", + Recommended: true, }, }, }, diff --git a/server/model/worktemplate.go b/server/model/worktemplate.go index b0c4262784..73857524bb 100644 --- a/server/model/worktemplate.go +++ b/server/model/worktemplate.go @@ -69,7 +69,8 @@ type WorkTemplatePlaybook struct { } type WorkTemplateIntegration struct { - ID string `json:"id"` + ID string `json:"id"` + Recommended bool `json:"recommended"` } type WorkTemplateContent struct { diff --git a/webapp/channels/src/components/work_templates/components/preview.tsx b/webapp/channels/src/components/work_templates/components/preview.tsx index 14ea11265d..30667461f6 100644 --- a/webapp/channels/src/components/work_templates/components/preview.tsx +++ b/webapp/channels/src/components/work_templates/components/preview.tsx @@ -117,7 +117,7 @@ const Preview = ({template, className, pluginsEnabled}: PreviewProps) => { if (c.playbook) { playbooks.push(c.playbook); } - if (c.integration) { + if (c.integration && c.integration.recommended) { availableIntegrations.push(c.integration); } }); diff --git a/webapp/channels/src/components/work_templates/index.tsx b/webapp/channels/src/components/work_templates/index.tsx index ed5eb0d6a6..d086c34525 100644 --- a/webapp/channels/src/components/work_templates/index.tsx +++ b/webapp/channels/src/components/work_templates/index.tsx @@ -232,7 +232,12 @@ const WorkTemplateModal = () => { const execute = async (template: WorkTemplate, name = '', visibility: Visibility) => { const pbTemplates = []; - for (const item of template.content) { + for (const ctt in template.content) { + if (!Object.hasOwn(template.content, ctt)) { + continue; + } + + const item = template.content[ctt]; if (item.playbook) { const pbTemplate = playbookTemplates.find((pb) => pb.title === item.playbook.template); if (pbTemplate) { @@ -241,11 +246,20 @@ const WorkTemplateModal = () => { } } + // remove non recommended integrations + const filteredTemplate = {...template}; + filteredTemplate.content = template.content.filter((item) => { + if (!item.integration) { + return true; + } + return item.integration.recommended; + }); + const req: ExecuteWorkTemplateRequest = { team_id: teamId, name, visibility, - work_template: template, + work_template: filteredTemplate, playbook_templates: pbTemplates, }; diff --git a/webapp/platform/types/src/work_templates.ts b/webapp/platform/types/src/work_templates.ts index d9d776e566..8b18abf958 100644 --- a/webapp/platform/types/src/work_templates.ts +++ b/webapp/platform/types/src/work_templates.ts @@ -64,6 +64,7 @@ export interface Playbook { } export interface Integration { id: string; + recommended: boolean; name?: string; icon?: string; installed?: boolean; From a52a6d9abdcedc8a1b71b737f89782a69a602dae Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Mon, 24 Apr 2023 14:46:16 -0600 Subject: [PATCH 40/69] Fixes for admins when LastBoardID doesn't exist (#22993) * fixes for admins when LastBoardID doesn't exist * lint fixes * Update webapp/boards/src/components/sidebar/sidebarCategory.tsx Co-authored-by: Caleb Roseland * after deletion, if no boards, send to team (template selector) --------- Co-authored-by: Caleb Roseland Co-authored-by: Mattermost Build --- .../src/components/sidebar/sidebarCategory.tsx | 13 +++++++++++++ webapp/boards/src/pages/boardPage/boardPage.tsx | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/webapp/boards/src/components/sidebar/sidebarCategory.tsx b/webapp/boards/src/components/sidebar/sidebarCategory.tsx index 5b54c8c7a9..734490c9ed 100644 --- a/webapp/boards/src/components/sidebar/sidebarCategory.tsx +++ b/webapp/boards/src/components/sidebar/sidebarCategory.tsx @@ -25,6 +25,7 @@ import CompassIcon from 'src/widgets/icons/compassIcon' import OptionsIcon from 'src/widgets/icons/options' import Menu from 'src/widgets/menu' import MenuWrapper from 'src/widgets/menuWrapper' +import {UserSettings} from 'src/userSettings' import './sidebarCategory.scss' import {Category, CategoryBoardMetadata, CategoryBoards} from 'src/store/sidebar' @@ -202,12 +203,24 @@ const SidebarCategory = (props: Props) => { setTimeout(() => { showBoard(props.boards[nextBoardId as number].id) }, 120) + } else { + setTimeout(() => { + const newPath = generatePath('/team/:teamId', {teamId: teamID,}) + history.push(newPath) + }, 120) } }, async () => { showBoard(deleteBoard.id) }, ) + if ( + UserSettings.lastBoardId && + UserSettings.lastBoardId[deleteBoard.teamId] == deleteBoard.id + ) { + UserSettings.setLastBoardID(deleteBoard.teamId, null) + UserSettings.setLastViewId(deleteBoard.id, null) + } }, [showBoard, deleteBoard, props.boards]) const updateCategory = useCallback(async (value: boolean) => { diff --git a/webapp/boards/src/pages/boardPage/boardPage.tsx b/webapp/boards/src/pages/boardPage/boardPage.tsx index 4f7fc321ba..1bce774cb7 100644 --- a/webapp/boards/src/pages/boardPage/boardPage.tsx +++ b/webapp/boards/src/pages/boardPage/boardPage.tsx @@ -186,7 +186,9 @@ const BoardPage = (props: Props): JSX.Element => { const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => { const member = await octoClient.joinBoard(boardId, allowAdmin) if (!member) { - if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) { + // if allowAdmin is true, then we failed to join the board + // as an admin, normally, this is deleted/missing board + if (!allowAdmin && myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) { setShowJoinBoardDialog(true) return } From e8915b318284f345eadb256a3e731e18fc00bada Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 25 Apr 2023 00:02:51 +0200 Subject: [PATCH 41/69] [MM-45296] Fix installation of pre-packaged plugins that are not in the Marketplace (#21895) Co-authored-by: Jesse Hallam --- server/channels/api4/plugin_test.go | 158 +++++++++++++++++--------- server/channels/app/plugin_install.go | 49 ++++---- 2 files changed, 129 insertions(+), 78 deletions(-) diff --git a/server/channels/api4/plugin_test.go b/server/channels/api4/plugin_test.go index 98ff5ac3f1..8a923d1d0c 100644 --- a/server/channels/api4/plugin_test.go +++ b/server/channels/api4/plugin_test.go @@ -1722,14 +1722,26 @@ func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { appErr := th.App.AddPublicKey("pub_key", key) require.Nil(t, appErr) + t.Cleanup(func() { + appErr = th.App.DeletePublicKey("pub_key") + require.Nil(t, appErr) + }) + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { serverVersion := req.URL.Query().Get("server_version") require.NotEmpty(t, serverVersion) require.Equal(t, model.CurrentVersion, serverVersion) res.WriteHeader(http.StatusOK) + var out []byte - out, err = json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]}) - require.NoError(t, err) + + // Return something if testplugin2 or no specific plugin is requested + pluginID := req.URL.Query().Get("plugin_id") + if pluginID == "" || pluginID == samplePlugins[1].Manifest.Id { + out, err = json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]}) + require.NoError(t, err) + } + res.Write(out) })) defer testServer.Close() @@ -1748,43 +1760,52 @@ func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { require.Len(t, pluginsResp.Active, 0) require.Len(t, pluginsResp.Inactive, 0) - // Should fail to install unknown prepackaged plugin - pRequest := &model.InstallMarketplacePluginRequest{Id: "testpluginXX"} - manifest, resp, err := client.InstallMarketplacePlugin(pRequest) - require.Error(t, err) - CheckInternalErrorStatus(t, resp) - require.Nil(t, manifest) + t.Run("Should fail to install unknown prepackaged plugin", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testpluginXX"} + manifest, resp, err := client.InstallMarketplacePlugin(pRequest) + require.Error(t, err) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) - plugins := env.PrepackagedPlugins() - require.Len(t, plugins, 1) - require.Equal(t, "testplugin", plugins[0].Manifest.Id) - require.Equal(t, pluginSignatureData, plugins[0].Signature) + plugins := env.PrepackagedPlugins() + require.Len(t, plugins, 1) + require.Equal(t, "testplugin", plugins[0].Manifest.Id) + require.Equal(t, pluginSignatureData, plugins[0].Signature) - pluginsResp, _, err = client.GetPlugins() - require.NoError(t, err) - require.Len(t, pluginsResp.Active, 0) - require.Len(t, pluginsResp.Inactive, 0) + pluginsResp, _, err = client.GetPlugins() + require.NoError(t, err) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + }) - pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin"} - manifest1, _, err := client.InstallMarketplacePlugin(pRequest) - require.NoError(t, err) - require.NotNil(t, manifest1) - require.Equal(t, "testplugin", manifest1.Id) - require.Equal(t, "0.0.1", manifest1.Version) + t.Run("Install prepackaged plugin with Marketplace disabled", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"} + manifest, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) + require.NotNil(t, manifest) + require.Equal(t, "testplugin", manifest.Id) + require.Equal(t, "0.0.1", manifest.Version) - pluginsResp, _, err = client.GetPlugins() - require.NoError(t, err) - require.Len(t, pluginsResp.Active, 0) - require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ - Manifest: *manifest1, - }}) + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest.Id) + require.NoError(t, err) + }) - // Try to install remote marketplace plugin - pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"} - manifest, resp, err = client.InstallMarketplacePlugin(pRequest) - require.Error(t, err) - CheckInternalErrorStatus(t, resp) - require.Nil(t, manifest) + pluginsResp, _, err = client.GetPlugins() + require.NoError(t, err) + require.Len(t, pluginsResp.Active, 0) + require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ + Manifest: *manifest, + }}) + }) + + t.Run("Try to install remote marketplace plugin while Marketplace is disabled", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2"} + manifest, resp, err := client.InstallMarketplacePlugin(pRequest) + require.Error(t, err) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) + }) // Enable remote marketplace th.App.UpdateConfig(func(cfg *model.Config) { @@ -1794,31 +1815,58 @@ func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { *cfg.PluginSettings.AllowInsecureDownloadURL = true }) - pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"} - manifest2, _, err := client.InstallMarketplacePlugin(pRequest) - require.NoError(t, err) - require.NotNil(t, manifest2) - require.Equal(t, "testplugin2", manifest2.Id) - require.Equal(t, "1.2.3", manifest2.Version) + t.Run("Install prepackaged, not listed plugin with Marketplace enabled", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"} + manifest, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) - pluginsResp, _, err = client.GetPlugins() - require.NoError(t, err) - require.Len(t, pluginsResp.Active, 0) - require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{ - { - Manifest: *manifest1, - }, - { - Manifest: *manifest2, - }, + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest.Id) + require.NoError(t, err) + }) + + require.NotNil(t, manifest) + assert.Equal(t, "testplugin", manifest.Id) + assert.Equal(t, "0.0.1", manifest.Version) }) - // Clean up - _, err = client.RemovePlugin(manifest1.Id) - require.NoError(t, err) + t.Run("Install both a prepacked and a Marketplace plugin", func(t *testing.T) { + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin"} + manifest1, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) + require.NotNil(t, manifest1) + assert.Equal(t, "testplugin", manifest1.Id) + assert.Equal(t, "0.0.1", manifest1.Version) - _, err = client.RemovePlugin(manifest2.Id) - require.NoError(t, err) + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest1.Id) + require.NoError(t, err) + }) + + pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2"} + manifest2, _, err := client.InstallMarketplacePlugin(pRequest) + require.NoError(t, err) + require.NotNil(t, manifest2) + require.Equal(t, "testplugin2", manifest2.Id) + require.Equal(t, "1.2.3", manifest2.Version) + + t.Cleanup(func() { + _, err = client.RemovePlugin(manifest2.Id) + require.NoError(t, err) + }) + + pluginsResp, _, err = client.GetPlugins() + require.NoError(t, err) + require.Len(t, pluginsResp.Active, 0) + require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{ + { + Manifest: *manifest1, + }, + { + Manifest: *manifest2, + }, + }) + }) appErr = th.App.DeletePublicKey("pub_key") require.Nil(t, appErr) diff --git a/server/channels/app/plugin_install.go b/server/channels/app/plugin_install.go index de40c6838a..59ae9be613 100644 --- a/server/channels/app/plugin_install.go +++ b/server/channels/app/plugin_install.go @@ -203,35 +203,38 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace { var plugin *model.BaseMarketplacePlugin plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version) - if appErr != nil { - return nil, appErr + // The plugin might only be prepackaged and not on the Marketplace. + if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" { + mlog.Warn("Failed to reach Marketplace to install plugin", mlog.String("plugin_id", request.Id), mlog.Err(appErr)) } - var prepackagedVersion semver.Version - if prepackagedPlugin != nil { - var err error - prepackagedVersion, err = semver.Parse(prepackagedPlugin.Manifest.Version) - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) + if plugin != nil { + var prepackagedVersion semver.Version + if prepackagedPlugin != nil { + var err error + prepackagedVersion, err = semver.Parse(prepackagedPlugin.Manifest.Version) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } } - } - marketplaceVersion, err := semver.Parse(plugin.Manifest.Version) - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.prepackged-plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) - } + marketplaceVersion, err := semver.Parse(plugin.Manifest.Version) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.prepackged-plugin.invalid_version.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } - if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found - downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL) - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + if prepackagedVersion.LT(marketplaceVersion) { // Always true if no prepackaged plugin was found + downloadedPluginBytes, err := ch.srv.downloadFromURL(plugin.DownloadURL) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + signature, err := plugin.DecodeSignature() + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, "", http.StatusNotImplemented).Wrap(err) + } + pluginFile = bytes.NewReader(downloadedPluginBytes) + signatureFile = signature } - signature, err := plugin.DecodeSignature() - if err != nil { - return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, "", http.StatusNotImplemented).Wrap(err) - } - pluginFile = bytes.NewReader(downloadedPluginBytes) - signatureFile = signature } } From f9836ee26a299ba0a3e690b04ab65739c5922fe9 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 25 Apr 2023 00:04:17 +0200 Subject: [PATCH 42/69] [MM-51274] Remove deprecated PermissionUseSlashCommands (#22819) Co-authored-by: Mattermost Build --- server/channels/api4/command.go | 14 -------------- server/channels/app/app_test.go | 1 - server/channels/app/import_functions_test.go | 2 +- server/channels/app/permissions_test.go | 6 +++--- .../testlib/testdata/mysql_migration_warmup.sql | 6 +++--- .../testlib/testdata/postgres_migration_warmup.sql | 6 +++--- server/model/permission.go | 11 ----------- server/model/role.go | 2 -- server/model/role_test.go | 1 - .../mattermost-redux/src/constants/permissions.ts | 1 - webapp/channels/src/utils/constants.tsx | 3 --- 11 files changed, 10 insertions(+), 43 deletions(-) diff --git a/server/channels/api4/command.go b/server/channels/api4/command.go index 312c0d093f..724994fe01 100644 --- a/server/channels/api4/command.go +++ b/server/channels/api4/command.go @@ -329,13 +329,6 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { return } - // For compatibility reasons, PermissionCreatePost is also checked. - // TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274 - if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) { - c.SetPermissionError(model.PermissionUseSlashCommands) - return - } - channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId) if err != nil { c.Err = err @@ -354,13 +347,6 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { c.SetPermissionError(model.PermissionCreatePost) return } - - // For compatibility reasons, PermissionCreatePost is also checked. - // TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274 - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) { - c.SetPermissionError(model.PermissionUseSlashCommands) - return - } } } diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go index 22f221d47b..0ba8caff88 100644 --- a/server/channels/app/app_test.go +++ b/server/channels/app/app_test.go @@ -119,7 +119,6 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PermissionGetPublicLink.Id, model.PermissionCreatePost.Id, model.PermissionUseChannelMentions.Id, - model.PermissionUseSlashCommands.Id, model.PermissionManagePublicChannelProperties.Id, model.PermissionDeletePublicChannel.Id, model.PermissionManagePrivateChannelProperties.Id, diff --git a/server/channels/app/import_functions_test.go b/server/channels/app/import_functions_test.go index 95eb6ec64d..d540783603 100644 --- a/server/channels/app/import_functions_test.go +++ b/server/channels/app/import_functions_test.go @@ -459,7 +459,7 @@ func TestImportImportRole(t *testing.T) { // Try changing all the params and reimporting. data.DisplayName = ptrStr("new display name") data.Description = ptrStr("description") - data.Permissions = &[]string{"use_slash_commands"} + data.Permissions = &[]string{"manage_slash_commands"} err = th.App.importRole(th.Context, &data, false, true) require.Nil(t, err, "Should have succeeded. %v", err) diff --git a/server/channels/app/permissions_test.go b/server/channels/app/permissions_test.go index 9ae52a1605..37ced9bd92 100644 --- a/server/channels/app/permissions_test.go +++ b/server/channels/app/permissions_test.go @@ -114,7 +114,7 @@ func TestImportPermissions(t *testing.T) { } beforeCount = len(results) - json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) + json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) r := strings.NewReader(json) err := th.App.ImportPermissions(r) @@ -183,7 +183,7 @@ func TestImportPermissions_idempotentScheme(t *testing.T) { roleName1 := model.NewId() roleName2 := model.NewId() - json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) + json := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) jsonl := strings.Repeat(json+"\n", 4) r := strings.NewReader(jsonl) @@ -226,7 +226,7 @@ func TestImportPermissions_schemeDeletedOnRoleFailure(t *testing.T) { roleName1 := model.NewId() roleName2 := model.NewId() - jsonl := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","use_slash_commands","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) + jsonl := fmt.Sprintf(`{"display_name":"%v","name":"%v","description":"%v","scope":"%v","default_team_admin_role":"","default_team_user_role":"","default_channel_admin_role":"%v","default_channel_user_role":"%v","roles":[{"id":"yzfx3g9xjjfw8cqo6bpn33xr7o","name":"%v","display_name":"Channel Admin Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589687,"update_at":1526475589687,"delete_at":0,"permissions":["manage_channel_roles"],"scheme_managed":true,"built_in":false},{"id":"a7s3cp4n33dfxbsrmyh9djao3a","name":"%v","display_name":"Channel User Role for Scheme my_scheme_1526475590","description":"","create_at":1526475589688,"update_at":1526475589688,"delete_at":0,"permissions":["read_channel","add_reaction","remove_reaction","manage_public_channel_members","upload_file","get_public_link","create_post","manage_private_channel_members","delete_post","edit_post"],"scheme_managed":true,"built_in":false}]}`, displayName, name, description, scope, roleName1, roleName2, roleName1, roleName2) r := strings.NewReader(jsonl) var results []*model.Scheme diff --git a/server/channels/testlib/testdata/mysql_migration_warmup.sql b/server/channels/testlib/testdata/mysql_migration_warmup.sql index 070dae56f6..eaafb2d368 100644 --- a/server/channels/testlib/testdata/mysql_migration_warmup.sql +++ b/server/channels/testlib/testdata/mysql_migration_warmup.sql @@ -81,14 +81,14 @@ INSERT INTO `Roles` VALUES ('hkcrew7wttb5fbuw3ime6g7nzc','system_read_only_admin INSERT INTO `Roles` VALUES ('iiwt9pt6wiyb9e1enixtxs5yme','run_admin','authentication.roles.run_admin.name','authentication.roles.run_admin.description',1662271985864,1662271986932,0,' run_manage_properties run_manage_members',1,1); INSERT INTO `Roles` VALUES ('jg1f1xfh3bb73pua938orwg9ie','system_guest','authentication.roles.global_guest.name','authentication.roles.global_guest.description',1605167829015,1662271986937,0,' create_direct_channel create_group_channel',1,1); INSERT INTO `Roles` VALUES ('k891n5tpd3n9peue79azejjocy','system_post_all_public','authentication.roles.system_post_all_public.name','authentication.roles.system_post_all_public.description',0,1662271986941,0,' use_channel_mentions create_post_public',0,1); -INSERT INTO `Roles` VALUES ('kb6r9i58x7dxdb3srfohd66sse','system_admin','authentication.roles.global_admin.name','authentication.roles.global_admin.description',0,1662271986948,0,' list_public_teams edit_brand manage_private_channel_properties sysconsole_read_user_management_teams playbook_public_create manage_others_bots invalidate_caches manage_shared_channels sysconsole_write_environment_logging manage_others_outgoing_webhooks sysconsole_read_reporting_team_statistics sysconsole_read_plugins list_team_channels use_group_mentions sysconsole_read_site_users_and_teams sysconsole_write_site_localization get_analytics sysconsole_read_experimental_bleve manage_team_roles sysconsole_read_site_localization use_slash_commands edit_post sysconsole_write_user_management_channels test_elasticsearch list_private_teams add_ldap_public_cert join_public_teams manage_slash_commands manage_others_incoming_webhooks manage_public_channel_members sysconsole_read_environment_elasticsearch sysconsole_write_site_customization delete_others_emojis run_manage_members create_emojis sysconsole_write_authentication_email sysconsole_write_compliance_compliance_export add_saml_private_cert create_bot sysconsole_write_environment_rate_limiting add_saml_public_cert edit_other_users sysconsole_write_integrations_integration_management read_user_access_token create_elasticsearch_post_indexing_job sysconsole_write_user_management_users assign_system_admin_role sysconsole_write_user_management_groups sysconsole_read_authentication_guest_access sysconsole_write_about_edition_and_license sysconsole_read_authentication_ldap sysconsole_read_experimental_feature_flags sysconsole_read_integrations_cors sysconsole_read_user_management_groups join_public_channels sysconsole_read_experimental_features test_ldap sysconsole_write_environment_elasticsearch sysconsole_write_reporting_server_logs sysconsole_read_environment_image_proxy sysconsole_read_site_announcement_banner sysconsole_read_reporting_site_statistics sysconsole_write_authentication_mfa sysconsole_read_authentication_openid purge_bleve_indexes playbook_public_manage_members delete_emojis sysconsole_write_environment_file_storage sysconsole_write_reporting_site_statistics playbook_private_manage_members import_team sysconsole_write_environment_web_server sysconsole_write_authentication_password read_public_channel_groups create_compliance_export_job sysconsole_read_authentication_password list_users_without_team sysconsole_read_authentication_mfa add_ldap_private_cert create_data_retention_job read_license_information sysconsole_write_authentication_signup sysconsole_read_environment_push_notification_server edit_others_posts download_compliance_export_result create_ldap_sync_job sysconsole_write_authentication_ldap sysconsole_write_plugins read_data_retention_job sysconsole_write_compliance_data_retention_policy sysconsole_read_site_public_links manage_bots manage_system sysconsole_write_compliance_custom_terms_of_service playbook_public_manage_roles playbook_public_manage_properties playbook_private_create sysconsole_write_experimental_bleve sysconsole_read_authentication_email promote_guest get_saml_cert_status add_user_to_team sysconsole_write_site_users_and_teams create_custom_group manage_private_channel_members read_jobs sysconsole_write_experimental_features read_other_users_teams sysconsole_write_reporting_team_statistics sysconsole_read_environment_file_storage create_post_bleve_indexes_job sysconsole_read_site_file_sharing_and_downloads playbook_private_make_public playbook_public_view create_user_access_token create_public_channel read_channel sysconsole_read_user_management_channels sysconsole_read_user_management_permissions read_public_channel sysconsole_read_compliance_custom_terms_of_service sysconsole_write_site_emoji sysconsole_read_integrations_gif sysconsole_read_site_customization sysconsole_write_integrations_cors invite_user create_direct_channel sysconsole_write_user_management_teams run_create manage_custom_group_members read_ldap_sync_job sysconsole_read_site_notifications playbook_private_manage_properties sysconsole_read_integrations_bot_accounts convert_public_channel_to_private invalidate_email_invite reload_config get_saml_metadata_from_idp manage_secure_connections delete_private_channel sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_environment_developer recycle_database_connections remove_saml_private_cert manage_oauth sysconsole_write_environment_database sysconsole_write_site_notifications sysconsole_write_authentication_guest_access sysconsole_write_compliance_compliance_monitoring sysconsole_write_environment_image_proxy create_post_public manage_jobs remove_user_from_team delete_others_posts create_post_ephemeral playbook_private_view create_elasticsearch_post_aggregation_job remove_reaction add_reaction sysconsole_write_environment_high_availability sysconsole_write_authentication_openid sysconsole_write_user_management_permissions add_saml_idp_cert sysconsole_read_site_posts view_members sysconsole_write_environment_smtp sysconsole_read_authentication_saml create_post use_channel_mentions create_team playbook_private_manage_roles get_public_link sysconsole_write_billing manage_system_wide_oauth sysconsole_read_environment_database sysconsole_write_environment_session_lengths run_manage_properties sysconsole_write_authentication_saml sysconsole_read_environment_web_server sysconsole_read_environment_rate_limiting manage_public_channel_properties create_group_channel sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_high_availability manage_others_slash_commands sysconsole_read_compliance_compliance_export delete_custom_group sysconsole_read_user_management_system_roles purge_elasticsearch_indexes view_team sysconsole_read_environment_performance_monitoring manage_channel_roles playbook_public_make_private remove_saml_public_cert demote_to_guest sysconsole_write_environment_performance_monitoring read_audits sysconsole_write_site_announcement_banner upload_file revoke_user_access_token read_others_bots test_email read_elasticsearch_post_aggregation_job sysconsole_read_compliance_compliance_monitoring join_private_teams delete_post sysconsole_write_site_public_links manage_team edit_custom_group sysconsole_write_experimental_feature_flags sysconsole_write_user_management_system_roles remove_others_reactions manage_license_information sysconsole_read_authentication_signup read_compliance_export_job sysconsole_write_environment_developer remove_saml_idp_cert manage_incoming_webhooks sysconsole_read_site_emoji assign_bot sysconsole_write_integrations_gif sysconsole_read_user_management_users delete_public_channel manage_outgoing_webhooks sysconsole_write_site_posts remove_ldap_private_cert sysconsole_write_site_file_sharing_and_downloads sysconsole_read_integrations_integration_management sysconsole_read_environment_logging test_site_url sysconsole_read_environment_session_lengths read_elasticsearch_post_indexing_job sysconsole_read_billing sysconsole_read_site_notices sysconsole_read_reporting_server_logs sysconsole_write_integrations_bot_accounts sysconsole_write_site_notices create_private_channel read_private_channel_groups run_view read_bots manage_roles test_s3 sysconsole_write_environment_push_notification_server get_logs invite_guest remove_ldap_public_cert sysconsole_read_environment_smtp',1,1); +INSERT INTO `Roles` VALUES ('kb6r9i58x7dxdb3srfohd66sse','system_admin','authentication.roles.global_admin.name','authentication.roles.global_admin.description',0,1662271986948,0,' list_public_teams edit_brand manage_private_channel_properties sysconsole_read_user_management_teams playbook_public_create manage_others_bots invalidate_caches manage_shared_channels sysconsole_write_environment_logging manage_others_outgoing_webhooks sysconsole_read_reporting_team_statistics sysconsole_read_plugins list_team_channels use_group_mentions sysconsole_read_site_users_and_teams sysconsole_write_site_localization get_analytics sysconsole_read_experimental_bleve manage_team_roles sysconsole_read_site_localization edit_post sysconsole_write_user_management_channels test_elasticsearch list_private_teams add_ldap_public_cert join_public_teams manage_slash_commands manage_others_incoming_webhooks manage_public_channel_members sysconsole_read_environment_elasticsearch sysconsole_write_site_customization delete_others_emojis run_manage_members create_emojis sysconsole_write_authentication_email sysconsole_write_compliance_compliance_export add_saml_private_cert create_bot sysconsole_write_environment_rate_limiting add_saml_public_cert edit_other_users sysconsole_write_integrations_integration_management read_user_access_token create_elasticsearch_post_indexing_job sysconsole_write_user_management_users assign_system_admin_role sysconsole_write_user_management_groups sysconsole_read_authentication_guest_access sysconsole_write_about_edition_and_license sysconsole_read_authentication_ldap sysconsole_read_experimental_feature_flags sysconsole_read_integrations_cors sysconsole_read_user_management_groups join_public_channels sysconsole_read_experimental_features test_ldap sysconsole_write_environment_elasticsearch sysconsole_write_reporting_server_logs sysconsole_read_environment_image_proxy sysconsole_read_site_announcement_banner sysconsole_read_reporting_site_statistics sysconsole_write_authentication_mfa sysconsole_read_authentication_openid purge_bleve_indexes playbook_public_manage_members delete_emojis sysconsole_write_environment_file_storage sysconsole_write_reporting_site_statistics playbook_private_manage_members import_team sysconsole_write_environment_web_server sysconsole_write_authentication_password read_public_channel_groups create_compliance_export_job sysconsole_read_authentication_password list_users_without_team sysconsole_read_authentication_mfa add_ldap_private_cert create_data_retention_job read_license_information sysconsole_write_authentication_signup sysconsole_read_environment_push_notification_server edit_others_posts download_compliance_export_result create_ldap_sync_job sysconsole_write_authentication_ldap sysconsole_write_plugins read_data_retention_job sysconsole_write_compliance_data_retention_policy sysconsole_read_site_public_links manage_bots manage_system sysconsole_write_compliance_custom_terms_of_service playbook_public_manage_roles playbook_public_manage_properties playbook_private_create sysconsole_write_experimental_bleve sysconsole_read_authentication_email promote_guest get_saml_cert_status add_user_to_team sysconsole_write_site_users_and_teams create_custom_group manage_private_channel_members read_jobs sysconsole_write_experimental_features read_other_users_teams sysconsole_write_reporting_team_statistics sysconsole_read_environment_file_storage create_post_bleve_indexes_job sysconsole_read_site_file_sharing_and_downloads playbook_private_make_public playbook_public_view create_user_access_token create_public_channel read_channel sysconsole_read_user_management_channels sysconsole_read_user_management_permissions read_public_channel sysconsole_read_compliance_custom_terms_of_service sysconsole_write_site_emoji sysconsole_read_integrations_gif sysconsole_read_site_customization sysconsole_write_integrations_cors invite_user create_direct_channel sysconsole_write_user_management_teams run_create manage_custom_group_members read_ldap_sync_job sysconsole_read_site_notifications playbook_private_manage_properties sysconsole_read_integrations_bot_accounts convert_public_channel_to_private invalidate_email_invite reload_config get_saml_metadata_from_idp manage_secure_connections delete_private_channel sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_environment_developer recycle_database_connections remove_saml_private_cert manage_oauth sysconsole_write_environment_database sysconsole_write_site_notifications sysconsole_write_authentication_guest_access sysconsole_write_compliance_compliance_monitoring sysconsole_write_environment_image_proxy create_post_public manage_jobs remove_user_from_team delete_others_posts create_post_ephemeral playbook_private_view create_elasticsearch_post_aggregation_job remove_reaction add_reaction sysconsole_write_environment_high_availability sysconsole_write_authentication_openid sysconsole_write_user_management_permissions add_saml_idp_cert sysconsole_read_site_posts view_members sysconsole_write_environment_smtp sysconsole_read_authentication_saml create_post use_channel_mentions create_team playbook_private_manage_roles get_public_link sysconsole_write_billing manage_system_wide_oauth sysconsole_read_environment_database sysconsole_write_environment_session_lengths run_manage_properties sysconsole_write_authentication_saml sysconsole_read_environment_web_server sysconsole_read_environment_rate_limiting manage_public_channel_properties create_group_channel sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_high_availability manage_others_slash_commands sysconsole_read_compliance_compliance_export delete_custom_group sysconsole_read_user_management_system_roles purge_elasticsearch_indexes view_team sysconsole_read_environment_performance_monitoring manage_channel_roles playbook_public_make_private remove_saml_public_cert demote_to_guest sysconsole_write_environment_performance_monitoring read_audits sysconsole_write_site_announcement_banner upload_file revoke_user_access_token read_others_bots test_email read_elasticsearch_post_aggregation_job sysconsole_read_compliance_compliance_monitoring join_private_teams delete_post sysconsole_write_site_public_links manage_team edit_custom_group sysconsole_write_experimental_feature_flags sysconsole_write_user_management_system_roles remove_others_reactions manage_license_information sysconsole_read_authentication_signup read_compliance_export_job sysconsole_write_environment_developer remove_saml_idp_cert manage_incoming_webhooks sysconsole_read_site_emoji assign_bot sysconsole_write_integrations_gif sysconsole_read_user_management_users delete_public_channel manage_outgoing_webhooks sysconsole_write_site_posts remove_ldap_private_cert sysconsole_write_site_file_sharing_and_downloads sysconsole_read_integrations_integration_management sysconsole_read_environment_logging test_site_url sysconsole_read_environment_session_lengths read_elasticsearch_post_indexing_job sysconsole_read_billing sysconsole_read_site_notices sysconsole_read_reporting_server_logs sysconsole_write_integrations_bot_accounts sysconsole_write_site_notices create_private_channel read_private_channel_groups run_view read_bots manage_roles test_s3 sysconsole_write_environment_push_notification_server get_logs invite_guest remove_ldap_public_cert sysconsole_read_environment_smtp',1,1); INSERT INTO `Roles` VALUES ('km7kijhdtjbajquwu36uqneyoc','system_post_all','authentication.roles.system_post_all.name','authentication.roles.system_post_all.description',0,1662271986953,0,' create_post use_channel_mentions',0,1); INSERT INTO `Roles` VALUES ('no7s4436sjbzzqjpupg85mszty','custom_group_user','authentication.roles.custom_group_user.name','authentication.roles.custom_group_user.description',1662271985801,1662271986956,0,'',0,0); INSERT INTO `Roles` VALUES ('qo7e17c1m3rezyjqx5iq9dpmxe','system_manager','authentication.roles.system_manager.name','authentication.roles.system_manager.description',0,1662271986960,0,' sysconsole_write_environment_image_proxy sysconsole_read_environment_developer read_ldap_sync_job sysconsole_read_reporting_team_statistics recycle_database_connections get_logs read_private_channel_groups test_elasticsearch sysconsole_read_environment_logging purge_elasticsearch_indexes sysconsole_write_site_posts sysconsole_read_environment_database sysconsole_read_environment_performance_monitoring manage_team sysconsole_read_authentication_password sysconsole_write_site_users_and_teams sysconsole_read_user_management_channels sysconsole_write_environment_rate_limiting sysconsole_write_site_notifications read_license_information edit_brand sysconsole_read_plugins sysconsole_read_environment_high_availability sysconsole_read_environment_file_storage sysconsole_read_environment_elasticsearch sysconsole_write_environment_web_server sysconsole_write_environment_smtp sysconsole_write_environment_performance_monitoring sysconsole_write_environment_session_lengths sysconsole_write_user_management_groups convert_private_channel_to_public manage_private_channel_properties sysconsole_read_site_posts list_private_teams sysconsole_read_authentication_ldap sysconsole_read_authentication_guest_access sysconsole_read_site_emoji sysconsole_write_integrations_integration_management convert_public_channel_to_private manage_private_channel_members read_elasticsearch_post_aggregation_job manage_team_roles sysconsole_write_site_file_sharing_and_downloads read_channel read_public_channel sysconsole_read_authentication_openid add_user_to_team sysconsole_write_environment_developer sysconsole_write_site_localization sysconsole_read_about_edition_and_license test_s3 reload_config sysconsole_write_environment_elasticsearch test_site_url sysconsole_write_site_announcement_banner get_analytics sysconsole_read_environment_push_notification_server sysconsole_read_authentication_signup test_email sysconsole_write_integrations_bot_accounts sysconsole_write_integrations_cors view_team sysconsole_write_integrations_gif sysconsole_read_site_notices sysconsole_read_environment_image_proxy sysconsole_read_integrations_cors sysconsole_write_environment_push_notification_server join_public_teams test_ldap create_elasticsearch_post_aggregation_job sysconsole_read_environment_session_lengths sysconsole_write_environment_file_storage manage_public_channel_members sysconsole_write_site_customization sysconsole_read_site_announcement_banner sysconsole_read_environment_smtp sysconsole_write_user_management_teams delete_public_channel sysconsole_write_environment_logging read_public_channel_groups sysconsole_read_site_users_and_teams sysconsole_read_reporting_site_statistics sysconsole_read_site_localization sysconsole_read_site_customization sysconsole_read_environment_rate_limiting sysconsole_read_environment_web_server sysconsole_write_user_management_permissions sysconsole_read_site_file_sharing_and_downloads sysconsole_write_site_public_links sysconsole_read_site_public_links sysconsole_read_authentication_email read_elasticsearch_post_indexing_job sysconsole_read_authentication_saml remove_user_from_team delete_private_channel sysconsole_write_user_management_channels sysconsole_read_reporting_server_logs sysconsole_read_integrations_bot_accounts sysconsole_read_user_management_teams list_public_teams create_elasticsearch_post_indexing_job sysconsole_write_site_emoji invalidate_caches sysconsole_read_integrations_integration_management sysconsole_write_environment_high_availability sysconsole_read_user_management_permissions join_private_teams manage_channel_roles sysconsole_write_site_notices manage_public_channel_properties sysconsole_write_environment_database sysconsole_read_site_notifications sysconsole_read_user_management_groups sysconsole_read_integrations_gif sysconsole_read_authentication_mfa',0,1); INSERT INTO `Roles` VALUES ('rkr97ikkh7fixy86qsoo5rqm4c','system_user_access_token','authentication.roles.system_user_access_token.name','authentication.roles.system_user_access_token.description',0,1662271986965,0,' create_user_access_token read_user_access_token revoke_user_access_token',0,1); INSERT INTO `Roles` VALUES ('rxzdk5irm7rcffcfej9e33kqeo','team_user','authentication.roles.team_user.name','authentication.roles.team_user.description',0,1662271986968,0,' invite_user view_team read_public_channel playbook_public_create add_user_to_team playbook_private_create create_private_channel list_team_channels create_public_channel join_public_channels',1,1); -INSERT INTO `Roles` VALUES ('x768jnyzw3rkfx7xb66ehcac6o','channel_user','authentication.roles.channel_user.name','authentication.roles.channel_user.description',0,1662271986972,0,' manage_public_channel_properties create_post manage_private_channel_properties delete_public_channel manage_private_channel_members get_public_link delete_post delete_private_channel upload_file edit_post remove_reaction use_channel_mentions add_reaction read_channel use_slash_commands manage_public_channel_members',1,1); -INSERT INTO `Roles` VALUES ('ynn8aynsn7n1trtbuq6p4cyzhe','channel_guest','authentication.roles.channel_guest.name','authentication.roles.channel_guest.description',1605167829001,1662271986975,0,' read_channel add_reaction remove_reaction upload_file edit_post create_post use_channel_mentions use_slash_commands',1,1); +INSERT INTO `Roles` VALUES ('x768jnyzw3rkfx7xb66ehcac6o','channel_user','authentication.roles.channel_user.name','authentication.roles.channel_user.description',0,1662271986972,0,' manage_public_channel_properties create_post manage_private_channel_properties delete_public_channel manage_private_channel_members get_public_link delete_post delete_private_channel upload_file edit_post remove_reaction use_channel_mentions add_reaction read_channel manage_public_channel_members',1,1); +INSERT INTO `Roles` VALUES ('ynn8aynsn7n1trtbuq6p4cyzhe','channel_guest','authentication.roles.channel_guest.name','authentication.roles.channel_guest.description',1605167829001,1662271986975,0,' read_channel add_reaction remove_reaction upload_file edit_post create_post use_channel_mentions',1,1); INSERT INTO `Roles` VALUES ('yqyby79r9jggxg7a9dnenuawmo','run_member','authentication.roles.run_member.name','authentication.roles.run_member.description',1662271985813,1662271986979,0,' run_view',1,1); INSERT INTO `Roles` VALUES ('zzehkfnp67bg5g1owh6eptdcxc','system_user','authentication.roles.global_user.name','authentication.roles.global_user.description',0,1662271986983,0,' create_emojis join_public_teams list_public_teams edit_custom_group delete_emojis create_team create_group_channel manage_custom_group_members view_members delete_custom_group create_custom_group create_direct_channel',1,1); /*!40000 ALTER TABLE `Roles` ENABLE KEYS */; diff --git a/server/channels/testlib/testdata/postgres_migration_warmup.sql b/server/channels/testlib/testdata/postgres_migration_warmup.sql index 4dc1481c3a..b58b54e62e 100644 --- a/server/channels/testlib/testdata/postgres_migration_warmup.sql +++ b/server/channels/testlib/testdata/postgres_migration_warmup.sql @@ -17,7 +17,7 @@ SET client_encoding = 'UTF8'; INSERT INTO public.roles VALUES ('gkegg9mqi3rgbm9u444mnxkmbc', 'team_post_all_public', 'authentication.roles.team_post_all_public.name', 'authentication.roles.team_post_all_public.description', 0, 1662230812026, 0, ' create_post_public use_channel_mentions', false, true); INSERT INTO public.roles VALUES ('7ta1wfbacjy3zxid54n3cqjzqw', 'system_post_all_public', 'authentication.roles.system_post_all_public.name', 'authentication.roles.system_post_all_public.description', 0, 1662230812027, 0, ' create_post_public use_channel_mentions', false, true); INSERT INTO public.roles VALUES ('xf95ytghtjfsfd543dum68uzua', 'system_user_access_token', 'authentication.roles.system_user_access_token.name', 'authentication.roles.system_user_access_token.description', 0, 1662230812027, 0, ' create_user_access_token read_user_access_token revoke_user_access_token', false, true); -INSERT INTO public.roles VALUES ('nh5i9ik1u78hdcny9usdoixkuo', 'channel_user', 'authentication.roles.channel_user.name', 'authentication.roles.channel_user.description', 0, 1662230812029, 0, ' delete_post delete_public_channel use_channel_mentions manage_private_channel_properties manage_public_channel_properties delete_private_channel upload_file read_channel use_slash_commands get_public_link remove_reaction create_post add_reaction manage_private_channel_members edit_post manage_public_channel_members', true, true); +INSERT INTO public.roles VALUES ('nh5i9ik1u78hdcny9usdoixkuo', 'channel_user', 'authentication.roles.channel_user.name', 'authentication.roles.channel_user.description', 0, 1662230812029, 0, ' delete_post delete_public_channel use_channel_mentions manage_private_channel_properties manage_public_channel_properties delete_private_channel upload_file read_channel get_public_link remove_reaction create_post add_reaction manage_private_channel_members edit_post manage_public_channel_members', true, true); INSERT INTO public.roles VALUES ('peooyqpsq7g5bfnfo45zb1jiro', 'system_guest', 'authentication.roles.global_guest.name', 'authentication.roles.global_guest.description', 1605163387739, 1662230812021, 0, ' create_group_channel create_direct_channel', true, true); INSERT INTO public.roles VALUES ('96whs8mg73dszp7cz4u7sdbd7c', 'team_guest', 'authentication.roles.team_guest.name', 'authentication.roles.team_guest.description', 1605163387741, 1662230812022, 0, ' view_team', true, true); INSERT INTO public.roles VALUES ('rfc1w7z71pnzurkhpb1jgrbmdh', 'team_user', 'authentication.roles.team_user.name', 'authentication.roles.team_user.description', 1605163387747, 1662230812023, 0, ' playbook_public_create view_team invite_user playbook_private_create list_team_channels join_public_channels create_private_channel add_user_to_team read_public_channel create_public_channel', true, true); @@ -26,14 +26,14 @@ INSERT INTO public.roles VALUES ('wxat9mo53tg79xdzn55kdq148w', 'channel_admin', INSERT INTO public.roles VALUES ('13kpq8iaqffmdf9qkrfqmpby9h', 'team_admin', 'authentication.roles.team_admin.name', 'authentication.roles.team_admin.description', 0, 1662230812024, 0, ' manage_incoming_webhooks manage_others_incoming_webhooks import_team manage_others_outgoing_webhooks manage_team_roles remove_user_from_team manage_team manage_outgoing_webhooks manage_slash_commands convert_public_channel_to_private playbook_public_manage_roles manage_others_slash_commands delete_others_posts delete_post manage_channel_roles convert_private_channel_to_public playbook_private_manage_roles', true, true); INSERT INTO public.roles VALUES ('tj3atgnwjfrt7emz8pgqmh5z4c', 'team_post_all', 'authentication.roles.team_post_all.name', 'authentication.roles.team_post_all.description', 0, 1662230812030, 0, ' create_post use_channel_mentions', false, true); INSERT INTO public.roles VALUES ('d54xjt4sat8h7dqwu6i35jocuy', 'system_user', 'authentication.roles.global_user.name', 'authentication.roles.global_user.description', 0, 1662230812030, 0, ' create_emojis edit_custom_group manage_custom_group_members view_members create_custom_group create_team create_direct_channel delete_custom_group list_public_teams delete_emojis create_group_channel join_public_teams', true, true); -INSERT INTO public.roles VALUES ('mrejpofuoffiiynqcsi98es9ya', 'channel_guest', 'authentication.roles.channel_guest.name', 'authentication.roles.channel_guest.description', 0, 1662230812026, 0, ' upload_file edit_post create_post use_channel_mentions use_slash_commands read_channel add_reaction remove_reaction', true, true); +INSERT INTO public.roles VALUES ('mrejpofuoffiiynqcsi98es9ya', 'channel_guest', 'authentication.roles.channel_guest.name', 'authentication.roles.channel_guest.description', 0, 1662230812026, 0, ' upload_file edit_post create_post use_channel_mentions read_channel add_reaction remove_reaction', true, true); INSERT INTO public.roles VALUES ('4fk7nq4jgi8t7n1re79eb7i96c', 'custom_group_user', 'authentication.roles.custom_group_user.name', 'authentication.roles.custom_group_user.description', 1662230811506, 1662230812031, 0, '', false, false); INSERT INTO public.roles VALUES ('qmagi7t1ifbjuy5r1pp53eoryo', 'playbook_admin', 'authentication.roles.playbook_admin.name', 'authentication.roles.playbook_admin.description', 1662230811507, 1662230812032, 0, ' playbook_public_manage_roles playbook_public_manage_properties playbook_private_manage_members playbook_private_manage_roles playbook_private_manage_properties playbook_public_make_private playbook_public_manage_members', true, true); INSERT INTO public.roles VALUES ('ozgjpnirx7fdjp3i1i8jrg1kwc', 'system_custom_group_admin', 'authentication.roles.system_custom_group_admin.name', 'authentication.roles.system_custom_group_admin.description', 1662230811510, 1662230812032, 0, ' create_custom_group edit_custom_group delete_custom_group manage_custom_group_members', false, true); INSERT INTO public.roles VALUES ('pfnwpqmbmjrexgqbxdu61wfd3w', 'playbook_member', 'authentication.roles.playbook_member.name', 'authentication.roles.playbook_member.description', 1662230811533, 1662230812034, 0, ' playbook_public_view playbook_public_manage_members playbook_public_manage_properties playbook_private_view playbook_private_manage_members playbook_private_manage_properties run_create', true, true); INSERT INTO public.roles VALUES ('dj5zm9bxbidi9ritmana9t1sxh', 'run_admin', 'authentication.roles.run_admin.name', 'authentication.roles.run_admin.description', 1662230811534, 1662230812035, 0, ' run_manage_members run_manage_properties', true, true); INSERT INTO public.roles VALUES ('abrocgnx8pni7esbrmb4pjxhoe', 'run_member', 'authentication.roles.run_member.name', 'authentication.roles.run_member.description', 1662230811534, 1662230812036, 0, ' run_view', true, true); -INSERT INTO public.roles VALUES ('ha8u9qxwx3dm8mnbq8sfi7ugdc', 'system_admin', 'authentication.roles.global_admin.name', 'authentication.roles.global_admin.description', 0, 1662230812038, 0, ' read_public_channel_groups manage_public_channel_properties create_post_ephemeral sysconsole_write_site_localization sysconsole_write_billing sysconsole_read_site_file_sharing_and_downloads playbook_public_manage_roles sysconsole_read_integrations_gif delete_emojis sysconsole_write_experimental_features sysconsole_write_site_posts add_ldap_private_cert use_group_mentions sysconsole_read_authentication_openid add_user_to_team sysconsole_read_user_management_channels sysconsole_write_environment_high_availability sysconsole_write_site_announcement_banner sysconsole_read_site_notices sysconsole_write_user_management_teams convert_public_channel_to_private sysconsole_read_reporting_server_logs manage_system_wide_oauth revoke_user_access_token invalidate_caches sysconsole_write_environment_push_notification_server sysconsole_read_site_emoji remove_others_reactions sysconsole_write_reporting_server_logs sysconsole_write_user_management_permissions sysconsole_read_site_posts assign_bot sysconsole_write_authentication_password add_saml_private_cert manage_jobs sysconsole_write_environment_developer use_channel_mentions add_ldap_public_cert purge_bleve_indexes playbook_public_manage_properties sysconsole_read_authentication_mfa read_public_channel sysconsole_read_environment_image_proxy import_team sysconsole_read_reporting_team_statistics sysconsole_write_user_management_channels list_private_teams sysconsole_read_user_management_groups join_private_teams sysconsole_read_compliance_data_retention_policy list_public_teams sysconsole_read_site_localization sysconsole_write_authentication_guest_access sysconsole_read_compliance_compliance_monitoring sysconsole_read_environment_developer edit_others_posts sysconsole_read_experimental_bleve read_audits sysconsole_write_authentication_email sysconsole_write_experimental_bleve sysconsole_read_environment_push_notification_server read_elasticsearch_post_aggregation_job remove_ldap_private_cert manage_team manage_bots sysconsole_write_environment_session_lengths sysconsole_write_user_management_users sysconsole_write_environment_file_storage invite_user join_public_channels create_direct_channel sysconsole_read_site_users_and_teams manage_slash_commands playbook_public_view sysconsole_write_compliance_custom_terms_of_service purge_elasticsearch_indexes sysconsole_read_authentication_email test_ldap sysconsole_write_plugins manage_outgoing_webhooks create_bot create_compliance_export_job get_logs create_private_channel get_saml_metadata_from_idp read_elasticsearch_post_indexing_job get_analytics manage_incoming_webhooks sysconsole_read_authentication_saml invite_guest manage_shared_channels create_public_channel sysconsole_write_site_file_sharing_and_downloads sysconsole_read_environment_rate_limiting manage_public_channel_members sysconsole_read_environment_file_storage sysconsole_read_environment_performance_monitoring sysconsole_write_environment_performance_monitoring sysconsole_write_integrations_gif create_post_public playbook_public_manage_members upload_file sysconsole_write_reporting_team_statistics manage_team_roles sysconsole_read_site_notifications delete_public_channel sysconsole_write_compliance_compliance_monitoring create_ldap_sync_job create_data_retention_job sysconsole_write_environment_smtp manage_custom_group_members manage_others_slash_commands read_ldap_sync_job sysconsole_read_integrations_bot_accounts read_others_bots read_bots sysconsole_read_authentication_ldap demote_to_guest remove_saml_public_cert create_post_bleve_indexes_job sysconsole_read_user_management_teams sysconsole_write_about_edition_and_license remove_ldap_public_cert read_channel sysconsole_read_environment_database sysconsole_write_authentication_signup test_s3 sysconsole_read_environment_high_availability manage_roles sysconsole_write_site_notifications run_view sysconsole_write_authentication_saml invalidate_email_invite playbook_private_view read_compliance_export_job list_users_without_team sysconsole_read_compliance_compliance_export sysconsole_write_integrations_cors promote_guest manage_oauth read_data_retention_job sysconsole_write_experimental_feature_flags sysconsole_read_environment_session_lengths manage_license_information sysconsole_write_authentication_ldap assign_system_admin_role create_post read_private_channel_groups add_saml_idp_cert playbook_private_create manage_private_channel_properties sysconsole_read_compliance_custom_terms_of_service sysconsole_read_integrations_integration_management sysconsole_read_billing sysconsole_read_authentication_password delete_private_channel sysconsole_write_site_notices create_elasticsearch_post_indexing_job test_email sysconsole_write_environment_database recycle_database_connections edit_brand sysconsole_write_authentication_mfa remove_user_from_team sysconsole_write_user_management_system_roles add_reaction remove_saml_private_cert sysconsole_read_environment_web_server run_create sysconsole_read_authentication_guest_access sysconsole_read_about_edition_and_license run_manage_properties create_user_access_token manage_others_incoming_webhooks create_elasticsearch_post_aggregation_job sysconsole_write_user_management_groups sysconsole_read_experimental_feature_flags create_team sysconsole_read_environment_elasticsearch join_public_teams sysconsole_read_user_management_users sysconsole_read_integrations_cors sysconsole_read_environment_smtp manage_secure_connections manage_channel_roles edit_other_users delete_others_emojis sysconsole_write_site_users_and_teams add_saml_public_cert sysconsole_read_site_announcement_banner create_custom_group download_compliance_export_result create_group_channel get_saml_cert_status sysconsole_read_site_public_links manage_system create_emojis sysconsole_read_authentication_signup sysconsole_write_environment_image_proxy list_team_channels remove_saml_idp_cert sysconsole_read_plugins sysconsole_read_site_customization sysconsole_write_site_customization use_slash_commands playbook_private_manage_roles delete_custom_group delete_others_posts sysconsole_write_compliance_data_retention_policy sysconsole_write_environment_logging test_elasticsearch playbook_public_make_private sysconsole_write_site_public_links edit_post playbook_private_make_public sysconsole_write_environment_elasticsearch test_site_url sysconsole_write_compliance_compliance_export playbook_private_manage_members delete_post reload_config edit_custom_group sysconsole_read_user_management_system_roles sysconsole_write_reporting_site_statistics sysconsole_write_site_emoji read_user_access_token sysconsole_write_environment_rate_limiting view_members sysconsole_write_integrations_bot_accounts manage_others_bots manage_others_outgoing_webhooks sysconsole_read_environment_logging sysconsole_read_experimental_features sysconsole_write_authentication_openid manage_private_channel_members read_jobs sysconsole_write_environment_web_server read_license_information sysconsole_read_user_management_permissions view_team convert_private_channel_to_public sysconsole_read_reporting_site_statistics get_public_link read_other_users_teams sysconsole_write_integrations_integration_management run_manage_members playbook_public_create remove_reaction playbook_private_manage_properties', true, true); +INSERT INTO public.roles VALUES ('ha8u9qxwx3dm8mnbq8sfi7ugdc', 'system_admin', 'authentication.roles.global_admin.name', 'authentication.roles.global_admin.description', 0, 1662230812038, 0, ' read_public_channel_groups manage_public_channel_properties create_post_ephemeral sysconsole_write_site_localization sysconsole_write_billing sysconsole_read_site_file_sharing_and_downloads playbook_public_manage_roles sysconsole_read_integrations_gif delete_emojis sysconsole_write_experimental_features sysconsole_write_site_posts add_ldap_private_cert use_group_mentions sysconsole_read_authentication_openid add_user_to_team sysconsole_read_user_management_channels sysconsole_write_environment_high_availability sysconsole_write_site_announcement_banner sysconsole_read_site_notices sysconsole_write_user_management_teams convert_public_channel_to_private sysconsole_read_reporting_server_logs manage_system_wide_oauth revoke_user_access_token invalidate_caches sysconsole_write_environment_push_notification_server sysconsole_read_site_emoji remove_others_reactions sysconsole_write_reporting_server_logs sysconsole_write_user_management_permissions sysconsole_read_site_posts assign_bot sysconsole_write_authentication_password add_saml_private_cert manage_jobs sysconsole_write_environment_developer use_channel_mentions add_ldap_public_cert purge_bleve_indexes playbook_public_manage_properties sysconsole_read_authentication_mfa read_public_channel sysconsole_read_environment_image_proxy import_team sysconsole_read_reporting_team_statistics sysconsole_write_user_management_channels list_private_teams sysconsole_read_user_management_groups join_private_teams sysconsole_read_compliance_data_retention_policy list_public_teams sysconsole_read_site_localization sysconsole_write_authentication_guest_access sysconsole_read_compliance_compliance_monitoring sysconsole_read_environment_developer edit_others_posts sysconsole_read_experimental_bleve read_audits sysconsole_write_authentication_email sysconsole_write_experimental_bleve sysconsole_read_environment_push_notification_server read_elasticsearch_post_aggregation_job remove_ldap_private_cert manage_team manage_bots sysconsole_write_environment_session_lengths sysconsole_write_user_management_users sysconsole_write_environment_file_storage invite_user join_public_channels create_direct_channel sysconsole_read_site_users_and_teams manage_slash_commands playbook_public_view sysconsole_write_compliance_custom_terms_of_service purge_elasticsearch_indexes sysconsole_read_authentication_email test_ldap sysconsole_write_plugins manage_outgoing_webhooks create_bot create_compliance_export_job get_logs create_private_channel get_saml_metadata_from_idp read_elasticsearch_post_indexing_job get_analytics manage_incoming_webhooks sysconsole_read_authentication_saml invite_guest manage_shared_channels create_public_channel sysconsole_write_site_file_sharing_and_downloads sysconsole_read_environment_rate_limiting manage_public_channel_members sysconsole_read_environment_file_storage sysconsole_read_environment_performance_monitoring sysconsole_write_environment_performance_monitoring sysconsole_write_integrations_gif create_post_public playbook_public_manage_members upload_file sysconsole_write_reporting_team_statistics manage_team_roles sysconsole_read_site_notifications delete_public_channel sysconsole_write_compliance_compliance_monitoring create_ldap_sync_job create_data_retention_job sysconsole_write_environment_smtp manage_custom_group_members manage_others_slash_commands read_ldap_sync_job sysconsole_read_integrations_bot_accounts read_others_bots read_bots sysconsole_read_authentication_ldap demote_to_guest remove_saml_public_cert create_post_bleve_indexes_job sysconsole_read_user_management_teams sysconsole_write_about_edition_and_license remove_ldap_public_cert read_channel sysconsole_read_environment_database sysconsole_write_authentication_signup test_s3 sysconsole_read_environment_high_availability manage_roles sysconsole_write_site_notifications run_view sysconsole_write_authentication_saml invalidate_email_invite playbook_private_view read_compliance_export_job list_users_without_team sysconsole_read_compliance_compliance_export sysconsole_write_integrations_cors promote_guest manage_oauth read_data_retention_job sysconsole_write_experimental_feature_flags sysconsole_read_environment_session_lengths manage_license_information sysconsole_write_authentication_ldap assign_system_admin_role create_post read_private_channel_groups add_saml_idp_cert playbook_private_create manage_private_channel_properties sysconsole_read_compliance_custom_terms_of_service sysconsole_read_integrations_integration_management sysconsole_read_billing sysconsole_read_authentication_password delete_private_channel sysconsole_write_site_notices create_elasticsearch_post_indexing_job test_email sysconsole_write_environment_database recycle_database_connections edit_brand sysconsole_write_authentication_mfa remove_user_from_team sysconsole_write_user_management_system_roles add_reaction remove_saml_private_cert sysconsole_read_environment_web_server run_create sysconsole_read_authentication_guest_access sysconsole_read_about_edition_and_license run_manage_properties create_user_access_token manage_others_incoming_webhooks create_elasticsearch_post_aggregation_job sysconsole_write_user_management_groups sysconsole_read_experimental_feature_flags create_team sysconsole_read_environment_elasticsearch join_public_teams sysconsole_read_user_management_users sysconsole_read_integrations_cors sysconsole_read_environment_smtp manage_secure_connections manage_channel_roles edit_other_users delete_others_emojis sysconsole_write_site_users_and_teams add_saml_public_cert sysconsole_read_site_announcement_banner create_custom_group download_compliance_export_result create_group_channel get_saml_cert_status sysconsole_read_site_public_links manage_system create_emojis sysconsole_read_authentication_signup sysconsole_write_environment_image_proxy list_team_channels remove_saml_idp_cert sysconsole_read_plugins sysconsole_read_site_customization sysconsole_write_site_customization playbook_private_manage_roles delete_custom_group delete_others_posts sysconsole_write_compliance_data_retention_policy sysconsole_write_environment_logging test_elasticsearch playbook_public_make_private sysconsole_write_site_public_links edit_post playbook_private_make_public sysconsole_write_environment_elasticsearch test_site_url sysconsole_write_compliance_compliance_export playbook_private_manage_members delete_post reload_config edit_custom_group sysconsole_read_user_management_system_roles sysconsole_write_reporting_site_statistics sysconsole_write_site_emoji read_user_access_token sysconsole_write_environment_rate_limiting view_members sysconsole_write_integrations_bot_accounts manage_others_bots manage_others_outgoing_webhooks sysconsole_read_environment_logging sysconsole_read_experimental_features sysconsole_write_authentication_openid manage_private_channel_members read_jobs sysconsole_write_environment_web_server read_license_information sysconsole_read_user_management_permissions view_team convert_private_channel_to_public sysconsole_read_reporting_site_statistics get_public_link read_other_users_teams sysconsole_write_integrations_integration_management run_manage_members playbook_public_create remove_reaction playbook_private_manage_properties', true, true); INSERT INTO public.roles VALUES ('hm1bxei8b3d68e4j95tqnndppw', 'system_manager', 'authentication.roles.system_manager.name', 'authentication.roles.system_manager.description', 0, 1662230812025, 0, ' manage_private_channel_members join_public_teams sysconsole_write_site_announcement_banner sysconsole_write_site_emoji manage_public_channel_members purge_elasticsearch_indexes sysconsole_read_authentication_openid sysconsole_read_about_edition_and_license edit_brand sysconsole_read_reporting_team_statistics sysconsole_read_site_file_sharing_and_downloads sysconsole_read_user_management_teams read_private_channel_groups delete_public_channel sysconsole_read_site_customization sysconsole_write_site_notices sysconsole_read_authentication_email sysconsole_write_environment_file_storage sysconsole_read_user_management_permissions sysconsole_read_reporting_site_statistics test_s3 sysconsole_write_user_management_permissions sysconsole_read_environment_rate_limiting read_license_information sysconsole_read_environment_file_storage sysconsole_write_environment_elasticsearch invalidate_caches sysconsole_read_integrations_cors sysconsole_write_user_management_teams add_user_to_team sysconsole_read_environment_performance_monitoring get_logs sysconsole_write_environment_high_availability sysconsole_read_authentication_signup manage_public_channel_properties sysconsole_write_integrations_integration_management read_elasticsearch_post_indexing_job sysconsole_read_user_management_groups view_team sysconsole_write_environment_rate_limiting sysconsole_read_authentication_guest_access sysconsole_read_environment_elasticsearch manage_team reload_config manage_team_roles test_ldap sysconsole_read_site_public_links sysconsole_read_authentication_saml sysconsole_write_integrations_cors read_public_channel_groups sysconsole_write_site_users_and_teams sysconsole_read_integrations_gif get_analytics create_elasticsearch_post_indexing_job sysconsole_read_authentication_ldap sysconsole_read_site_announcement_banner test_site_url sysconsole_read_site_localization sysconsole_write_environment_push_notification_server sysconsole_write_integrations_bot_accounts sysconsole_write_environment_performance_monitoring sysconsole_write_site_posts sysconsole_read_environment_logging read_elasticsearch_post_aggregation_job sysconsole_write_site_localization sysconsole_write_environment_database sysconsole_read_site_posts sysconsole_write_environment_developer sysconsole_read_site_emoji sysconsole_read_plugins create_elasticsearch_post_aggregation_job manage_channel_roles sysconsole_write_user_management_groups remove_user_from_team read_ldap_sync_job sysconsole_write_site_notifications recycle_database_connections test_email sysconsole_read_site_notifications list_public_teams sysconsole_write_site_customization sysconsole_read_environment_smtp sysconsole_read_authentication_mfa sysconsole_read_integrations_integration_management sysconsole_read_user_management_channels sysconsole_read_reporting_server_logs sysconsole_write_site_public_links test_elasticsearch sysconsole_write_environment_smtp sysconsole_read_environment_push_notification_server sysconsole_write_environment_web_server sysconsole_write_environment_logging sysconsole_read_environment_session_lengths sysconsole_read_site_notices sysconsole_read_environment_high_availability join_private_teams sysconsole_read_authentication_password sysconsole_read_environment_developer delete_private_channel sysconsole_read_integrations_bot_accounts sysconsole_write_environment_session_lengths convert_private_channel_to_public sysconsole_read_environment_database sysconsole_read_environment_image_proxy convert_public_channel_to_private manage_private_channel_properties sysconsole_write_site_file_sharing_and_downloads read_public_channel list_private_teams sysconsole_write_integrations_gif sysconsole_read_environment_web_server sysconsole_read_site_users_and_teams sysconsole_write_user_management_channels read_channel sysconsole_write_environment_image_proxy', false, true); INSERT INTO public.roles VALUES ('f9drbz6cyjdmb8jof6smiqya7h', 'system_user_manager', 'authentication.roles.system_user_manager.name', 'authentication.roles.system_user_manager.description', 0, 1662230812028, 0, ' manage_team_roles sysconsole_read_authentication_saml manage_public_channel_members manage_channel_roles add_user_to_team sysconsole_read_authentication_ldap read_public_channel_groups join_public_teams convert_private_channel_to_public join_private_teams sysconsole_read_user_management_teams list_public_teams sysconsole_read_authentication_email list_private_teams sysconsole_read_authentication_signup read_public_channel sysconsole_read_authentication_mfa sysconsole_read_authentication_guest_access test_ldap manage_private_channel_members sysconsole_read_user_management_permissions read_channel remove_user_from_team delete_public_channel sysconsole_write_user_management_channels delete_private_channel sysconsole_read_authentication_openid sysconsole_write_user_management_teams manage_team sysconsole_read_user_management_groups view_team sysconsole_write_user_management_groups sysconsole_read_user_management_channels manage_public_channel_properties manage_private_channel_properties sysconsole_read_authentication_password read_ldap_sync_job convert_public_channel_to_private read_private_channel_groups', false, true); INSERT INTO public.roles VALUES ('tkioqq1sgtribqgjbzwop1846c', 'system_read_only_admin', 'authentication.roles.system_read_only_admin.name', 'authentication.roles.system_read_only_admin.description', 0, 1662230812033, 0, ' sysconsole_read_integrations_bot_accounts sysconsole_read_authentication_openid sysconsole_read_user_management_users sysconsole_read_authentication_saml read_ldap_sync_job read_other_users_teams sysconsole_read_user_management_permissions download_compliance_export_result sysconsole_read_environment_smtp sysconsole_read_site_localization read_public_channel read_audits sysconsole_read_compliance_custom_terms_of_service read_data_retention_job sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy sysconsole_read_environment_developer sysconsole_read_site_file_sharing_and_downloads sysconsole_read_user_management_channels read_elasticsearch_post_indexing_job sysconsole_read_authentication_mfa sysconsole_read_compliance_compliance_monitoring sysconsole_read_authentication_signup sysconsole_read_authentication_ldap sysconsole_read_authentication_password get_analytics sysconsole_read_site_posts sysconsole_read_environment_performance_monitoring sysconsole_read_compliance_compliance_export sysconsole_read_integrations_integration_management test_ldap sysconsole_read_environment_file_storage sysconsole_read_environment_logging sysconsole_read_user_management_groups sysconsole_read_environment_high_availability sysconsole_read_environment_database sysconsole_read_environment_elasticsearch sysconsole_read_environment_push_notification_server sysconsole_read_site_notices read_compliance_export_job read_license_information sysconsole_read_environment_session_lengths read_private_channel_groups sysconsole_read_integrations_gif read_elasticsearch_post_aggregation_job sysconsole_read_experimental_bleve sysconsole_read_reporting_team_statistics sysconsole_read_about_edition_and_license sysconsole_read_environment_image_proxy sysconsole_read_site_customization sysconsole_read_environment_rate_limiting view_team sysconsole_read_site_announcement_banner sysconsole_read_environment_web_server get_logs sysconsole_read_experimental_feature_flags sysconsole_read_integrations_cors sysconsole_read_authentication_guest_access sysconsole_read_plugins read_channel list_public_teams sysconsole_read_user_management_teams sysconsole_read_reporting_server_logs sysconsole_read_experimental_features sysconsole_read_authentication_email sysconsole_read_site_notifications sysconsole_read_site_users_and_teams sysconsole_read_reporting_site_statistics read_public_channel_groups list_private_teams sysconsole_read_site_public_links', false, true); diff --git a/server/model/permission.go b/server/model/permission.go index 231154e2d4..e91809127e 100644 --- a/server/model/permission.go +++ b/server/model/permission.go @@ -21,10 +21,6 @@ type Permission struct { var PermissionInviteUser *Permission var PermissionAddUserToTeam *Permission - -// Deprecated: PermissionCreatePost should be used to determine if a slash command can be executed. -// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274 -var PermissionUseSlashCommands *Permission var PermissionManageSlashCommands *Permission var PermissionManageOthersSlashCommands *Permission var PermissionCreatePublicChannel *Permission @@ -393,12 +389,6 @@ func initializePermissions() { "authentication.permissions.add_user_to_team.description", PermissionScopeTeam, } - PermissionUseSlashCommands = &Permission{ - "use_slash_commands", - "authentication.permissions.team_use_slash_commands.name", - "authentication.permissions.team_use_slash_commands.description", - PermissionScopeChannel, - } PermissionManageSlashCommands = &Permission{ "manage_slash_commands", "authentication.permissions.manage_slash_commands.name", @@ -2318,7 +2308,6 @@ func initializePermissions() { } ChannelScopedPermissions := []*Permission{ - PermissionUseSlashCommands, PermissionManagePublicChannelMembers, PermissionManagePrivateChannelMembers, PermissionManageChannelRoles, diff --git a/server/model/role.go b/server/model/role.go index 2c7a8fbf7b..4fba0c64f7 100644 --- a/server/model/role.go +++ b/server/model/role.go @@ -755,7 +755,6 @@ func MakeDefaultRoles() map[string]*Role { PermissionEditPost.Id, PermissionCreatePost.Id, PermissionUseChannelMentions.Id, - PermissionUseSlashCommands.Id, }, SchemeManaged: true, BuiltIn: true, @@ -774,7 +773,6 @@ func MakeDefaultRoles() map[string]*Role { PermissionGetPublicLink.Id, PermissionCreatePost.Id, PermissionUseChannelMentions.Id, - PermissionUseSlashCommands.Id, PermissionManagePublicChannelProperties.Id, PermissionDeletePublicChannel.Id, PermissionManagePrivateChannelProperties.Id, diff --git a/server/model/role_test.go b/server/model/role_test.go index 431a3286f1..d6142841dc 100644 --- a/server/model/role_test.go +++ b/server/model/role_test.go @@ -71,7 +71,6 @@ func TestRolePatchFromChannelModerationsPatch(t *testing.T) { PermissionManagePublicChannelMembers.Id, PermissionUploadFile.Id, PermissionGetPublicLink.Id, - PermissionUseSlashCommands.Id, } baseModeratedPermissions := []string{ diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts index 069f4e7e53..19ff5a3ccb 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/permissions.ts @@ -4,7 +4,6 @@ const values = { INVITE_USER: 'invite_user', ADD_USER_TO_TEAM: 'add_user_to_team', - USE_SLASH_COMMANDS: 'use_slash_commands', MANAGE_SLASH_COMMANDS: 'manage_slash_commands', MANAGE_OTHERS_SLASH_COMMANDS: 'manage_others_slash_commands', CREATE_PUBLIC_CHANNEL: 'create_public_channel', diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 97ea8d65d0..29f0ef75f5 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -1136,7 +1136,6 @@ export const PermissionsScope = { [Permissions.INVITE_USER]: 'team_scope', [Permissions.INVITE_GUEST]: 'team_scope', [Permissions.ADD_USER_TO_TEAM]: 'team_scope', - [Permissions.USE_SLASH_COMMANDS]: 'channel_scope', [Permissions.MANAGE_SLASH_COMMANDS]: 'team_scope', [Permissions.MANAGE_OTHERS_SLASH_COMMANDS]: 'team_scope', [Permissions.CREATE_PUBLIC_CHANNEL]: 'team_scope', @@ -1250,7 +1249,6 @@ export const DefaultRolePermissions = { Permissions.UPLOAD_FILE, Permissions.GET_PUBLIC_LINK, Permissions.CREATE_POST, - Permissions.USE_SLASH_COMMANDS, Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS, Permissions.DELETE_POST, Permissions.EDIT_POST, @@ -1315,7 +1313,6 @@ export const DefaultRolePermissions = { Permissions.ADD_REACTION, Permissions.REMOVE_REACTION, Permissions.USE_CHANNEL_MENTIONS, - Permissions.USE_SLASH_COMMANDS, Permissions.READ_CHANNEL, Permissions.UPLOAD_FILE, Permissions.CREATE_POST, From b7a5f22bcfcbdf5eac0e6b9331f250e36d050d9e Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Tue, 25 Apr 2023 15:34:41 +0800 Subject: [PATCH 43/69] fix/e2e: remove use_slash_commands in roles (#23098) --- e2e-tests/cypress/tests/support/api/role.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e-tests/cypress/tests/support/api/role.js b/e2e-tests/cypress/tests/support/api/role.js index 6a8eed7082..d53862b72d 100644 --- a/e2e-tests/cypress/tests/support/api/role.js +++ b/e2e-tests/cypress/tests/support/api/role.js @@ -10,14 +10,14 @@ import xor from 'lodash.xor'; export const defaultRolesPermissions = { channel_admin: 'use_channel_mentions remove_reaction manage_public_channel_members use_group_mentions manage_channel_roles manage_private_channel_members add_reaction read_public_channel_groups create_post read_private_channel_groups', - channel_guest: 'upload_file edit_post create_post use_channel_mentions use_slash_commands read_channel add_reaction remove_reaction', - channel_user: 'manage_private_channel_members read_public_channel_groups delete_post read_private_channel_groups use_group_mentions manage_private_channel_properties delete_public_channel use_slash_commands add_reaction manage_public_channel_properties edit_post upload_file use_channel_mentions get_public_link read_channel delete_private_channel manage_public_channel_members create_post remove_reaction', + channel_guest: 'upload_file edit_post create_post use_channel_mentions read_channel add_reaction remove_reaction', + channel_user: 'manage_private_channel_members read_public_channel_groups delete_post read_private_channel_groups use_group_mentions manage_private_channel_properties delete_public_channel add_reaction manage_public_channel_properties edit_post upload_file use_channel_mentions get_public_link read_channel delete_private_channel manage_public_channel_members create_post remove_reaction', custom_group_user: '', playbook_admin: 'playbook_private_manage_properties playbook_public_make_private playbook_public_manage_members playbook_public_manage_roles playbook_public_manage_properties playbook_private_manage_members playbook_private_manage_roles', playbook_member: 'playbook_public_view playbook_public_manage_members playbook_public_manage_properties playbook_private_view playbook_private_manage_members playbook_private_manage_properties run_create', run_admin: 'run_manage_properties run_manage_members', run_member: 'run_view', - system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_slash_commands sysconsole_read_authentication_ldap read_channel sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user manage_others_outgoing_webhooks create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job purge_bleve_indexes sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml create_post_bleve_indexes_job invalidate_caches sysconsole_write_experimental_bleve view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_others_slash_commands manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization use_slash_commands manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth delete_others_emojis sysconsole_write_integrations_gif manage_incoming_webhooks sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_read_experimental_bleve sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license manage_others_incoming_webhooks run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private', + system_admin: 'sysconsole_write_environment_elasticsearch playbook_public_manage_properties sysconsole_write_authentication_ldap run_view manage_jobs manage_roles playbook_public_create manage_public_channel_properties sysconsole_read_plugins delete_post purge_elasticsearch_indexes sysconsole_read_integrations_bot_accounts read_data_retention_job manage_private_channel_members create_elasticsearch_post_indexing_job sysconsole_read_authentication_guest_access create_elasticsearch_post_aggregation_job join_public_teams sysconsole_read_site_public_links add_saml_idp_cert sysconsole_write_site_announcement_banner sysconsole_write_site_notices sysconsole_read_experimental_feature_flags sysconsole_read_site_users_and_teams manage_slash_commands sysconsole_read_authentication_ldap read_channel sysconsole_write_authentication_password list_users_without_team sysconsole_read_authentication_email add_saml_public_cert playbook_private_create promote_guest sysconsole_read_user_management_system_roles manage_public_channel_members create_data_retention_job add_saml_private_cert sysconsole_write_user_management_users sysconsole_read_compliance_compliance_monitoring playbook_public_manage_members sysconsole_write_environment_database sysconsole_write_user_management_teams playbook_private_manage_roles read_public_channel sysconsole_write_plugins sysconsole_read_authentication_openid sysconsole_write_user_management_groups sysconsole_write_site_file_sharing_and_downloads playbook_private_manage_properties sysconsole_read_site_customization join_public_channels add_user_to_team restore_custom_group download_compliance_export_result sysconsole_write_user_management_system_roles sysconsole_write_environment_session_lengths create_custom_group manage_private_channel_properties create_post_public remove_ldap_private_cert sysconsole_write_site_public_links import_team sysconsole_read_environment_developer sysconsole_read_environment_database sysconsole_read_environment_web_server use_channel_mentions view_team remove_others_reactions sysconsole_read_environment_session_lengths sysconsole_write_integrations_bot_accounts playbook_public_view use_group_mentions sysconsole_write_environment_web_server add_ldap_private_cert read_public_channel_groups invite_guest sysconsole_read_environment_smtp create_post sysconsole_read_about_edition_and_license sysconsole_read_authentication_signup sysconsole_read_authentication_saml sysconsole_read_environment_file_storage sysconsole_write_experimental_feature_flags sysconsole_write_site_localization sysconsole_write_environment_rate_limiting sysconsole_read_environment_rate_limiting sysconsole_read_products_boards get_saml_cert_status sysconsole_read_environment_high_availability manage_secure_connections read_compliance_export_job sysconsole_write_compliance_custom_terms_of_service read_user_access_token edit_post sysconsole_write_environment_logging sysconsole_read_environment_push_notification_server sysconsole_write_site_customization read_other_users_teams read_elasticsearch_post_aggregation_job sysconsole_write_compliance_data_retention_policy sysconsole_read_user_management_permissions sysconsole_read_site_emoji sysconsole_read_compliance_data_retention_policy read_license_information sysconsole_read_experimental_features read_deleted_posts sysconsole_read_environment_logging sysconsole_read_reporting_site_statistics test_elasticsearch sysconsole_read_site_posts add_reaction sysconsole_write_authentication_signup manage_outgoing_webhooks create_post_ephemeral sysconsole_read_environment_image_proxy invite_user manage_others_outgoing_webhooks create_user_access_token sysconsole_write_environment_image_proxy sysconsole_write_products_boards read_elasticsearch_post_indexing_job purge_bleve_indexes sysconsole_write_environment_performance_monitoring sysconsole_write_authentication_guest_access sysconsole_read_compliance_custom_terms_of_service edit_others_posts sysconsole_write_billing get_saml_metadata_from_idp sysconsole_write_authentication_saml create_post_bleve_indexes_job invalidate_caches sysconsole_write_experimental_bleve view_members manage_others_bots run_create join_private_teams convert_private_channel_to_public read_audits assign_bot read_jobs remove_user_from_team revoke_user_access_token manage_team sysconsole_read_reporting_server_logs get_public_link manage_others_slash_commands manage_system delete_public_channel read_private_channel_groups sysconsole_read_authentication_mfa delete_emojis list_private_teams create_emojis sysconsole_read_billing sysconsole_write_site_emoji invalidate_email_invite sysconsole_write_environment_file_storage sysconsole_write_compliance_compliance_monitoring remove_saml_public_cert sysconsole_read_compliance_compliance_export sysconsole_read_site_localization manage_team_roles list_public_teams get_logs sysconsole_write_integrations_integration_management sysconsole_read_integrations_cors manage_oauth delete_others_emojis sysconsole_write_integrations_gif manage_incoming_webhooks sysconsole_write_authentication_email create_private_channel playbook_private_make_public manage_bots add_ldap_public_cert remove_ldap_public_cert sysconsole_write_site_notifications sysconsole_write_environment_developer playbook_private_manage_members sysconsole_read_user_management_teams edit_custom_group remove_reaction playbook_public_manage_roles sysconsole_write_reporting_server_logs read_others_bots sysconsole_write_site_posts sysconsole_read_site_notifications sysconsole_read_authentication_password playbook_private_view manage_system_wide_oauth get_analytics list_team_channels sysconsole_write_user_management_channels delete_private_channel manage_custom_group_members test_s3 create_ldap_sync_job sysconsole_read_integrations_integration_management test_site_url recycle_database_connections sysconsole_read_site_announcement_banner test_email manage_shared_channels read_bots sysconsole_write_environment_smtp sysconsole_read_experimental_bleve sysconsole_write_environment_push_notification_server sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_write_reporting_site_statistics sysconsole_write_site_users_and_teams demote_to_guest create_team test_ldap remove_saml_idp_cert delete_others_posts edit_other_users sysconsole_write_reporting_team_statistics sysconsole_read_integrations_gif sysconsole_read_site_notices sysconsole_write_about_edition_and_license manage_others_incoming_webhooks run_manage_members create_bot sysconsole_write_authentication_mfa sysconsole_read_user_management_users assign_system_admin_role sysconsole_write_experimental_features edit_brand create_group_channel sysconsole_write_authentication_openid create_direct_channel manage_license_information reload_config manage_channel_roles sysconsole_read_user_management_groups create_compliance_export_job read_ldap_sync_job upload_file sysconsole_read_site_file_sharing_and_downloads delete_custom_group sysconsole_read_user_management_channels sysconsole_write_compliance_compliance_export remove_saml_private_cert sysconsole_read_environment_performance_monitoring create_public_channel sysconsole_write_integrations_cors sysconsole_write_environment_high_availability playbook_public_make_private run_manage_properties sysconsole_read_reporting_team_statistics convert_public_channel_to_private', system_custom_group_admin: 'create_custom_group edit_custom_group delete_custom_group restore_custom_group manage_custom_group_members', system_guest: 'create_group_channel create_direct_channel', system_manager: ' sysconsole_read_site_announcement_banner manage_private_channel_properties edit_brand read_private_channel_groups manage_private_channel_members manage_team_roles sysconsole_write_environment_session_lengths sysconsole_read_site_emoji sysconsole_write_environment_developer sysconsole_read_user_management_groups sysconsole_write_user_management_groups sysconsole_write_environment_rate_limiting delete_private_channel sysconsole_read_environment_performance_monitoring sysconsole_read_environment_rate_limiting sysconsole_write_user_management_teams sysconsole_write_integrations_integration_management sysconsole_write_site_public_links sysconsole_read_authentication_ldap sysconsole_write_integrations_cors reload_config sysconsole_write_user_management_channels sysconsole_read_environment_high_availability sysconsole_read_site_users_and_teams sysconsole_read_user_management_teams sysconsole_write_site_users_and_teams sysconsole_read_site_customization sysconsole_write_environment_high_availability sysconsole_read_integrations_bot_accounts sysconsole_read_authentication_guest_access sysconsole_read_site_public_links read_elasticsearch_post_indexing_job sysconsole_read_user_management_channels sysconsole_read_reporting_team_statistics invalidate_caches sysconsole_read_authentication_signup read_elasticsearch_post_aggregation_job sysconsole_write_environment_smtp manage_public_channel_members list_public_teams add_user_to_team sysconsole_read_environment_web_server sysconsole_read_site_localization get_logs sysconsole_write_site_posts sysconsole_write_integrations_bot_accounts sysconsole_write_user_management_permissions sysconsole_read_environment_elasticsearch sysconsole_read_environment_smtp list_private_teams read_public_channel_groups sysconsole_write_environment_file_storage sysconsole_write_integrations_gif manage_public_channel_properties sysconsole_write_environment_performance_monitoring sysconsole_write_site_notifications sysconsole_read_site_notifications sysconsole_read_environment_image_proxy sysconsole_write_site_announcement_banner sysconsole_write_site_emoji test_site_url sysconsole_read_integrations_gif sysconsole_write_environment_logging convert_public_channel_to_private get_analytics sysconsole_read_user_management_permissions sysconsole_write_environment_image_proxy test_elasticsearch recycle_database_connections sysconsole_write_site_localization sysconsole_read_reporting_server_logs create_elasticsearch_post_indexing_job sysconsole_read_reporting_site_statistics test_ldap delete_public_channel sysconsole_write_environment_push_notification_server read_license_information sysconsole_write_products_boards sysconsole_read_about_edition_and_license convert_private_channel_to_public sysconsole_read_integrations_integration_management create_elasticsearch_post_aggregation_job purge_elasticsearch_indexes sysconsole_read_environment_database join_public_teams sysconsole_read_authentication_email sysconsole_read_environment_push_notification_server view_team read_channel sysconsole_read_authentication_password read_ldap_sync_job sysconsole_read_integrations_cors sysconsole_read_environment_logging manage_team sysconsole_read_authentication_openid read_public_channel sysconsole_write_environment_elasticsearch sysconsole_read_plugins manage_channel_roles remove_user_from_team test_email sysconsole_write_site_file_sharing_and_downloads test_s3 sysconsole_read_site_file_sharing_and_downloads sysconsole_read_site_notices sysconsole_read_environment_file_storage join_private_teams sysconsole_read_products_boards sysconsole_read_environment_session_lengths sysconsole_write_environment_database sysconsole_read_authentication_saml sysconsole_read_authentication_mfa sysconsole_write_site_notices sysconsole_write_environment_web_server sysconsole_read_site_posts sysconsole_read_environment_developer sysconsole_write_site_customization', From 502708499d3da6593ca52f06d0a3601ebc06d9b8 Mon Sep 17 00:00:00 2001 From: Pantelis Vratsalis Date: Wed, 19 Apr 2023 13:56:49 +0300 Subject: [PATCH 44/69] [MM-51777] Wrap integrations backstage UI options --- webapp/channels/src/sass/utils/_flex.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/channels/src/sass/utils/_flex.scss b/webapp/channels/src/sass/utils/_flex.scss index 07f58478bd..c882a68a39 100644 --- a/webapp/channels/src/sass/utils/_flex.scss +++ b/webapp/channels/src/sass/utils/_flex.scss @@ -17,3 +17,7 @@ -ms-flex-positive: 1; -ms-flex-preferred-size: 0; } + +.flex-wrap { + flex-wrap: wrap; +} From 2c5eb631814a1dfeed37891fc3ce5be0120e0464 Mon Sep 17 00:00:00 2001 From: yasserfaraazkhan Date: Tue, 25 Apr 2023 17:11:25 +0530 Subject: [PATCH 45/69] chore: lint --- .../channels/channel/archived_channels_1_spec.js | 6 +++--- .../integration/channels/channel/more_channels_spec.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js index 579ed06b0f..d62984cca0 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.js @@ -16,9 +16,9 @@ describe('Leave an archived channel', () => { let testTeam; let offTopicUrl; const channelType = { - public : 'Channel Type: Public', - archived : 'Channel Type: Archived', - } + public: 'Channel Type: Public', + archived: 'Channel Type: Archived', + }; before(() => { cy.apiUpdateConfig({ TeamSettings: { diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index e1a496781a..d7fd550e37 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -15,9 +15,9 @@ import * as TIMEOUTS from '../../../fixtures/timeouts'; import {createPrivateChannel} from '../enterprise/elasticsearch_autocomplete/helpers'; const channelType = { - public : 'Channel Type: Public', - archived : 'Channel Type: Archived', -} + public: 'Channel Type: Public', + archived: 'Channel Type: Archived', +}; describe('Channels', () => { let testUser; From 826f6e561abb3bd48dbcbf9b1f3022f59a24e06a Mon Sep 17 00:00:00 2001 From: Daniel Schalla Date: Tue, 25 Apr 2023 14:59:31 +0200 Subject: [PATCH 46/69] Update DockerFile to Ubuntu LTS Base Image (#22987) Co-authored-by: Mattermost Build --- server/build/Dockerfile | 71 +++++------------------------------------ 1 file changed, 8 insertions(+), 63 deletions(-) diff --git a/server/build/Dockerfile b/server/build/Dockerfile index 2db70884b0..8017e620f1 100644 --- a/server/build/Dockerfile +++ b/server/build/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster-slim@sha256:5b0b1a9a54651bbe9d4d3ee96bbda2b2a1da3d2fa198ddebbced46dfdca7f216 +FROM ubuntu:jammy-20230308@sha256:7a57c69fe1e9d5b97c5fe649849e79f2cfc3bf11d10bbd5218b4eb61716aebe6 # Setting bash as our shell, and enabling pipefail option @@ -13,68 +13,13 @@ ARG MM_PACKAGE="https://releases.mattermost.com/7.10.0/mattermost-7.10.0-linux-a # # Install needed packages and indirect dependencies RUN apt-get update \ && apt-get install --no-install-recommends -y \ - ca-certificates=20200601~deb10u2 \ - curl=7.64.0-4+deb10u2 \ - mime-support=3.62 \ - unrtf=0.21.10-clean-1 \ - wv=1.2.9-4.2+b2 \ - poppler-utils=0.71.0-5 \ - tidy=2:5.6.0-10 \ - libssl1.1=1.1.1n-0+deb10u3 \ - sensible-utils=0.0.12 \ - libsasl2-modules-db=2.1.27+dfsg-1+deb10u2 \ - libsasl2-2=2.1.27+dfsg-1+deb10u2 \ - libldap-common=2.4.47+dfsg-3+deb10u7 \ - libldap-2.4-2=2.4.47+dfsg-3+deb10u7 \ - libicu63=63.1-6+deb10u3 \ - libxml2=2.9.4+dfsg1-7+deb10u4 \ - ucf=3.0038+nmu1 \ - openssl=1.1.1n-0+deb10u3 \ - libkeyutils1=1.6-6 \ - libkrb5support0=1.17-3+deb10u4 \ - libk5crypto3=1.17-3+deb10u4 \ - libkrb5-3=1.17-3+deb10u4 \ - libgssapi-krb5-2=1.17-3+deb10u4 \ - libnghttp2-14=1.36.0-2+deb10u1 \ - libpsl5=0.20.2-2 \ - librtmp1=2.4+20151223.gitfa8646d.1-2 \ - libssh2-1=1.8.0-2.1 \ - libcurl4=7.64.0-4+deb10u2 \ - fonts-dejavu-core=2.37-1 \ - fontconfig-config=2.13.1-2 \ - libbsd0=0.9.1-2+deb10u1 \ - libexpat1=2.2.6-2+deb10u4 \ - libpng16-16=1.6.36-6 \ - libfreetype6=2.9.1-3+deb10u2 \ - libfontconfig1=2.13.1-2 \ - libpixman-1-0=0.36.0-1 \ - libxau6=1:1.0.8-1+b2 \ - libxdmcp6=1:1.1.2-3 \ - libxcb1=1.13.1-2 \ - libx11-data=2:1.6.7-1+deb10u2 \ - libx11-6=2:1.6.7-1+deb10u2 \ - libxcb-render0=1.13.1-2 \ - libxcb-shm0=1.13.1-2 \ - libxext6=2:1.3.3-1+b2 \ - libxrender1=1:0.9.10-1 \ - libcairo2=1.16.0-4+deb10u1 \ - libcurl3-gnutls=7.64.0-4+deb10u6 \ - libglib2.0-0=2.58.3-2+deb10u3 \ - libgsf-1-common=1.14.45-1 \ - libgsf-1-114=1.14.45-1 \ - libjbig0=2.1-3.1+b2 \ - libjpeg62-turbo=1:1.5.2-2+deb10u1 \ - liblcms2-2=2.9-3 \ - libnspr4=2:4.20-1 \ - libsqlite3-0=3.27.2-3+deb10u1 \ - libnss3=2:3.42.1-1+deb10u5 \ - libopenjp2-7=2.3.0-2+deb10u2 \ - libwebp6=0.6.1-2+deb10u1 \ - libtiff5=4.1.0+git191117-2~deb10u4 \ - libpoppler82=0.71.0-5 \ - libtidy5deb1=2:5.6.0-10 \ - libwmf0.2-7=0.2.8.4-14 \ - libwv-1.2-4=1.2.9-4.2+b2 \ + ca-certificates \ + curl \ + mime-support \ + unrtf \ + wv \ + poppler-utils \ + tidy \ && rm -rf /var/lib/apt/lists/* # Set mattermost group/user and download Mattermost From 33af25f2abc40b03a7102b5a8c198ce37f891fdb Mon Sep 17 00:00:00 2001 From: Ashish Dhama <16203333+AshishDhama@users.noreply.github.com> Date: Tue, 25 Apr 2023 18:44:18 +0530 Subject: [PATCH 47/69] [MM-52119]: Add condition for bot tag for webhook post if a bot account is used for webhook (#22926) * Add condition for bot tag for webhook post if a bot account used for webhook Co-authored-by: Mattermost Build --- webapp/channels/src/components/post/user_profile.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webapp/channels/src/components/post/user_profile.tsx b/webapp/channels/src/components/post/user_profile.tsx index f9556de359..ca2d0ea41e 100644 --- a/webapp/channels/src/components/post/user_profile.tsx +++ b/webapp/channels/src/components/post/user_profile.tsx @@ -95,7 +95,11 @@ const PostUserProfile = (props: Props): JSX.Element | null => { /> ); - botIndicator = (); + // user profile component checks and add bot tag in case webhook is from bot account, but if webhook is from user account we need this. + + if (!isBot) { + botIndicator = (); + } } else if (isFromAutoResponder) { userProfile = ( From b66144e3b0e16dadd0b196a267a00ab01ea309e6 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Tue, 25 Apr 2023 22:59:27 +0300 Subject: [PATCH 48/69] [MM-52237] - Update copy on compliance block screen (#23087) * [MM-52237] - Update copy on compliance block screen * fix translations --------- Co-authored-by: Mattermost Build --- .../src/components/purchase_modal/process_payment_setup.tsx | 3 --- .../channels/src/components/purchase_modal/purchase_modal.tsx | 3 --- webapp/channels/src/i18n/en.json | 3 +-- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx b/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx index 9f7731c9f8..4d70d1ede8 100644 --- a/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx +++ b/webapp/channels/src/components/purchase_modal/process_payment_setup.tsx @@ -368,9 +368,6 @@ class ProcessPaymentSetup extends React.PureComponent { title={t( 'admin.billing.subscription.complianceScreenFailed.title', )} - subtitle={t( - 'admin.billing.subscription.complianceScreenFailed.subtitle', - )} icon={ { title={t( 'admin.billing.subscription.complianceScreenFailed.title', )} - subtitle={t( - 'admin.billing.subscription.complianceScreenFailed.subtitle', - )} icon={ Date: Tue, 25 Apr 2023 23:02:01 +0300 Subject: [PATCH 49/69] [MM-52286] - Update Annual discount panel (#23092) Co-authored-by: Mattermost Build --- .../billing/billing_subscriptions/contact_sales_card.tsx | 8 ++++---- webapp/channels/src/i18n/en.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx index 3e5bec63e9..fd42f863a1 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx @@ -59,13 +59,13 @@ const ContactSalesCard = (props: Props) => { title = ( ); description = ( ); } else { @@ -103,13 +103,13 @@ const ContactSalesCard = (props: Props) => { title = ( ); description = ( ); break; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c7f1f879fa..c18988305b 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -381,8 +381,8 @@ "admin.billing.subscription.planDetails.productName.unknown": "Unknown product", "admin.billing.subscription.planDetails.subheader": "Plan details", "admin.billing.subscription.planDetails.userCount": "{userCount} users", - "admin.billing.subscription.privateCloudCard.cloudEnterprise.description": "At Mattermost, we work with you and your team to meet your needs throughout the product. If you are looking for an annual discount, please reach out to our sales team.", - "admin.billing.subscription.privateCloudCard.cloudEnterprise.title": "Looking for an annual discount? ", + "admin.billing.subscription.privateCloudCard.cloudEnterprise.description": "At Mattermost, we work with you and your organization to meet your needs throughout the product. If you’re considering a wider rollout, talk to us.", + "admin.billing.subscription.privateCloudCard.cloudEnterprise.title": "Looking to rollout Mattermost for your entire organization? ", "admin.billing.subscription.privateCloudCard.cloudFree.description": "Optimize your processes with Guest Accounts, Office365 suite integrations, GitLab SSO and advanced permissions.", "admin.billing.subscription.privateCloudCard.cloudFree.title": "Upgrade to Cloud Professional", "admin.billing.subscription.privateCloudCard.cloudProfessional.description": "Advanced security and compliance features with premium support. See {pricingLink} for more details.", From f079f3ba8dd89e92361accd7f0557350694665d0 Mon Sep 17 00:00:00 2001 From: na Date: Wed, 26 Apr 2023 15:24:53 +0700 Subject: [PATCH 50/69] [48399][52129] - Fix permalink and navigation issues between teams (#22934) * Fix permalink issues with timestamp and navigation between teams * fix types * update teamurl --------- Co-authored-by: Nevyana Angelova Co-authored-by: Mattermost Build --- webapp/channels/src/components/post/index.tsx | 10 ++++------ .../src/components/post/post_component.tsx | 19 ++++++++++++++++--- .../thread_footer/thread_footer.tsx | 2 +- .../virtualized_thread_viewer/index.ts | 1 - .../virtualized_thread_viewer/reply/reply.tsx | 3 --- .../thread_viewer_row.tsx | 4 ---- .../virtualized_thread_viewer.tsx | 2 -- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/webapp/channels/src/components/post/index.tsx b/webapp/channels/src/components/post/index.tsx index 39c664736f..8ac03cbb63 100644 --- a/webapp/channels/src/components/post/index.tsx +++ b/webapp/channels/src/components/post/index.tsx @@ -13,7 +13,7 @@ import { getBool, isCollapsedThreadsEnabled, } from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeam, getCurrentTeamId, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentTeam, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import {Emoji} from '@mattermost/types/emojis'; @@ -48,7 +48,6 @@ interface OwnProps { post?: Post | UserActivityPost; previousPostId?: string; postId?: string; - teamId?: string; shouldHighlight?: boolean; location: keyof typeof Locations; } @@ -120,7 +119,6 @@ function makeMapStateToProps() { const config = getConfig(state); const enableEmojiPicker = config.EnableEmojiPicker === 'true'; const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true'; - const teamId = ownProps.teamId || getCurrentTeamId(state); const channel = state.entities.channels.channels[post.channel_id]; const shortcutReactToLastPostEmittedFrom = getShortcutReactToLastPostEmittedFrom(state); @@ -148,6 +146,7 @@ function makeMapStateToProps() { } const currentTeam = getCurrentTeam(state); + const team = getTeam(state, channel.team_id); let teamName = currentTeam.name; let teamDisplayName = ''; @@ -159,7 +158,6 @@ function makeMapStateToProps() { !isDMorGM && // Not show for DM or GMs since they don't belong to a team memberships && Object.values(memberships).length > 1 // Not show if the user only belongs to one team ) { - const team = getTeam(state, channel.team_id); teamDisplayName = team?.display_name; teamName = team?.name || currentTeam.name; } @@ -186,7 +184,6 @@ function makeMapStateToProps() { enablePostUsernameOverride, isEmbedVisible: isEmbedVisible(state, post.id), isReadOnly: false, - teamId, currentUserId: getCurrentUserId(state), isFirstReply: previousPost ? isFirstReply(post, previousPost) : false, hasReplies: getReplyCount(state, post) > 0, @@ -200,7 +197,8 @@ function makeMapStateToProps() { compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT) === 'true', shouldShowActionsMenu: shouldShowActionsMenu(state, post), - + currentTeam, + team, shortcutReactToLastPostEmittedFrom, isBot, collapsedThreadsEnabled: isCollapsedThreadsEnabled(state), diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx index 8a549c272e..52ee868f3d 100644 --- a/webapp/channels/src/components/post/post_component.tsx +++ b/webapp/channels/src/components/post/post_component.tsx @@ -50,10 +50,12 @@ import {Emoji} from '@mattermost/types/emojis'; import PostUserProfile from './user_profile'; import PostOptions from './post_options'; +import {Team} from '@mattermost/types/teams'; export type Props = { post: Post; - teamId: string; + currentTeam: Team; + team?: Team; currentUserId: string; compactDisplay?: boolean; colorizeUsernames?: boolean; @@ -123,6 +125,7 @@ const PostComponent = (props: Props): JSX.Element => { const isRHS = props.location === Locations.RHS_ROOT || props.location === Locations.RHS_COMMENT || props.location === Locations.SEARCH; const postRef = useRef(null); const postHeaderRef = useRef(null); + const teamId = props.team?.id || props.currentTeam.id; const [hover, setHover] = useState(false); const [a11yActive, setA11y] = useState(false); @@ -355,7 +358,15 @@ const PostComponent = (props: Props): JSX.Element => { return; } props.actions.selectPostFromRightHandSideSearch(post); - }, [post, props.actions]); + }, [post, props.actions, props.actions.selectPostFromRightHandSideSearch]); + + const handleThreadClick = useCallback((e: React.MouseEvent) => { + if (props.currentTeam.id === props.team?.id) { + handleCommentClick(e); + } else { + handleJumpClick(e); + } + }, [handleCommentClick, handleJumpClick]); const postClass = classNames('post__body', {'post--edited': PostUtils.isEdited(post), 'search-item-snippet': isSearchResultItem}); @@ -435,7 +446,7 @@ const PostComponent = (props: Props): JSX.Element => { const threadFooter = props.location !== Locations.RHS_ROOT && props.isCollapsedThreadsEnabled && !post.root_id && (props.hasReplies || post.is_following) ? ( ) : null; const currentPostDay = getDateForUnixTicks(post.create_at); @@ -538,6 +549,7 @@ const PostComponent = (props: Props): JSX.Element => { {((!hideProfilePicture && props.location === Locations.CENTER) || hover || props.location !== Locations.CENTER) && { {!props.isPostBeingEdited && { e.stopPropagation(); diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts b/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts index 15b1abea38..e786684366 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/index.ts @@ -52,7 +52,6 @@ function makeMapStateToProps() { directTeammate, lastPost, replyListIds, - teamId: channel.team_id, }; }; } diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/reply/reply.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/reply/reply.tsx index d162e48b95..a7deaffe54 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/reply/reply.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/reply/reply.tsx @@ -16,7 +16,6 @@ type Props = { onCardClick: (post: Post) => void; post: Post; previousPostId: string; - teamId: string; timestampProps?: Partial; id?: Post['id']; } @@ -27,7 +26,6 @@ function Reply({ onCardClick, post, previousPostId, - teamId, timestampProps, }: Props) { return ( @@ -37,7 +35,6 @@ function Reply({ isLastPost={isLastPost} post={post} previousPostId={previousPostId} - teamId={teamId} timestampProps={timestampProps} location={Locations.RHS_COMMENT} /> diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx index 4917f2fa79..1f6479b357 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx @@ -25,7 +25,6 @@ type Props = { listId: string; onCardClick: (post: Post) => void; previousPostId: string; - teamId: string; timestampProps?: Partial; }; @@ -38,7 +37,6 @@ function ThreadViewerRow({ listId, onCardClick, previousPostId, - teamId, timestampProps, }: Props) { switch (true) { @@ -61,7 +59,6 @@ function ThreadViewerRow({ postId={listId} isLastPost={isLastPost} handleCardClick={onCardClick} - teamId={teamId} timestampProps={timestampProps} location={Locations.RHS_ROOT} /> @@ -87,7 +84,6 @@ function ThreadViewerRow({ isLastPost={isLastPost} onCardClick={onCardClick} previousPostId={previousPostId} - teamId={teamId} timestampProps={timestampProps} /> ); diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx index bbd46812e9..66c6d2ca6a 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx @@ -34,7 +34,6 @@ type Props = { onCardClick: (post: Post) => void; replyListIds: string[]; selected: Post | FakePost; - teamId: string; useRelativeTimestamp: boolean; isThreadView: boolean; } @@ -401,7 +400,6 @@ class ThreadViewerVirtualized extends PureComponent { listId={itemId} onCardClick={this.props.onCardClick} previousPostId={getPreviousPostId(data, index)} - teamId={this.props.teamId} timestampProps={this.props.useRelativeTimestamp ? THREADING_TIME : undefined} />
From b0e4e11cd4d6d1de818c8a2283c2b77cc0e3f31b Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Wed, 26 Apr 2023 12:32:01 +0300 Subject: [PATCH 51/69] [MM-52236] - Include newsletter opt-in for cloud signups (#23103) --- webapp/channels/src/components/signup/signup.test.tsx | 10 +++++++--- webapp/channels/src/components/signup/signup.tsx | 7 +------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/webapp/channels/src/components/signup/signup.test.tsx b/webapp/channels/src/components/signup/signup.test.tsx index 900e8de8b7..60e4a1cb23 100644 --- a/webapp/channels/src/components/signup/signup.test.tsx +++ b/webapp/channels/src/components/signup/signup.test.tsx @@ -316,15 +316,19 @@ describe('components/signup/Signup', () => { expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?Sign up at https://mattermost.com/security-updates/.'); }); - it('should not show any newsletter related opt-in or text for cloud', async () => { + it('should show newsletter related opt-in or text for cloud', async () => { jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true); mockLicense = {IsLicensed: 'true', Cloud: 'true'}; - renderWithIntlAndStore( + const {container: signupContainer} = renderWithIntlAndStore( , {}); - expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow(); + screen.getByTestId('signup-body-card-form-check-newsletter'); + const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter'); + expect(checkInput).toHaveAttribute('type', 'checkbox'); + + expect(signupContainer).toHaveTextContent('I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time'); }); }); diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index 0407a74895..0075949586 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -101,7 +101,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { TermsOfServiceLink, PrivacyPolicyLink, } = config; - const {IsLicensed, Cloud} = useSelector(getLicense); + const {IsLicensed} = useSelector(getLicense); const loggedIn = Boolean(useSelector(getCurrentUserId)); const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined)); const graphQLEnabled = useSelector(isGraphQLEnabled); @@ -111,7 +111,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const passwordInput = useRef(null); const isLicensed = IsLicensed === 'true'; - const isCloud = Cloud === 'true'; const enableOpenServer = EnableOpenServer === 'true'; const noAccounts = NoAccounts === 'true'; const enableSignUpWithEmail = EnableSignUpWithEmail === 'true'; @@ -579,10 +578,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const handleReturnButtonOnClick = () => history.replace('/'); const getNewsletterCheck = () => { - if (isCloud) { - return null; - } - if (canReachCWS) { return ( Date: Wed, 26 Apr 2023 12:45:32 +0300 Subject: [PATCH 52/69] fix: Disable branch name manipulation to remove attack vector --- .github/workflows/artifacts.yml | 131 +++++++++++++++----------------- 1 file changed, 61 insertions(+), 70 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 44b59b7548..35829f9f04 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -4,15 +4,12 @@ on: workflows: ["Mattermost Build"] types: - completed + jobs: upload-s3: name: cd/Upload artifacts to S3 runs-on: ubuntu-22.04 - env: - REPO_NAME: ${{ github.event.repository.name }} - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - name: cd/Configure AWS uses: aws-actions/configure-aws-credentials@07c2f971bac433df982ccc261983ae443861db49 # v1-node16 @@ -28,89 +25,83 @@ jobs: workflow_conclusion: success name: server-dist-artifact path: server/dist + # DISABLING THIS UNTIL WE FIGURE OUT IF WE NEED IT # Get Branch name from calling workflow # Search for the string "pull" and replace it with "PR" in branch-name - - name: cd/Get branch name - run: echo "BRANCH_NAME=$(echo ${{ github.event.workflow_run.head_branch }} | sed 's/^pull\//PR-/g')" >> $GITHUB_ENV + # - name: cd/Get branch name + # run: echo "BRANCH_NAME=$(echo ${{ github.event.workflow_run.head_branch }} | sed 's/^pull\//PR-/g')" >> $GITHUB_ENV - name: cd/Upload artifacts to S3 run: | - aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/$BRANCH_NAME/ --acl public-read --cache-control "no-cache" --recursive --no-progress - aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/ --acl public-read --cache-control "no-cache" --recursive --no-progress + aws s3 cp server/dist/ s3://pr-builds.mattermost.com/${{ github.event.repository.name }}/${{ github.event.workflow_run.head_branch }}/ --acl public-read --cache-control "no-cache" --recursive --no-progress + aws s3 cp server/dist/ s3://pr-builds.mattermost.com/${{ github.event.repository.name }}/commit/${{ github.sha }}/ --acl public-read --cache-control "no-cache" --recursive --no-progress + build-docker: name: cd/Build and push docker image needs: upload-s3 - env: - REPO_NAME: ${{ github.event.repository.name }} runs-on: ubuntu-22.04 - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - name: cd/Login to Docker Hub - uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 - with: - username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} - password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} - - name: cd/Download artifacts - uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 - with: - workflow: ${{ github.event.workflow_run.workflow_id }} - run_id: ${{ github.event.workflow_run.id }} - workflow_conclusion: success - name: server-build-artifact - path: server/build/ - - name: cd/Setup Docker Buildx - uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 - - name: cd/Docker build and push - env: - DOCKER_CLI_EXPERIMENTAL: enabled - run: | - export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) - cd server/build - export DOCKER_CLI_EXPERIMENTAL=enabled - export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz - docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermostdevelopment/mm-te-test:${TAG} . + - name: cd/Login to Docker Hub + uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} + password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} + - name: cd/Download artifacts + uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 + with: + workflow: ${{ github.event.workflow_run.workflow_id }} + run_id: ${{ github.event.workflow_run.id }} + workflow_conclusion: success + name: server-build-artifact + path: server/build/ + - name: cd/Setup Docker Buildx + uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 + - name: cd/Docker build and push + env: + DOCKER_CLI_EXPERIMENTAL: enabled + run: | + export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) + cd server/build + export DOCKER_CLI_EXPERIMENTAL=enabled + export MM_PACKAGE=https://pr-builds.mattermost.com/${{ github.event.repository.name }}/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz + docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermostdevelopment/mm-te-test:${TAG} . + # Temporary uploading also to mattermost/mm-te-test:${TAG} except mattermostdevelopment/mm-te-test:${TAG} # Context: https://community.mattermost.com/private-core/pl/3jzzxzfiji8hx833ewyuthzkjh build-docker-temp: name: cd/Build and push docker image needs: upload-s3 - env: - REPO_NAME: ${{ github.event.repository.name }} runs-on: ubuntu-22.04 - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - name: cd/Login to Docker Hub - uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: cd/Download artifacts - uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 - with: - workflow: ${{ github.event.workflow_run.workflow_id }} - run_id: ${{ github.event.workflow_run.id }} - workflow_conclusion: success - name: server-build-artifact - path: server/build/ - - name: cd/Setup Docker Buildx - uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 - - name: cd/Docker build and push - env: - DOCKER_CLI_EXPERIMENTAL: enabled - run: | - export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) - cd server/build - export DOCKER_CLI_EXPERIMENTAL=enabled - export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz - docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mm-te-test:${TAG} . + - name: cd/Login to Docker Hub + uses: docker/login-action@3da7dc6e2b31f99ef2cb9fb4c50fb0971e0d0139 # v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: cd/Download artifacts + uses: dawidd6/action-download-artifact@0c49384d39ceb023b8040f480a25596fd6cf441b # v2.26.0 + with: + workflow: ${{ github.event.workflow_run.workflow_id }} + run_id: ${{ github.event.workflow_run.id }} + workflow_conclusion: success + name: server-build-artifact + path: server/build/ + - name: cd/Setup Docker Buildx + uses: docker/setup-buildx-action@11e8a2e2910826a92412015c515187a2d6750279 # v2.4 + - name: cd/Docker build and push + env: + DOCKER_CLI_EXPERIMENTAL: enabled + run: | + export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) + cd server/build + export DOCKER_CLI_EXPERIMENTAL=enabled + export MM_PACKAGE=https://pr-builds.mattermost.com/${{ github.event.repository.name }}/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz + docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mm-te-test:${TAG} . + sentry: name: Send build info to sentry - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-22.04 env: SENTRY_AUTH_TOKEN: ${{ secrets.MM_SERVER_SENTRY_AUTH_TOKEN }} From ee23f6dc6b864bf9ea4c51de799048b6c9927f5d Mon Sep 17 00:00:00 2001 From: Antonis Stamatiou Date: Wed, 26 Apr 2023 13:15:04 +0300 Subject: [PATCH 53/69] fix: Fixed wrong commit sha reference from incoming webhook --- .github/workflows/artifacts.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 35829f9f04..1886775a8e 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -25,15 +25,14 @@ jobs: workflow_conclusion: success name: server-dist-artifact path: server/dist - # DISABLING THIS UNTIL WE FIGURE OUT IF WE NEED IT - # Get Branch name from calling workflow - # Search for the string "pull" and replace it with "PR" in branch-name - # - name: cd/Get branch name - # run: echo "BRANCH_NAME=$(echo ${{ github.event.workflow_run.head_branch }} | sed 's/^pull\//PR-/g')" >> $GITHUB_ENV - name: cd/Upload artifacts to S3 + env: + BRANCH_NAME: ${{ github.event.workflow_run.head_branch }} + REPO_NAME: ${{ github.event.repository.name }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} run: | - aws s3 cp server/dist/ s3://pr-builds.mattermost.com/${{ github.event.repository.name }}/${{ github.event.workflow_run.head_branch }}/ --acl public-read --cache-control "no-cache" --recursive --no-progress - aws s3 cp server/dist/ s3://pr-builds.mattermost.com/${{ github.event.repository.name }}/commit/${{ github.sha }}/ --acl public-read --cache-control "no-cache" --recursive --no-progress + aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/$BRANCH_NAME/ --acl public-read --cache-control "no-cache" --recursive --no-progress + aws s3 cp server/dist/ s3://pr-builds.mattermost.com/$REPO_NAME/commit/$COMMIT_SHA/ --acl public-read --cache-control "no-cache" --recursive --no-progress build-docker: name: cd/Build and push docker image @@ -59,11 +58,13 @@ jobs: - name: cd/Docker build and push env: DOCKER_CLI_EXPERIMENTAL: enabled + REPO_NAME: ${{ github.event.repository.name }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} run: | - export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) + export TAG=$(echo "${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}" | cut -c1-7) cd server/build export DOCKER_CLI_EXPERIMENTAL=enabled - export MM_PACKAGE=https://pr-builds.mattermost.com/${{ github.event.repository.name }}/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz + export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/$COMMIT_SHA/mattermost-team-linux-amd64.tar.gz docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermostdevelopment/mm-te-test:${TAG} . # Temporary uploading also to mattermost/mm-te-test:${TAG} except mattermostdevelopment/mm-te-test:${TAG} @@ -71,6 +72,9 @@ jobs: build-docker-temp: name: cd/Build and push docker image needs: upload-s3 + env: + REPO_NAME: ${{ github.event.repository.name }} + runs-on: ubuntu-22.04 if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: @@ -92,11 +96,13 @@ jobs: - name: cd/Docker build and push env: DOCKER_CLI_EXPERIMENTAL: enabled + REPO_NAME: ${{ github.event.repository.name }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} run: | - export TAG=$(echo "${{ github.event.pull_request.head.sha || github.sha }}" | cut -c1-7) + export TAG=$(echo "${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}" | cut -c1-7) cd server/build export DOCKER_CLI_EXPERIMENTAL=enabled - export MM_PACKAGE=https://pr-builds.mattermost.com/${{ github.event.repository.name }}/commit/${{ github.sha }}/mattermost-team-linux-amd64.tar.gz + export MM_PACKAGE=https://pr-builds.mattermost.com/$REPO_NAME/commit/$COMMIT_SHA/mattermost-team-linux-amd64.tar.gz docker buildx build --push --build-arg MM_PACKAGE=$MM_PACKAGE -t mattermost/mm-te-test:${TAG} . sentry: From 506c4178b5aae96ea302d09fbdc7085e14fff18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Garc=C3=ADa=20Montoro?= Date: Wed, 26 Apr 2023 12:37:39 +0200 Subject: [PATCH 54/69] Use ubuntu-22.04 in the check-style step in CI (#23118) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24ec4e83a1..44048c8641 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: run: if [[ -n $(git status --porcelain) ]]; then echo "Please update the serialized files using 'make gen-serialized'"; exit 1; fi check-mattermost-vet: name: Check style - runs-on: ubuntu-latest-8-cores + runs-on: ubuntu-22.04 defaults: run: working-directory: server From b143a882be1fe07c2df1fa77fed9cdeb3d3fbc05 Mon Sep 17 00:00:00 2001 From: Akis Maziotis Date: Wed, 26 Apr 2023 15:40:45 +0300 Subject: [PATCH 55/69] [fix] CI Sentry. running on push events (#23142) * [fix] CI Sentry. running on push events --- .github/workflows/artifacts.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 1886775a8e..b313f7367d 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -107,7 +107,8 @@ jobs: sentry: name: Send build info to sentry - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + if: > + github.event.workflow_run.event == 'push' runs-on: ubuntu-22.04 env: SENTRY_AUTH_TOKEN: ${{ secrets.MM_SERVER_SENTRY_AUTH_TOKEN }} From 466304e6537c7f76f8c8a9a27f2bf2e9a919aae9 Mon Sep 17 00:00:00 2001 From: Kyriakos Z <3829551+koox00@users.noreply.github.com> Date: Wed, 26 Apr 2023 12:49:50 +0000 Subject: [PATCH 56/69] MM-52451: always show mark all threads as read button (#23100) Enables the "Mark All Threads as Read" button even when you don't have unreads. This is done as a temporary action to mitigate the phantom unread mentions pending issue. --- .../thread_list/__snapshots__/thread_list.test.tsx.snap | 1 - .../threading/global_threads/thread_list/thread_list.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/webapp/channels/src/components/threading/global_threads/thread_list/__snapshots__/thread_list.test.tsx.snap b/webapp/channels/src/components/threading/global_threads/thread_list/__snapshots__/thread_list.test.tsx.snap index 509c934c12..30ab57bc69 100644 --- a/webapp/channels/src/components/threading/global_threads/thread_list/__snapshots__/thread_list.test.tsx.snap +++ b/webapp/channels/src/components/threading/global_threads/thread_list/__snapshots__/thread_list.test.tsx.snap @@ -52,7 +52,6 @@ exports[`components/threading/global_threads/thread_list should match snapshot 1 >
+
+
+
+
+`; + +exports[`InviteMembers component should match snapshot when it is cloud 1`] = ` +
+
+
+
+
+
+ Previous step +
+

+ + Who works with you? + +

+

+ + Collaboration is tough by yourself. Invite a few team members. Separate each email address with a space or comma. + +

+
+
+ +

+   + +

+

+   + 0 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu. +

+
+
+
+
+ Enter email addresses +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
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 index 0610646391..1abd12f11b 100644 --- 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 @@ -1,6 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = ` +
+ +
+`; + +exports[`components/preparing-workspace/invite_members_link should match snapshot when displayed including the input field 1`] = `