From 5a4dba8809cbff0aeb06b7e7888bc283f163702e Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Mon, 4 Dec 2023 16:29:17 +0000 Subject: [PATCH] [MM-55999] Bring the revoke sessions button to the admin header and improve the styling of the header (#25590) --- .../revoke_all_sessions_spec.js | 38 ++++--- .../admin_console/admin_definition.tsx | 2 - .../__snapshots__/system_users.test.tsx.snap | 51 +-------- .../admin_console/system_users/index.ts | 2 - .../system_users_list.test.tsx.snap | 3 - .../system_users/list/system_users_list.tsx | 2 - .../revoke_sessions_button/index.tsx | 77 +++++++++++++ .../system_users/system_users.test.tsx | 1 - .../system_users/system_users.tsx | 106 ++---------------- .../system_users_dropdown.test.tsx | 1 - .../system_users_dropdown.tsx | 10 +- .../searchable_user_list.tsx | 2 +- webapp/channels/src/i18n/en.json | 2 +- .../mattermost-redux/src/actions/users.ts | 8 +- .../src/sass/components/_buttons.scss | 16 +++ .../src/sass/routes/_admin-console.scss | 4 +- 16 files changed, 138 insertions(+), 187 deletions(-) create mode 100644 webapp/channels/src/components/admin_console/system_users/revoke_sessions_button/index.tsx diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/revoke_all_sessions_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/revoke_all_sessions_spec.js index 5bddee4465..798f3ba8b6 100644 --- a/e2e-tests/cypress/tests/integration/channels/system_console/revoke_all_sessions_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/system_console/revoke_all_sessions_spec.js @@ -16,39 +16,47 @@ import {getAdminAccount} from '../../../support/env'; describe('System Console > User Management > Users', () => { const admin = getAdminAccount(); - it('MM-T940 Users - Revoke all sessions', () => { + it('MM-T940 Users - Revoke all sessions from a button in admin console', () => { // # Login as System Admin cy.apiAdminLogin(); cy.visit('/admin_console/user_management/users'); - // * Verify the presence of Revoke All Sessions button - cy.get('#revoke-all-users').should('be.visible').and('not.have.class', 'btn-danger').click(); + // * Verify the presence of Revoke All Sessions button and click on it + cy.findByText('Revoke All Sessions').should('be.visible').click(); // * Verify the confirmation message when users clicks on the Revoke All Sessions button - cy.get('#confirmModalLabel').should('be.visible').and('have.text', 'Revoke all sessions in the system'); - cy.get('.modal-body').should('be.visible').and('have.text', 'This action revokes all sessions in the system. All users will be logged out from all devices. Are you sure you want to revoke all sessions?'); - cy.get('#confirmModalButton').should('be.visible').and('have.class', 'btn-danger'); + cy.get('#confirmModal').should('be.visible').within(() => { + // * Verify the presence of confirmation messages and buttons + cy.findByText('Revoke all sessions in the system').should('be.visible'); + cy.findByText('This action revokes all sessions in the system. All users will be logged out from all devices, including your session. Are you sure you want to revoke all sessions?').should('be.visible'); + cy.findByText('Cancel').should('be.visible'); + cy.findByText('Revoke All Sessions').should('be.visible'); - // # Click on Cancel button in the confirmation message - cy.get('#cancelModalButton').click(); + // # Click on Cancel button in the confirmation message + cy.findByText('Cancel').click(); + }); // * Verify if Confirmation message is closed cy.get('#confirmModal').should('not.exist'); - // * Verify if the Admin's session is still active and user is still in the same page + // * Since we have cancelled the confirmation message, verify if the Admin's session is still active and user is still in the same page cy.url().should('contain', '/admin_console/user_management/users'); - // * Verify if the Admin's Session is still active and click on it and then confirm - cy.get('#revoke-all-users').should('be.visible').click(); - cy.get('#confirmModalButton').click(); + // # Open revoke all sessions modal again + cy.findByText('Revoke All Sessions').should('be.visible').click(); + + cy.get('#confirmModal').should('be.visible').within(() => { + // # Click on Revoke All Sessions button in the confirmation message + cy.findByText('Revoke All Sessions').click(); + }); // * Verify if Admin User's session is expired and is redirected to login page cy.url({timeout: TIMEOUTS.HALF_MIN}).should('include', '/login'); - cy.get('.login-body-card', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible'); + cy.findByText('Log in to your account').should('be.visible'); }); - it('Verify for Regular Member', () => { + it('MM-T940-1 Users - Revoke all sessions with an API call', () => { // # Login as System Admin cy.apiAdminLogin(); @@ -65,7 +73,7 @@ describe('System Console > User Management > Users', () => { // * Verify if the regular member is logged out and redirected to login page cy.url({timeout: TIMEOUTS.HALF_MIN}).should('include', '/login'); - cy.get('.login-body-card', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible'); + cy.findByText('Log in to your account').should('be.visible'); }); }); }); diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index c0849c5077..b8f3a5a986 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -540,7 +540,6 @@ const AdminDefinition: AdminDefinitionType = { searchableStrings: [ ['admin.system_users.title', {siteName: ''}], ], - isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.USER_MANAGEMENT.USERS)), isHidden: it.not(it.userHasReadPermissionOnResource(RESOURCE_KEYS.USER_MANAGEMENT.USERS)), schema: { id: 'SystemUsers', @@ -549,7 +548,6 @@ const AdminDefinition: AdminDefinitionType = { }, system_user_detail: { url: 'user_management/user/:user_id', - isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.USER_MANAGEMENT.USERS)), isHidden: it.not(it.userHasReadPermissionOnResource(RESOURCE_KEYS.USER_MANAGEMENT.USERS)), schema: { id: 'SystemUserDetail', diff --git a/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap b/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap index 956568f4ea..d559c8bd42 100644 --- a/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap @@ -14,6 +14,7 @@ exports[`components/admin_console/system_users should match default snapshot 1`] } } /> +
- - - } - message={ -
- -
- } - modalClass="" - onCancel={[Function]} - onConfirm={[Function]} - show={false} - title={ - - } - /> -
- -
-
diff --git a/webapp/channels/src/components/admin_console/system_users/index.ts b/webapp/channels/src/components/admin_console/system_users/index.ts index f6012933bc..3e2cc18b12 100644 --- a/webapp/channels/src/components/admin_console/system_users/index.ts +++ b/webapp/channels/src/components/admin_console/system_users/index.ts @@ -16,7 +16,6 @@ import { getUserAccessToken, getProfiles, searchProfiles, - revokeSessionsForAllUsers, getFilteredUsersStats, } from 'mattermost-redux/actions/users'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; @@ -106,7 +105,6 @@ function mapDispatchToProps(dispatch: Dispatch) { loadProfilesWithoutTeam, getProfiles, searchProfiles, - revokeSessionsForAllUsers, logError, getFilteredUsersStats, }, dispatch), diff --git a/webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap b/webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap index 25133db64c..d73a8006f0 100644 --- a/webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap @@ -12,7 +12,6 @@ exports[`components/admin_console/system_users/list should match default snapsho "doPasswordReset": [Function], "enableUserAccessTokens": false, "experimentalEnableAuthenticationTransfer": false, - "isDisabled": false, "mfaEnabled": false, } } @@ -96,7 +95,6 @@ exports[`components/admin_console/system_users/list should match default snapsho "doPasswordReset": [Function], "enableUserAccessTokens": false, "experimentalEnableAuthenticationTransfer": false, - "isDisabled": false, "mfaEnabled": false, } } @@ -299,7 +297,6 @@ exports[`components/admin_console/system_users/list should match default snapsho "doPasswordReset": [Function], "enableUserAccessTokens": false, "experimentalEnableAuthenticationTransfer": false, - "isDisabled": false, "mfaEnabled": true, } } diff --git a/webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx b/webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx index 7ffaa12739..38d20105fb 100644 --- a/webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx +++ b/webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx @@ -37,7 +37,6 @@ type Props = { filter: string; term: string; onTermChange: (term: string) => void; - isDisabled?: boolean; /** * Whether MFA is licensed and enabled. @@ -345,7 +344,6 @@ export default class SystemUsersList extends React.PureComponent { doManageTeams: this.doManageTeams, doManageRoles: this.doManageRoles, doManageTokens: this.doManageTokens, - isDisabled: this.props.isDisabled, }} nextPage={this.nextPage} previousPage={this.previousPage} diff --git a/webapp/channels/src/components/admin_console/system_users/revoke_sessions_button/index.tsx b/webapp/channels/src/components/admin_console/system_users/revoke_sessions_button/index.tsx new file mode 100644 index 0000000000..3a93d1001d --- /dev/null +++ b/webapp/channels/src/components/admin_console/system_users/revoke_sessions_button/index.tsx @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useDispatch} from 'react-redux'; + +import type {ServerError} from '@mattermost/types/errors'; + +import {revokeSessionsForAllUsers} from 'mattermost-redux/actions/users'; +import {Permissions} from 'mattermost-redux/constants'; +import type {ActionResult} from 'mattermost-redux/types/actions'; + +import {emitUserLoggedOutEvent} from 'actions/global_actions'; + +import ConfirmModal from 'components/confirm_modal'; +import SystemPermissionGate from 'components/permissions_gates/system_permission_gate'; + +function RevokeSessionsButton() { + const dispatch = useDispatch(); + + const [showModal, setShowModal] = useState(false); + + function handleModalToggle() { + setShowModal((showModal) => !showModal); + } + + async function handleModalConfirm() { + const {data} = await dispatch(revokeSessionsForAllUsers()) as ActionResult; + + if (data) { + emitUserLoggedOutEvent(); + } else { + setShowModal(false); + } + } + + return ( + + + + } + message={ + + } + confirmButtonClass='btn btn-danger' + confirmButtonText={ + + } + onConfirm={handleModalConfirm} + onCancel={handleModalToggle} + /> + + ); +} + +export default RevokeSessionsButton; diff --git a/webapp/channels/src/components/admin_console/system_users/system_users.test.tsx b/webapp/channels/src/components/admin_console/system_users/system_users.test.tsx index cfedd4b544..3cef77f478 100644 --- a/webapp/channels/src/components/admin_console/system_users/system_users.test.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users.test.tsx @@ -36,7 +36,6 @@ describe('components/admin_console/system_users', () => { loadProfilesWithoutTeam: jest.fn().mockResolvedValue({data: true}), getProfiles: jest.fn().mockResolvedValue({data: []}), searchProfiles: jest.fn().mockResolvedValue({data: []}), - revokeSessionsForAllUsers: jest.fn().mockResolvedValue({data: true}), logError: jest.fn(), getFilteredUsersStats: jest.fn(), }, diff --git a/webapp/channels/src/components/admin_console/system_users/system_users.tsx b/webapp/channels/src/components/admin_console/system_users/system_users.tsx index 165bd01b80..25cb956123 100644 --- a/webapp/channels/src/components/admin_console/system_users/system_users.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users.tsx @@ -1,8 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; -import type {ChangeEvent} from 'react'; +import React, {type ChangeEvent} from 'react'; import {FormattedMessage, type IntlShape, injectIntl} from 'react-intl'; import type {ServerError} from '@mattermost/types/errors'; @@ -10,21 +9,15 @@ import type {Team} from '@mattermost/types/teams'; import type {GetFilteredUsersStatsOpts, UserProfile, UsersStats} from '@mattermost/types/users'; import {debounce} from 'mattermost-redux/actions/helpers'; -import {Permissions} from 'mattermost-redux/constants'; import type {ActionFunc} from 'mattermost-redux/types/actions'; -import {emitUserLoggedOutEvent} from 'actions/global_actions'; - -import ConfirmModal from 'components/confirm_modal'; -import FormattedMarkdownMessage from 'components/formatted_markdown_message'; -import SystemPermissionGate from 'components/permissions_gates/system_permission_gate'; import AdminHeader from 'components/widgets/admin_console/admin_header'; import {Constants, UserSearchOptions, SearchUserTeamFilter, UserFilters} from 'utils/constants'; import {getUserOptionsFromFilter, searchUserOptionsFromFilter} from 'utils/filter_users'; -import * as Utils from 'utils/utils'; import SystemUsersList from './list'; +import RevokeSessionsButton from './revoke_sessions_button'; const USER_ID_LENGTH = 26; const USERS_PER_PAGE = 50; @@ -62,7 +55,6 @@ type Props = { teamId: string; filter: string; users: Record; - isDisabled?: boolean; actions: { @@ -91,11 +83,6 @@ type Props = { setSystemUsersSearch: (searchTerm: string, teamId: string, filter: string) => void; searchProfiles: (term: string, options?: any) => Promise | ActionFunc; - /** - * Function to revoke all sessions in the system - */ - revokeSessionsForAllUsers: () => any; - /** * Function to log errors */ @@ -110,7 +97,6 @@ type Props = { type State = { loading: boolean; searching: boolean; - showRevokeAllSessionsModal: boolean; term?: string; }; @@ -121,7 +107,6 @@ export class SystemUsers extends React.PureComponent { this.state = { loading: true, searching: false, - showRevokeAllSessionsModal: false, }; } @@ -182,20 +167,6 @@ export class SystemUsers extends React.PureComponent { handleTermChange = (term: string) => { this.props.actions.setSystemUsersSearch(term, this.props.teamId, this.props.filter); }; - handleRevokeAllSessions = async () => { - const {data} = await this.props.actions.revokeSessionsForAllUsers(); - if (data) { - emitUserLoggedOutEvent(); - } else { - this.props.actions.logError({type: 'critical', message: 'Can\'t revoke all sessions'}); - } - }; - handleRevokeAllSessionsCancel = () => { - this.setState({showRevokeAllSessionsModal: false}); - }; - handleShowRevokeAllSessionsModal = () => { - this.setState({showRevokeAllSessionsModal: true}); - }; nextPage = async (page: number) => { const {teamId, filter} = this.props; @@ -267,44 +238,6 @@ export class SystemUsers extends React.PureComponent { this.getUserById(id); }; - renderRevokeAllUsersModal = () => { - const title = ( - - ); - - const message = ( -
- -
- ); - - const confirmButtonClass = 'btn btn-danger'; - const revokeAllButton = ( - - ); - - return ( - - ); - }; - renderFilterRow = (doSearch: ((event: React.FormEvent) => void) | undefined) => { const teams = this.props.teams.map((team) => ( - + + {teams} @@ -355,11 +288,11 @@ export class SystemUsers extends React.PureComponent { value={this.props.filter} onChange={this.handleFilterChange} > - - - - - + + + + + @@ -367,8 +300,6 @@ export class SystemUsers extends React.PureComponent { }; render() { - const revokeAllUsersModal = this.renderRevokeAllUsersModal(); - return (
@@ -379,6 +310,7 @@ export class SystemUsers extends React.PureComponent { siteName: this.props.siteName, }} /> +
@@ -398,26 +330,8 @@ export class SystemUsers extends React.PureComponent { mfaEnabled={this.props.mfaEnabled} enableUserAccessTokens={this.props.enableUserAccessTokens} experimentalEnableAuthenticationTransfer={this.props.experimentalEnableAuthenticationTransfer} - isDisabled={this.props.isDisabled} />
- - {revokeAllUsersModal} -
- -
-
diff --git a/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.test.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.test.tsx index 64cdbb6947..0557109777 100644 --- a/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.test.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.test.tsx @@ -39,7 +39,6 @@ describe('components/admin_console/system_users/system_users_dropdown/system_use currentUser: otherUser, index: 0, totalUsers: 10, - isDisabled: false, actions: { updateUserActive: jest.fn().mockResolvedValue({data: true}), revokeAllSessionsForUser: jest.fn().mockResolvedValue({data: true}), diff --git a/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx index b47feae266..8d28a35411 100644 --- a/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx @@ -42,7 +42,6 @@ export type Props = { config: DeepPartial; bots: Record; isLicensed: boolean; - isDisabled: boolean; actions: { updateUserActive: (id: string, active: boolean) => Promise<{error: ServerError}>; revokeAllSessionsForUser: (id: string) => Promise<{error: ServerError; data: any}>; @@ -575,13 +574,8 @@ export default class SystemUsersDropdown extends React.PureComponent void; doManageRoles: (user: UserProfile) => void; doManageTokens: (user: UserProfile) => void; - isDisabled: boolean | undefined; + isDisabled?: boolean; }; actionUserProps?: { [userId: string]: { diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c024da1d4c..b1cc1ea934 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -2423,7 +2423,7 @@ "admin.system_users.inactive": "Inactive", "admin.system_users.noTeams": "No Teams", "admin.system_users.revoke_all_sessions_button": "Revoke All Sessions", - "admin.system_users.revoke_all_sessions_modal_message": "This action revokes all sessions in the system. All users will be logged out from all devices. Are you sure you want to revoke all sessions?", + "admin.system_users.revoke_all_sessions_modal_message": "This action revokes all sessions in the system. All users will be logged out from all devices, including your session. Are you sure you want to revoke all sessions?", "admin.system_users.revoke_all_sessions_modal_title": "Revoke all sessions in the system", "admin.system_users.revokeAllSessions": "Revoke All Sessions", "admin.system_users.system_admin": "System Admin", diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts index e2738576b6..f869609d93 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts @@ -711,19 +711,21 @@ export function revokeAllSessionsForUser(userId: string): ActionFunc { }; } -export function revokeSessionsForAllUsers(): ActionFunc { - return async (dispatch: DispatchFunc, getState: GetStateFunc) => { +export function revokeSessionsForAllUsers(): ActionFunc { + return async (dispatch, getState) => { try { await Client4.revokeSessionsForAllUsers(); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); - return {error}; + return {error: error as ServerError}; } + dispatch({ type: UserTypes.REVOKE_SESSIONS_FOR_ALL_USERS_SUCCESS, data: null, }); + return {data: true}; }; } diff --git a/webapp/channels/src/sass/components/_buttons.scss b/webapp/channels/src/sass/components/_buttons.scss index 8b11f38cbd..d22fdef69c 100644 --- a/webapp/channels/src/sass/components/_buttons.scss +++ b/webapp/channels/src/sass/components/_buttons.scss @@ -270,6 +270,22 @@ button { color: rgba(var(--center-channel-color-rgb), 0.32); opacity: 1; } + + &.btn-danger { + background-color: rgba(var(--error-text-color-rgb), 0.08); + color: var(--error-text); + + &:hover { + background-color: rgba(var(--error-text-color-rgb), 0.12); + color: var(--error-text); + } + + &:active, + &:focus { + background-color: rgba(var(--error-text-color-rgb), 0.16); + color: var(--error-text); + } + } } &.btn-quaternary { diff --git a/webapp/channels/src/sass/routes/_admin-console.scss b/webapp/channels/src/sass/routes/_admin-console.scss index cab11fbb23..c7f77435e6 100644 --- a/webapp/channels/src/sass/routes/_admin-console.scss +++ b/webapp/channels/src/sass/routes/_admin-console.scss @@ -406,16 +406,16 @@ z-index: 100; display: flex; height: 65px; - -webkit-flex: 0 0 65px; flex: 0 0 65px; flex-direction: row; align-items: center; justify-content: space-between; - padding: 10px 20px; + padding: 12px 20px; border-bottom: 1px solid alpha-color($black, 0.1); background: white; font-size: 22px; font-weight: normal; + line-height: 32px; &.with-back { padding: 0;