[MM-54296] Admin Reporting for Users, new User Management screen (#26055)
* trial commit * [MM-56172] Integrate fetching and sorting of user reporting into the table component (#25749) * inint * lint * Update system_users.test.tsx.snap * cypress test fix * added pkg * revoke sessio into new compo * Update system_users.test.tsx.snap * trans * more disable * rename * Update system_users_dropdown.test.tsx * snap * Delete system_users_list.test.tsx.snap * Create system_users_list.test.tsx.snap * Delete system_users_list.test.tsx.snap * name2 * start removing the filter from the list * more clean * snaps * Update package-lock.json * des * Update index.scss * Update system_users_list.tsx * Update system_users_list.tsx * Update system_users.tsx * Update system_users_list.tsx * Update index.scss * menu * action menu * split * Update system_users_list.tsx * Update system_users_list_new.tsx * fix * Update en.json * temp remove * Update system_users.test.tsx.snap * Delete system_users_list.test.tsx.snap * Update en.json * ch * move * a * Update en.json * fix * Update system_users.test.tsx.snap * Update menu.tsx * Update admin_header.tsx * changes in the file system * fixes * integration begin * lint * Update rhs.ts * upper pagination * Update list_table.scss * pinned * more a11y * Update list_table.scss * i18n mor * design changes * descriptions * lint * loading states * fix * actions update * index fix * single action --------- Co-authored-by: Mattermost Build <build@mattermost.com> * [MM-56467] Clear up unused code of old systems users (#25855) * ccc * Update en.json * [MM-56478] Improve the user loading in user details page after changes in users list page in admin console (#25861) * init * tem * more fix * handlers clear up, team's fixes --------- Co-authored-by: Mattermost Build <build@mattermost.com> * Merge'd * [MM-56279] Add paging to user list (#25863) * Update system_users.tsx * Update system_users.tsx * [MM-56270] Hook up user actions menu, refactor modals to work with new menuing style (#25897) * Hookup menu items and modals * Add error handling, test fixes * Ensure modals animate properly * Add a bunch of TODOs around keeping the user data up to date * Fix i18n and snaps --------- Co-authored-by: Mattermost Build <build@mattermost.com> * [MM-56263] Enable selective column hiding for users table (#25898) * [MM-56539] Revisiting the design of the total users count on the left and in pagination in users table (#25947) * default sort order * default sort order (#25956) Co-authored-by: Mattermost Build <build@mattermost.com> * [MM-56267] Add search term filtering (#25952) * Include search term with request * Update user count with filtering * Oops * Add 500ms debounce to avoid spamming server with requests * PR feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com> * [MM-56398] Add date range filter to user reporting table (#25974) * WIP * Some small adjustments * Two fixes * PR feedback * Remove menuitem --------- Co-authored-by: Mattermost Build <build@mattermost.com> * Merge'd * [MM-56615] Add messaging for MySQL instances around the post stats (#26036) * [MM-56261] Add export modals and hook up button to user management screen (#26015) * [MM-56576] Update users when actions occur and show errors when actions fail (#25951) * [MM-56576] Update users when actions occur and show errors when actions fail * Forgot the else * PR feedback * [MM-56617] Move action modals to ConfirmModalRedux * Add filters popover to the users admin table (#26042) * int * 0 zeros * dd * re * file move * Update system_users_filter_popover.scss * a * Update system_users_filter_popover.scss * role and status * fix * Update index.test.ts * overlay * Update dropdown_input.tsx * i * Update en.json * done * Update en.json * dd --------- Co-authored-by: Mattermost Build <build@mattermost.com> * Make column toggler column menu items consistent with table column names * Couple typo fixes * Fix i18n * some e2e related changes * Fixed issue where you couldn't queue multiple batch export jobs * Fix id tag * table header mis allignment issue solved * [MM-56727] Fix a TODO, fix Demote to guest should only appear when enabled * rev comments * Stop sending down all profile data * chore(e2e): Fix tests cases for new user Management ui (#26089) * chore(e2e): Fix lint issues in cypress tests * chore(e2e): Fix e2e tests - user_management_spec - authentication_method_spec * [MM-56710] Revert to in-memory compilation of CSV report * Update system_users.tsx * Merge'd * Fix test --------- Co-authored-by: M-ZubairAhmed <m-zubairahmed@protonmail.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4f3eba381b
Коммит
03f71c9c84
@@ -12,6 +12,7 @@
|
||||
|
||||
import {createBotPatch} from '../../../support/api/bots';
|
||||
import {generateRandomUser} from '../../../support/api/user';
|
||||
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
||||
|
||||
describe('Bots in lists', () => {
|
||||
let team;
|
||||
@@ -59,13 +60,13 @@ describe('Bots in lists', () => {
|
||||
|
||||
bots.forEach(({username}) => {
|
||||
// # Search for bot
|
||||
cy.get('#searchUsers').clear().type(`@${username}`);
|
||||
cy.get('#input_searchTerm').clear().type(`${username}`).wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
// * Verify bot not in list
|
||||
cy.findByTestId('noUsersFound').should('have.text', 'No users found');
|
||||
cy.get('.noRows').should('have.text', 'No data');
|
||||
|
||||
// * Verify pseudo checksum total of non bot users
|
||||
cy.get('#searchableUserListTotal').contains('0 users of').should('be.visible');
|
||||
cy.get('.adminConsoleListTabletOptionalHead > span').should('have.text', '0 users').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,11 +58,10 @@ describe('Authentication', () => {
|
||||
|
||||
// # Navigate to System Console -> User Management -> Users
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
cy.get('#searchUsers', {timeout: TIMEOUTS.ONE_MIN}).type(testUser.email);
|
||||
cy.findByPlaceholderText('Search users').type(testUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Remove MFA option not available for the user
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Remove MFA').should('not.exist');
|
||||
|
||||
cy.apiLogout();
|
||||
@@ -105,11 +104,10 @@ describe('Authentication', () => {
|
||||
|
||||
// # Navigate to System Console -> User Management -> Users
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
cy.get('#searchUsers', {timeout: TIMEOUTS.ONE_MIN}).type(testUser.email);
|
||||
cy.findByPlaceholderText('Search users').type(testUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Remove MFA option available for the user and click it
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Remove MFA').should('be.visible').click();
|
||||
|
||||
// # Navigate to System Console -> Authentication -> MFA Page.
|
||||
@@ -121,6 +119,7 @@ describe('Authentication', () => {
|
||||
|
||||
// # Click "Save".
|
||||
cy.findByText('Save').click().wait(TIMEOUTS.ONE_SEC);
|
||||
cy.apiLogout();
|
||||
|
||||
// # Login as test user
|
||||
cy.apiLogin(testUser);
|
||||
@@ -138,11 +137,11 @@ describe('Authentication', () => {
|
||||
|
||||
// # Navigate to System Console -> User Management -> Users
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
cy.get('#searchUsers', {timeout: TIMEOUTS.ONE_MIN}).type(testUser.email);
|
||||
cy.findByPlaceholderText('Search users').type(testUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Remove MFA option available for the user and click it
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Remove MFA').should('not.exist');
|
||||
|
||||
// # Done with that MFA stuff so we disable it all
|
||||
|
||||
@@ -127,12 +127,11 @@ describe('Guest Account - Guest User Invitation Flow', () => {
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
|
||||
// # Search for User by username and select the option to update email
|
||||
cy.get('#searchUsers').should('be.visible').type(guest.username);
|
||||
cy.findByPlaceholderText('Search users').should('be.visible').type(guest.username).wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
// # Click on the option to update email
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
cy.findByText('Update Email').should('be.visible').click();
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click();
|
||||
cy.findByText('Update email').should('be.visible').click();
|
||||
|
||||
// * Update email outside whitelisted domain and verify error message
|
||||
cy.findByTestId('resetEmailModal').should('be.visible').within(() => {
|
||||
|
||||
@@ -18,8 +18,6 @@ import * as TIMEOUTS from '../../../../fixtures/timeouts';
|
||||
import {getRandomId} from '../../../../utils';
|
||||
import {getAdminAccount} from '../../../../support/env';
|
||||
|
||||
import {verifyGuest} from './helpers';
|
||||
|
||||
describe('Guest Account - Verify Manage Guest Users', () => {
|
||||
const admin = getAdminAccount();
|
||||
let guestUser: Cypress.UserProfile;
|
||||
@@ -60,18 +58,15 @@ describe('Guest Account - Verify Manage Guest Users', () => {
|
||||
cy.reload();
|
||||
|
||||
// # Search for Guest User by username
|
||||
cy.get('#searchUsers', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible').type(guestUser.username);
|
||||
cy.findByPlaceholderText('Search users').should('be.visible').type(guestUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
});
|
||||
|
||||
it('MM-T1391 Verify the manage options displayed for Guest User', () => {
|
||||
// * Verify Guest user
|
||||
verifyGuest();
|
||||
|
||||
// # Click on the Manage User option
|
||||
cy.wait(TIMEOUTS.HALF_SEC).findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
// * Verify Guest user and Click on the Manage User option
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').should('have.text', 'Guest').click();
|
||||
|
||||
// * Verify the manage options which should be displayed for Guest User
|
||||
const includeOptions = ['Deactivate', 'Manage Roles', 'Manage Teams', 'Reset Password', 'Update Email', 'Promote to Member', 'Revoke Sessions'];
|
||||
const includeOptions = ['Deactivate', 'Manage roles', 'Manage teams', 'Reset password', 'Update email', 'Promote to member', 'Remove sessions'];
|
||||
includeOptions.forEach((includeOption) => {
|
||||
cy.findByText(includeOption).should('be.visible');
|
||||
});
|
||||
@@ -85,8 +80,8 @@ describe('Guest Account - Verify Manage Guest Users', () => {
|
||||
|
||||
it('MM-18048 Change Email of a Guest User and Verify', () => {
|
||||
// # Click on the Update Email option
|
||||
cy.wait(TIMEOUTS.HALF_SEC).findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
cy.wait(TIMEOUTS.HALF_SEC).findByText('Update Email').click();
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').should('have.text', 'Guest').click();
|
||||
cy.findByText('Update email').click();
|
||||
|
||||
// * Update email of Guest User
|
||||
const email = `temp-${getRandomId()}@mattermost.com`;
|
||||
@@ -100,14 +95,14 @@ describe('Guest Account - Verify Manage Guest Users', () => {
|
||||
|
||||
// # Reload and verify if behavior is same
|
||||
cy.reload();
|
||||
cy.get('#searchUsers').should('be.visible').type(guestUser.username);
|
||||
cy.findByPlaceholderText('Search users').should('be.visible').type(guestUser.username);
|
||||
cy.findByText(email).should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-18048 Revoke Session of a Guest User and Verify', () => {
|
||||
// # Click on the Revoke Session option
|
||||
cy.wait(TIMEOUTS.HALF_SEC).findByTestId('userListRow').find('.MenuWrapper a').should('be.visible').click();
|
||||
cy.wait(TIMEOUTS.HALF_SEC).findByText('Revoke Sessions').click();
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').should('have.text', 'Guest').click();
|
||||
cy.findByText('Remove sessions').click();
|
||||
|
||||
// * Verify the confirmation message displayed
|
||||
cy.get('#confirmModal').should('be.visible').within(() => {
|
||||
|
||||
@@ -65,44 +65,36 @@ describe('Settings', () => {
|
||||
const adminUsername = getAdminAccount().username;
|
||||
|
||||
// # Type sysadmin
|
||||
cy.get('#searchUsers').clear().type(adminUsername).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').clear().type(adminUsername).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method
|
||||
cy.findByTestId('userListRow').within(() => {
|
||||
cy.get('.more-modal__details').
|
||||
should('be.visible').
|
||||
and('contain.text', 'Sign-in Method: Email');
|
||||
});
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('have.text', 'Email');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type saml user
|
||||
cy.get('#searchUsers').clear().type(samlUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').clear().type(samlUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method
|
||||
cy.findByTestId('userListRow').within(() => {
|
||||
cy.get('.more-modal__details').
|
||||
should('be.visible').
|
||||
and('contain.text', 'Sign-in Method: SAML');
|
||||
});
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('have.text', 'SAML');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type ldap user
|
||||
cy.get('#searchUsers').clear().type(ldapUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').clear().type(ldapUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method
|
||||
cy.findByTestId('userListRow').within(() => {
|
||||
cy.get('.more-modal__details').
|
||||
should('be.visible').
|
||||
and('contain.text', 'Sign-in Method: LDAP');
|
||||
});
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('have.text', 'LDAP');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type mfa user
|
||||
cy.get('#searchUsers').clear().type(mfaUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').clear().type(mfaUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method
|
||||
cy.findByTestId('userListRow').within(() => {
|
||||
cy.get('.more-modal__details').
|
||||
should('be.visible').
|
||||
and('contain.text', 'MFA: Yes');
|
||||
});
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('.AdminUserCard__body > :nth-child(4) > span:nth-child(3)').should('contain.text', 'MFA');
|
||||
cy.get('a.back').click();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('System Console > User Management > Users', () => {
|
||||
wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click on the searched user name
|
||||
cy.findByText(`@${testUser.username}`).
|
||||
cy.findByText(`${testUser.username}`).
|
||||
should('be.visible').
|
||||
click({force: true});
|
||||
|
||||
@@ -67,15 +67,13 @@ describe('System Console > User Management > Users', () => {
|
||||
|
||||
it('MM-T932 Users - Change a user\'s password', () => {
|
||||
// # Search for the user.
|
||||
cy.get('#searchUsers').type(testUser.email).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').type(testUser.email).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open the actions menu.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .text-right a').
|
||||
click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Reset Password menu option.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .MenuWrapperAnimation-enter-done').
|
||||
find('li').eq(3).click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Type new password and submit.
|
||||
cy.get('input[type=password]').type('new' + testUser.password);
|
||||
@@ -99,22 +97,20 @@ describe('System Console > User Management > Users', () => {
|
||||
|
||||
it('MM-T933 Users - System admin changes own password - Cancel out of changes', () => {
|
||||
// # Search for the admin.
|
||||
cy.get('#searchUsers').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open the actions menu.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .text-right a').
|
||||
click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Reset Password menu option.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .MenuWrapperAnimation-enter-done').
|
||||
find('li').eq(2).click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Type current password and a new password.
|
||||
cy.get('input[type=password]').eq(0).type(otherAdmin.password);
|
||||
cy.get('input[type=password]').eq(1).type('new' + otherAdmin.password);
|
||||
|
||||
// # Click the 'Cancel' button.
|
||||
cy.get('button[type=button].btn.btn-link').should('contain', 'Cancel').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('.modal-footer > .btn-tertiary').should('contain', 'Cancel').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Log out.
|
||||
cy.apiLogout();
|
||||
@@ -125,15 +121,13 @@ describe('System Console > User Management > Users', () => {
|
||||
|
||||
it('MM-T934 Users - System admin changes own password - Incorrect old password', () => {
|
||||
// # Search for the admin.
|
||||
cy.get('#searchUsers').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open the actions menu.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .text-right a').
|
||||
click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Reset Password menu option.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .MenuWrapperAnimation-enter-done').
|
||||
find('li').eq(2).click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Type wrong current password and a new password.
|
||||
cy.get('input[type=password]').eq(0).type('wrong' + otherAdmin.password);
|
||||
@@ -149,15 +143,13 @@ describe('System Console > User Management > Users', () => {
|
||||
|
||||
it('MM-T935 Users - System admin changes own password - Invalid new password', () => {
|
||||
// # Search for the admin.
|
||||
cy.get('#searchUsers').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open the actions menu.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .text-right a').
|
||||
click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Reset Password menu option.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .MenuWrapperAnimation-enter-done').
|
||||
find('li').eq(2).click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Type current password and a new too short password.
|
||||
cy.get('input[type=password]').eq(0).type(otherAdmin.password);
|
||||
@@ -173,15 +165,13 @@ describe('System Console > User Management > Users', () => {
|
||||
|
||||
it('MM-T936 Users - System admin changes own password - Blank fields', () => {
|
||||
// # Search for the admin.
|
||||
cy.get('#searchUsers').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open the actions menu.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .text-right a').
|
||||
click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Reset Password menu option.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .MenuWrapperAnimation-enter-done').
|
||||
find('li').eq(2).click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Click the 'Reset' button.
|
||||
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
@@ -203,15 +193,13 @@ describe('System Console > User Management > Users', () => {
|
||||
|
||||
it('MM-T937 Users - System admin changes own password - Successfully changed', () => {
|
||||
// # Search for the admin.
|
||||
cy.get('#searchUsers').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByPlaceholderText('Search users').type(otherAdmin.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Open the actions menu.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .text-right a').
|
||||
click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Reset Password menu option.
|
||||
cy.get('[data-testid="userListRow"] .more-modal__right .more-modal__actions .MenuWrapper .MenuWrapperAnimation-enter-done').
|
||||
find('li').eq(2).click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Reset password').click();
|
||||
|
||||
// # Type current and new passwords..
|
||||
cy.get('input[type=password]').eq(0).type(otherAdmin.password);
|
||||
|
||||
@@ -52,20 +52,20 @@ describe('User Management', () => {
|
||||
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
|
||||
cy.get('#searchableUserListTotal').then((el) => {
|
||||
cy.get('.adminConsoleListTabletOptionalHead > span').then((el) => {
|
||||
const count1 = el[0].innerText.replace(/\n/g, '').replace(/\s/g, ' ');
|
||||
|
||||
// * Can page through several pages of users.
|
||||
cy.get('#searchableUserListNextBtn').should('be.visible').click();
|
||||
cy.get('.adminConsoleListTabletOptionalHead > .paginationButtons > [aria-label="Go to next page"]').should('be.visible').click();
|
||||
|
||||
// * Count at top changes appropriately.
|
||||
cy.get('#searchableUserListTotal').then((el2) => {
|
||||
cy.get('.adminConsoleListTabletOptionalHead > span').then((el2) => {
|
||||
const count2 = el2[0].innerText.replace(/\n/g, '').replace(/\s/g, ' ');
|
||||
expect(count1).not.equal(count2);
|
||||
});
|
||||
|
||||
// * Can page backward as well.
|
||||
cy.get('#searchableUserListPrevBtn').should('be.visible').click();
|
||||
cy.get('.adminConsoleListTabletOptionalHead > .paginationButtons > [aria-label="Go to previous page"]').should('be.visible').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,10 +103,8 @@ describe('User Management', () => {
|
||||
resetUserEmail(testUser.email, newEmailAddr, '');
|
||||
|
||||
// # Updates immediately in Profile for the user.
|
||||
cy.get('#searchUsers').clear().type(newEmailAddr).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('.more-modal__details').should('be.visible').within(() => {
|
||||
cy.findByText(newEmailAddr).should('exist');
|
||||
});
|
||||
cy.get('#input_searchTerm').clear().type(newEmailAddr).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_emailColumn').should('have.text', newEmailAddr);
|
||||
|
||||
// * User also receives email confirmation that email address has been changed.
|
||||
checkResetEmail(testUser, newEmailAddr);
|
||||
@@ -186,15 +184,21 @@ describe('User Management', () => {
|
||||
cy.apiUpdateUserAuth(gitlabUser.id, gitlabUser.email, '', 'gitlab');
|
||||
|
||||
// # Search for the user.
|
||||
cy.get('#searchUsers').clear().type(gitlabUser.email).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#input_searchTerm').clear().type(gitlabUser.email).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
cy.findByTestId('userListRow').within(() => {
|
||||
// # Open the actions menu.
|
||||
cy.findByText('Member').click().wait(TIMEOUTS.HALF_SEC);
|
||||
// # Open actions menu.
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click();
|
||||
|
||||
// # Click the Update email menu option.
|
||||
cy.findByLabelText('User Actions Menu').findByText('Update Email').should('not.exist');
|
||||
});
|
||||
// * Verify Switch to Email/Password is visible.
|
||||
cy.findByText('Switch to Email/Password').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Set new password.
|
||||
cy.get('input[type=password]').type('new' + testUser.password);
|
||||
cy.get('button[type=submit]').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify Update email option is visible.
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Update email').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,15 +226,10 @@ describe('User Management', () => {
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
|
||||
// # Search for the user.
|
||||
cy.get('#searchUsers').clear().type(oldEmail).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#input_searchTerm').clear().type(oldEmail).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
cy.findByTestId('userListRow').within(() => {
|
||||
// # Open the actions menu.
|
||||
cy.findByText('Member').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click the Update email menu option.
|
||||
cy.findByLabelText('User Actions Menu').findByText('Update Email').click().wait(TIMEOUTS.HALF_SEC);
|
||||
});
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Update email').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Verify the modal opened.
|
||||
cy.findByTestId('resetEmailModal').should('exist');
|
||||
|
||||
@@ -54,8 +54,8 @@ func (a *App) CompileReportChunks(format string, prefix string, numberOfChunks i
|
||||
func (a *App) compileCSVChunks(prefix string, numberOfChunks int, headers []string) *model.AppError {
|
||||
filePath := makeCompiledFilePath(prefix, "csv")
|
||||
|
||||
var headerBuf bytes.Buffer
|
||||
w := csv.NewWriter(&headerBuf)
|
||||
var compiledBuf bytes.Buffer
|
||||
w := csv.NewWriter(&compiledBuf)
|
||||
err := w.Write(headers)
|
||||
if err != nil {
|
||||
return model.NewAppError("compileCSVChunks", "app.compile_csv_chunks.header_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
@@ -64,10 +64,6 @@ func (a *App) compileCSVChunks(prefix string, numberOfChunks int, headers []stri
|
||||
if err = w.Error(); err != nil {
|
||||
return model.NewAppError("saveCSVChunk", "app.save_csv_chunk.write_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
_, appErr := a.WriteFile(&headerBuf, filePath)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
for i := 0; i < numberOfChunks; i++ {
|
||||
chunkFilePath := makeFilePath(prefix, i, "csv")
|
||||
@@ -75,11 +71,17 @@ func (a *App) compileCSVChunks(prefix string, numberOfChunks int, headers []stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = a.AppendFile(bytes.NewReader(chunk), filePath); err != nil {
|
||||
_, writeErr := compiledBuf.Write(chunk)
|
||||
if writeErr != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, appErr := a.WriteFile(&compiledBuf, filePath)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -230,7 +232,7 @@ func (a *App) StartUsersBatchExport(rctx request.CTX, dateRange string, startAt
|
||||
}
|
||||
}
|
||||
|
||||
_, err = a.Srv().Jobs.CreateJobOnce(rctx, model.JobTypeExportUsersToCSV, options)
|
||||
_, err = a.Srv().Jobs.CreateJob(rctx, model.JobTypeExportUsersToCSV, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ func (u *UserReportOptions) IsValid() *AppError {
|
||||
}
|
||||
|
||||
func (u *UserReportQuery) ToReport() *UserReport {
|
||||
u.ClearNonProfileFields()
|
||||
return &UserReport{
|
||||
User: u.User,
|
||||
UserPostStats: u.UserPostStats,
|
||||
|
||||
@@ -1,11 +1,82 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserReportOptions, UserReport, UserReportFilter, ReportDuration} from '@mattermost/types/reports';
|
||||
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
export function setNeedsLoggedInLimitReachedCheck(data: boolean) {
|
||||
return {
|
||||
type: ActionTypes.NEEDS_LOGGED_IN_LIMIT_REACHED_CHECK,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to set the properties of the admin console user management table. Only pass the properties you want to set/modify. If you pass no properties, the table properties will be cleared.
|
||||
*/
|
||||
export function setAdminConsoleUsersManagementTableProperties(data?: Partial<AdminConsoleUserManagementTableProperties>) {
|
||||
if (!data) {
|
||||
return {
|
||||
type: ActionTypes.CLEAR_ADMIN_CONSOLE_USER_MANAGEMENT_TABLE_PROPERTIES,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActionTypes.SET_ADMIN_CONSOLE_USER_MANAGEMENT_TABLE_PROPERTIES,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function getUserReports(options = {} as UserReportOptions): ActionFuncAsync<UserReport[]> {
|
||||
return async (dispatch, getState) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.getUsersForReporting(options);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error: error as ServerError};
|
||||
}
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function getUserCountForReporting(filter = {} as UserReportFilter): ActionFuncAsync<number> {
|
||||
return async (dispatch, getState) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.getUserCountForReporting(filter);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error: error as ServerError};
|
||||
}
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function startUsersBatchExport(dateRange: ReportDuration): ActionFuncAsync {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
await Client4.startUsersBatchExport(dateRange);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error: error as ServerError};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,13 +45,6 @@ export function setUserGridFilters(filters = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setSystemUsersSearch(term, team = '', filter = '') {
|
||||
return {
|
||||
type: SearchTypes.SET_SYSTEM_USERS_SEARCH,
|
||||
data: {term, team, filter},
|
||||
};
|
||||
}
|
||||
|
||||
export function setTeamListSearch(term) {
|
||||
return {
|
||||
type: SearchTypes.SET_TEAM_LIST_SEARCH,
|
||||
|
||||
@@ -71,6 +71,9 @@ class AdminConsole extends React.PureComponent<Props, State> {
|
||||
document.body.classList.remove('console__body');
|
||||
document.getElementById('root')?.classList.remove('console__root');
|
||||
applyTheme(this.props.currentTheme);
|
||||
|
||||
// Reset the admin console users management table properties
|
||||
this.props.actions.setAdminConsoleUsersManagementTableProperties();
|
||||
}
|
||||
|
||||
private handleSearchChange = (search: string) => {
|
||||
|
||||
@@ -47,9 +47,7 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match d
|
||||
<div
|
||||
class="AdminUserCard__user-id"
|
||||
>
|
||||
User ID:
|
||||
|
||||
1234
|
||||
User ID: 1234
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -104,9 +102,7 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
<div
|
||||
class="AdminUserCard__user-id"
|
||||
>
|
||||
User ID:
|
||||
|
||||
1234
|
||||
User ID: 1234
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -159,9 +155,7 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
<div
|
||||
class="AdminUserCard__user-id"
|
||||
>
|
||||
User ID:
|
||||
|
||||
1234
|
||||
User ID: 1234
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -216,9 +210,7 @@ exports[`components/admin_console/admin_user_card/admin_user_card should match s
|
||||
<div
|
||||
class="AdminUserCard__user-id"
|
||||
>
|
||||
User ID:
|
||||
|
||||
1234
|
||||
User ID: 1234
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
margin: 2em 0 1em;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.5);
|
||||
|
||||
.noUserBody {
|
||||
display: flex;
|
||||
min-height: 20vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.AdminUserCard__header {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import ProfilePicture from 'components/profile_picture';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
import './admin_user_card.scss';
|
||||
|
||||
@@ -18,19 +18,37 @@ type BulletProps = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
user: UserProfile;
|
||||
user?: UserProfile;
|
||||
isLoading?: boolean;
|
||||
body?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Bullet: React.FC<BulletProps> = (props: BulletProps) => {
|
||||
if ((props.user.first_name || props.user.last_name) && props.user.nickname) {
|
||||
return (<span>{' • '}</span>);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const AdminUserCard = ({isLoading = false, ...props}: Props) => {
|
||||
if (!props.user || isLoading) {
|
||||
return (
|
||||
<div className='AdminUserCard'>
|
||||
<div className='AdminUserCard__header'>
|
||||
<ProfilePicture
|
||||
src=''
|
||||
size='xxl'
|
||||
wrapperClass='admin-user-card'
|
||||
/>
|
||||
</div>
|
||||
<div className='noUserBody'>
|
||||
{isLoading && <LoadingSpinner/>}
|
||||
{!isLoading &&
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.notFound'
|
||||
defaultMessage='User not found'
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AdminUserCard: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='AdminUserCard'>
|
||||
<div className='AdminUserCard__header'>
|
||||
@@ -46,7 +64,13 @@ const AdminUserCard: React.FC<Props> = (props: Props) => {
|
||||
<span className='AdminUserCard__user-nickname'>{props.user.nickname}</span>
|
||||
</div>
|
||||
<div className='AdminUserCard__user-id'>
|
||||
{Utils.localizeMessage('admin.userManagement.userDetail.userId', 'User ID:')} {props.user.id}
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.userId'
|
||||
defaultMessage='User ID: {userId}'
|
||||
values={{
|
||||
userId: props.user.id,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='AdminUserCard__body'>
|
||||
@@ -58,4 +82,11 @@ const AdminUserCard: React.FC<Props> = (props: Props) => {
|
||||
</div>);
|
||||
};
|
||||
|
||||
const Bullet = (props: BulletProps) => {
|
||||
if ((props.user.first_name || props.user.last_name) && props.user.nickname) {
|
||||
return (<span>{' • '}</span>);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default AdminUserCard;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {isCurrentUserSystemAdmin, currentUserHasAnAdminRole, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {setNavigationBlocked, deferNavigation, cancelNavigation, confirmNavigation} from 'actions/admin_actions.jsx';
|
||||
import {setAdminConsoleUsersManagementTableProperties} from 'actions/views/admin';
|
||||
import {selectLhsItem} from 'actions/views/lhs';
|
||||
import {getAdminDefinition, getConsoleAccess} from 'selectors/admin_console';
|
||||
import {showNavigationPrompt} from 'selectors/views/admin';
|
||||
@@ -68,6 +69,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
editRole,
|
||||
selectLhsItem,
|
||||
selectTeam,
|
||||
setAdminConsoleUsersManagementTableProperties,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper} from '@tanstack/react-table';
|
||||
export type {CellContext, PaginationState, SortingState, OnChangeFn, ColumnDef} from '@tanstack/react-table';
|
||||
|
||||
export {ListTable as AdminConsoleListTable, PAGE_SIZES} from './list_table';
|
||||
export {ListTable as AdminConsoleListTable, PAGE_SIZES, LoadingStates} from './list_table';
|
||||
export type {TableMeta, PageSizeOption} from './list_table';
|
||||
|
||||
export {ElapsedDurationCell} from './elapsed_duration_cell';
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@mixin textElipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.adminConsoleListTableContainer {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table.adminConsoleListTable {
|
||||
// This is rgba(var(--sys-center-channel-color-rgb), 0.08) but since we cannot use opacity
|
||||
// due to sticky row which shows the background thus a defined color is used
|
||||
@@ -17,10 +27,10 @@ table.adminConsoleListTable {
|
||||
white-space: nowrap;
|
||||
|
||||
th {
|
||||
max-width: 50rem;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
padding-block-end: 12px;
|
||||
padding-block-start: 12px;
|
||||
vertical-align: middle;
|
||||
|
||||
&:first-child {
|
||||
padding-inline-end: 12px;
|
||||
@@ -42,7 +52,6 @@ table.adminConsoleListTable {
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
left: 0;
|
||||
min-width: 20rem;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
|
||||
&:first-child {
|
||||
@@ -81,8 +90,6 @@ table.adminConsoleListTable {
|
||||
}
|
||||
|
||||
&.sortable {
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover {
|
||||
background-color: $sysCenterChannelColorWith8Alpha;
|
||||
cursor: pointer;
|
||||
@@ -103,25 +110,6 @@ table.adminConsoleListTable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sortable.pinned {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& > span {
|
||||
&:first-child {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pinned:not(.sortable) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +132,11 @@ table.adminConsoleListTable {
|
||||
td.pinned {
|
||||
background-color: $sysCenterChannelColorWith8Alpha;
|
||||
}
|
||||
|
||||
td.noRows {
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -203,6 +196,10 @@ table.adminConsoleListTable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.noRows {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,11 +230,14 @@ table.adminConsoleListTable {
|
||||
.adminConsoleListTablePageSize {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
justify-self: flex-end;
|
||||
margin-inline-end: 12px;
|
||||
|
||||
& > .react-select {
|
||||
min-width: 64px;
|
||||
min-width: 72px;
|
||||
margin: 0 6px;
|
||||
|
||||
.react-select__value-container {
|
||||
|
||||
@@ -10,10 +10,7 @@ import {FormattedMessage, defineMessages, useIntl} from 'react-intl';
|
||||
import ReactSelect, {components} from 'react-select';
|
||||
import type {IndicatorContainerProps, ValueType} from 'react-select';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
import {Pagination} from './pagination';
|
||||
|
||||
@@ -47,18 +44,24 @@ export type PageSizeOption = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
export enum LoadingStates {
|
||||
Loading = 'loading',
|
||||
Loaded = 'loaded',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
export type TableMeta = {
|
||||
tableId: string;
|
||||
tableCaption?: string;
|
||||
isLoading?: boolean;
|
||||
loadingState?: LoadingStates;
|
||||
emptyDataMessage?: ReactNode;
|
||||
onRowClick?: (row: string) => void;
|
||||
disablePrevPage?: boolean;
|
||||
disableNextPage?: boolean;
|
||||
onPreviousPageClick?: () => void;
|
||||
onNextPageClick?: () => void;
|
||||
paginationInfo?: ReactNode;
|
||||
totalRowInfo?: ReactNode;
|
||||
hasAdditionalPaginationAtTop?: boolean;
|
||||
hasDualSidedPagination?: boolean;
|
||||
};
|
||||
|
||||
interface TableMandatoryTypes {
|
||||
@@ -111,24 +114,28 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
}
|
||||
}
|
||||
|
||||
const colCount = props.table.getAllColumns().length;
|
||||
const rowCount = props.table.getRowModel().rows.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='adminConsoleListTableContainer'>
|
||||
<div className='adminConsoleListTabletOptionalHead'>
|
||||
{tableMeta.totalRowInfo}
|
||||
{tableMeta.hasAdditionalPaginationAtTop && (
|
||||
<Pagination
|
||||
disablePrevPage={tableMeta.disablePrevPage}
|
||||
disableNextPage={tableMeta.disableNextPage}
|
||||
isLoading={tableMeta.isLoading}
|
||||
onPreviousPageClick={tableMeta.onPreviousPageClick}
|
||||
onNextPageClick={tableMeta.onNextPageClick}
|
||||
paginationInfo={tableMeta.paginationInfo}
|
||||
/>
|
||||
{tableMeta.hasDualSidedPagination && (
|
||||
<>
|
||||
{tableMeta.paginationInfo}
|
||||
<Pagination
|
||||
disablePrevPage={tableMeta.disablePrevPage}
|
||||
disableNextPage={tableMeta.disableNextPage}
|
||||
isLoading={tableMeta.loadingState === LoadingStates.Loading}
|
||||
onPreviousPageClick={tableMeta.onPreviousPageClick}
|
||||
onNextPageClick={tableMeta.onNextPageClick}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<table
|
||||
id={tableMeta.tableId}
|
||||
aria-colcount={props.table.getAllColumns().length}
|
||||
aria-colcount={colCount}
|
||||
aria-describedby={`${tableMeta.tableId}-headerId`} // Set this id to the table header so that the title describes the table
|
||||
className={classNames(
|
||||
'adminConsoleListTable',
|
||||
@@ -145,12 +152,12 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
id={`${headerIdPrefix}${header.id}`}
|
||||
colSpan={header.colSpan}
|
||||
scope='col'
|
||||
role='columnheader'
|
||||
aria-sort={getAriaSortForTableHeader(header.column.getCanSort(), header.column.getIsSorted())}
|
||||
className={classNames(`${header.id}`, {
|
||||
[SORTABLE_CLASS]: header.column.getCanSort(),
|
||||
[PINNED_CLASS]: header.column.getCanPin(),
|
||||
})}
|
||||
disabled={header.column.getCanSort() && tableMeta.loadingState === LoadingStates.Loading}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
@@ -176,29 +183,6 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
className='icon icon-arrow-up hoverSortingIcon'
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pinned Icon */}
|
||||
{header.column.getCanPin() && (
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement={
|
||||
header.index === 0 ? 'bottom' : 'left'
|
||||
}
|
||||
overlay={
|
||||
<Tooltip id='system-users-column-pinned-tooltip'>
|
||||
<FormattedMessage
|
||||
id='adminConsole.list.table.pinnedColumn'
|
||||
defaultMessage='This column is pinned'
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='icon icon-pin-outline'
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -209,14 +193,12 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
<tr
|
||||
id={`${rowIdPrefix}${row.original.id}`}
|
||||
key={row.id}
|
||||
role='row'
|
||||
onClick={handleRowClick}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
id={`${cellIdPrefix}${cell.id}`}
|
||||
role='cell'
|
||||
headers={`${headerIdPrefix}${cell.column.id}`}
|
||||
className={classNames(`${cell.column.id}`, {
|
||||
[PINNED_CLASS]: cell.column.getCanPin(),
|
||||
@@ -227,6 +209,47 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{/* State where it is initially loading the data */}
|
||||
{(tableMeta.loadingState === LoadingStates.Loading && rowCount === 0) && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={colCount}
|
||||
className='noRows'
|
||||
disabled={true}
|
||||
>
|
||||
<LoadingSpinner
|
||||
text={formatMessage({id: 'adminConsole.list.table.genericLoading', defaultMessage: 'Loading'})}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* State where there is no data */}
|
||||
{(tableMeta.loadingState === LoadingStates.Loaded && rowCount === 0) && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={colCount}
|
||||
className='noRows'
|
||||
disabled={true}
|
||||
>
|
||||
{tableMeta.emptyDataMessage || formatMessage({id: 'adminConsole.list.table.genericNoData', defaultMessage: 'No data'})}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* State where there is an error loading the data */}
|
||||
{tableMeta.loadingState === LoadingStates.Failed && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={colCount}
|
||||
className='noRows'
|
||||
disabled={true}
|
||||
>
|
||||
{formatMessage({id: 'adminConsole.list.table.genericError', defaultMessage: 'There was an error loading the data, please try again'})}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{props.table.getFooterGroups().map((footerGroup) => (
|
||||
@@ -247,16 +270,11 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
</tfoot>
|
||||
</table>
|
||||
<div className='adminConsoleListTabletOptionalFoot'>
|
||||
{tableMeta.paginationInfo}
|
||||
{handlePageSizeChange && (
|
||||
<div
|
||||
className='adminConsoleListTablePageSize'
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: 'adminConsole.list.table.rowCount.label',
|
||||
defaultMessage: 'Show {count} rows per page',
|
||||
},
|
||||
{count: selectedPageSize.label},
|
||||
)}
|
||||
aria-label={formatMessage({id: 'adminConsole.list.table.rowCount.label', defaultMessage: 'Show {count} rows per page'}, {count: selectedPageSize.label})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='adminConsole.list.table.rowsCount.(show)rowsPerPage'
|
||||
@@ -273,7 +291,7 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
options={pageSizeOptions}
|
||||
value={selectedPageSize}
|
||||
onChange={handlePageSizeChange}
|
||||
isDisabled={tableMeta.isLoading}
|
||||
isDisabled={tableMeta.loadingState === LoadingStates.Loading}
|
||||
components={{
|
||||
IndicatorSeparator: null,
|
||||
IndicatorsContainer,
|
||||
@@ -288,13 +306,12 @@ export function ListTable<TableType extends TableMandatoryTypes>(
|
||||
<Pagination
|
||||
disablePrevPage={tableMeta.disablePrevPage}
|
||||
disableNextPage={tableMeta.disableNextPage}
|
||||
isLoading={tableMeta.isLoading}
|
||||
isLoading={tableMeta.loadingState === LoadingStates.Loading}
|
||||
onPreviousPageClick={tableMeta.onPreviousPageClick}
|
||||
onNextPageClick={tableMeta.onNextPageClick}
|
||||
paginationInfo={tableMeta.paginationInfo}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,7 +328,7 @@ function getAriaSortForTableHeader(
|
||||
sortDirection: boolean | SortDirection,
|
||||
): AriaAttributes['aria-sort'] {
|
||||
if (!canSort) {
|
||||
return 'none';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
|
||||
@@ -6,22 +6,21 @@ import {useIntl} from 'react-intl';
|
||||
|
||||
import type {TableMeta} from './list_table';
|
||||
|
||||
type Props = Pick<
|
||||
TableMeta,
|
||||
| 'onPreviousPageClick'
|
||||
| 'onNextPageClick'
|
||||
| 'disablePrevPage'
|
||||
| 'disableNextPage'
|
||||
| 'isLoading'
|
||||
| 'paginationInfo'
|
||||
>;
|
||||
interface Props
|
||||
extends Pick<TableMeta,
|
||||
| 'onPreviousPageClick'
|
||||
| 'onNextPageClick'
|
||||
| 'disablePrevPage'
|
||||
| 'disableNextPage'
|
||||
> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function Pagination(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<div className='paginationButtons'>
|
||||
{props.paginationInfo}
|
||||
{props.onPreviousPageClick && (
|
||||
<button
|
||||
className='btn btn-icon btn-sm'
|
||||
|
||||
@@ -23,17 +23,20 @@ import {DeveloperLinks} from 'utils/constants';
|
||||
import {isSuccess} from 'types/actions';
|
||||
|
||||
export type Props = {
|
||||
show: boolean;
|
||||
user?: UserProfile;
|
||||
userAccessTokensEnabled: boolean;
|
||||
|
||||
// defining custom function type instead of using React.MouseEventHandler
|
||||
// to make the event optional
|
||||
onModalDismissed: (e?: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
actions: { updateUserRoles: (userId: string, roles: string) => Promise<ActionResult>};
|
||||
onSuccess: (roles: string) => void;
|
||||
onExited: () => void;
|
||||
actions: {
|
||||
updateUserRoles: (userId: string, roles: string) => Promise<ActionResult>;
|
||||
};
|
||||
}
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
user?: UserProfile;
|
||||
error: any | null;
|
||||
hasPostAllRole: boolean;
|
||||
@@ -46,6 +49,7 @@ function getStateFromProps(props: Props): State {
|
||||
const roles = props.user && props.user.roles ? props.user.roles : '';
|
||||
|
||||
return {
|
||||
show: true,
|
||||
user: props.user,
|
||||
error: null,
|
||||
hasPostAllRole: UserUtils.hasPostAllRole(roles),
|
||||
@@ -120,6 +124,10 @@ export default class ManageRolesModal extends React.PureComponent<Props, State>
|
||||
}
|
||||
};
|
||||
|
||||
onHide = () => {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
handleSave = async () => {
|
||||
this.setState({error: null});
|
||||
|
||||
@@ -140,7 +148,8 @@ export default class ManageRolesModal extends React.PureComponent<Props, State>
|
||||
this.trackRoleChanges(roles, this.props.user!.roles);
|
||||
|
||||
if (isSuccess(result)) {
|
||||
this.props.onModalDismissed();
|
||||
this.props.onSuccess(roles);
|
||||
this.onHide();
|
||||
} else {
|
||||
this.handleError(
|
||||
<FormattedMessage
|
||||
@@ -343,8 +352,9 @@ export default class ManageRolesModal extends React.PureComponent<Props, State>
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.props.onModalDismissed}
|
||||
show={this.state.show}
|
||||
onHide={this.onHide}
|
||||
onExited={this.props.onExited}
|
||||
dialogClassName='a11y__modal manage-teams'
|
||||
role='dialog'
|
||||
aria-labelledby='manageRolesModalLabel'
|
||||
@@ -368,7 +378,7 @@ export default class ManageRolesModal extends React.PureComponent<Props, State>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-tertiary'
|
||||
onClick={this.props.onModalDismissed}
|
||||
onClick={this.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.manage_roles.cancel'
|
||||
|
||||
@@ -23,7 +23,8 @@ exports[`ManageTeamsModal should match snapshot init 1`] = `
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onHide={[MockFunction]}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
|
||||
@@ -18,8 +18,8 @@ import ManageTeamsDropdown from './manage_teams_dropdown';
|
||||
describe('ManageTeamsModal', () => {
|
||||
const baseProps = {
|
||||
locale: General.DEFAULT_LOCALE,
|
||||
onModalDismissed: jest.fn(),
|
||||
show: true,
|
||||
onHide: jest.fn(),
|
||||
onExited: jest.fn(),
|
||||
user: TestHelper.getUserMock({
|
||||
id: 'currentUserId',
|
||||
last_picture_update: 1234,
|
||||
|
||||
@@ -23,9 +23,8 @@ import RemoveFromTeamButton from './remove_from_team_button';
|
||||
|
||||
export type Props = {
|
||||
locale: string;
|
||||
onModalDismissed: () => void;
|
||||
show: boolean;
|
||||
user?: UserProfile;
|
||||
onExited: () => void;
|
||||
actions: {
|
||||
getTeamMembersForUser: (userId: string) => Promise<ActionResult>;
|
||||
getTeamsForUser: (userId: string) => Promise<ActionResult>;
|
||||
@@ -34,10 +33,11 @@ export type Props = {
|
||||
};
|
||||
}
|
||||
|
||||
const ManageTeamsModal = ({locale, onModalDismissed, show, user, actions}: Props) => {
|
||||
const ManageTeamsModal = ({locale, onExited, user, actions}: Props) => {
|
||||
const [error, setError] = React.useState<JSX.Element | null>(null);
|
||||
const [teams, setTeams] = React.useState<Team[] | null>(null);
|
||||
const [teamMembers, setTeamMembers] = React.useState<TeamMembership[] | null>(null);
|
||||
const [show, setShow] = React.useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
@@ -52,6 +52,10 @@ const ManageTeamsModal = ({locale, onModalDismissed, show, user, actions}: Props
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
const onModalDismissed = () => {
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
const loadTeamsAndTeamMembers = async (user: UserProfile) => {
|
||||
await getTeamMembers(user.id);
|
||||
const {data} = await actions.getTeamsForUser(user.id);
|
||||
@@ -185,6 +189,7 @@ const ManageTeamsModal = ({locale, onModalDismissed, show, user, actions}: Props
|
||||
<Modal
|
||||
show={show}
|
||||
onHide={onModalDismissed}
|
||||
onExited={onExited}
|
||||
dialogClassName='a11y__modal manage-teams modal--overflow-visible'
|
||||
role='dialog'
|
||||
aria-labelledby='manageTeamsModalLabel'
|
||||
|
||||
@@ -23,11 +23,12 @@ exports[`components/admin_console/manage_tokens_modal/manage_tokens_modal.tsx sh
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onHide={[MockFunction]}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={false}
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('components/admin_console/manage_tokens_modal/manage_tokens_modal.tsx',
|
||||
user: TestHelper.getUserMock({
|
||||
id: 'defaultuser',
|
||||
}),
|
||||
show: false,
|
||||
onModalDismissed: jest.fn(),
|
||||
onHide: jest.fn(),
|
||||
onExited: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -19,11 +19,6 @@ import {DeveloperLinks} from 'utils/constants';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* Set to render the modal
|
||||
*/
|
||||
show: boolean;
|
||||
|
||||
/**
|
||||
* The user the roles are being managed for
|
||||
*/
|
||||
@@ -34,10 +29,7 @@ export type Props = {
|
||||
*/
|
||||
userAccessTokens?: Record<string, UserAccessToken>;
|
||||
|
||||
/**
|
||||
* Function called when modal is dismissed
|
||||
*/
|
||||
onModalDismissed: (e?: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onExited: () => void;
|
||||
actions: {
|
||||
|
||||
/**
|
||||
@@ -48,6 +40,7 @@ export type Props = {
|
||||
};
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@@ -55,6 +48,7 @@ export default class ManageTokensModal extends React.PureComponent<Props, State>
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
@@ -73,6 +67,10 @@ export default class ManageTokensModal extends React.PureComponent<Props, State>
|
||||
});
|
||||
};
|
||||
|
||||
private onModalDismissed = () => {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
private renderContents = (): JSX.Element => {
|
||||
const {user, userAccessTokens} = this.props;
|
||||
|
||||
@@ -188,8 +186,9 @@ export default class ManageTokensModal extends React.PureComponent<Props, State>
|
||||
public render = (): JSX.Element => {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.props.onModalDismissed}
|
||||
show={this.state.show}
|
||||
onHide={this.onModalDismissed}
|
||||
onExited={this.props.onExited}
|
||||
dialogClassName='a11y__modal manage-teams'
|
||||
role='dialog'
|
||||
aria-labelledby='manageTokensModalLabel'
|
||||
|
||||
@@ -24,6 +24,7 @@ exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
@@ -138,6 +139,7 @@ exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
|
||||
@@ -13,8 +13,6 @@ import {TestHelper} from 'utils/test_helper';
|
||||
import ResetEmailModal from './reset_email_modal';
|
||||
|
||||
describe('components/admin_console/reset_email_modal/reset_email_modal.tsx', () => {
|
||||
const emptyFunction = jest.fn();
|
||||
|
||||
const user: UserProfile = TestHelper.getUserMock({
|
||||
email: 'arvin.darmawan@gmail.com',
|
||||
});
|
||||
@@ -23,9 +21,9 @@ describe('components/admin_console/reset_email_modal/reset_email_modal.tsx', ()
|
||||
actions: {patchUser: jest.fn(() => Promise.resolve({}))},
|
||||
user,
|
||||
currentUserId: 'random_user_id',
|
||||
show: true,
|
||||
onModalSubmit: emptyFunction,
|
||||
onModalDismissed: emptyFunction,
|
||||
onHide: jest.fn(),
|
||||
onSuccess: jest.fn(),
|
||||
onExited: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot when not the current user', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
error: JSX.Element|string|null;
|
||||
isEmailError: boolean;
|
||||
isCurrentPasswordError: boolean;
|
||||
@@ -19,9 +20,8 @@ type State = {
|
||||
type Props = {
|
||||
user?: UserProfile;
|
||||
currentUserId: string;
|
||||
show: boolean;
|
||||
onModalSubmit: (user?: UserProfile) => void;
|
||||
onModalDismissed: () => void;
|
||||
onSuccess: (email: string) => void;
|
||||
onExited: () => void;
|
||||
actions: {
|
||||
patchUser: (user: UserProfile) => Promise<ActionResult>;
|
||||
};
|
||||
@@ -30,14 +30,12 @@ type Props = {
|
||||
export default class ResetEmailModal extends React.PureComponent<Props, State> {
|
||||
private emailRef: React.RefObject<HTMLInputElement>;
|
||||
private currentPasswordRef: React.RefObject<HTMLInputElement>;
|
||||
public static defaultProps: Partial<Props> = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
show: true,
|
||||
error: null,
|
||||
isEmailError: false,
|
||||
isCurrentPasswordError: false,
|
||||
@@ -47,20 +45,6 @@ export default class ResetEmailModal extends React.PureComponent<Props, State> {
|
||||
this.currentPasswordRef = React.createRef();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props): void {
|
||||
if (!prevProps.show && this.props.show) {
|
||||
this.resetState();
|
||||
}
|
||||
}
|
||||
|
||||
private resetState = (): void => {
|
||||
this.setState({
|
||||
error: null,
|
||||
isEmailError: false,
|
||||
isCurrentPasswordError: false,
|
||||
});
|
||||
};
|
||||
|
||||
private isEmailValid = (): boolean => {
|
||||
if (!this.emailRef.current || !this.emailRef.current.value || !isEmail(this.emailRef.current.value)) {
|
||||
const errMsg = (
|
||||
@@ -125,14 +109,15 @@ export default class ResetEmailModal extends React.PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onModalSubmit(this.props.user);
|
||||
this.props.onSuccess(user.email);
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
private doCancel = (): void => {
|
||||
this.setState({
|
||||
show: false,
|
||||
error: null,
|
||||
});
|
||||
this.props.onModalDismissed();
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -153,8 +138,9 @@ export default class ResetEmailModal extends React.PureComponent<Props, State> {
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal'
|
||||
show={this.props.show}
|
||||
show={this.state.show}
|
||||
onHide={this.doCancel}
|
||||
onExited={this.props.onExited}
|
||||
role='dialog'
|
||||
aria-labelledby='resetEmailModalLabel'
|
||||
data-testid='resetEmailModal'
|
||||
|
||||
@@ -23,6 +23,7 @@ exports[`components/admin_console/reset_password_modal/reset_password_modal.tsx
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {TestHelper} from 'utils/test_helper';
|
||||
import ResetPasswordModal from './reset_password_modal';
|
||||
|
||||
describe('components/admin_console/reset_password_modal/reset_password_modal.tsx', () => {
|
||||
const emptyFunction = jest.fn();
|
||||
const notifyProps: UserNotifyProps = {
|
||||
channel: 'true',
|
||||
comments: 'never',
|
||||
@@ -37,9 +36,8 @@ describe('components/admin_console/reset_password_modal/reset_password_modal.tsx
|
||||
actions: {updateUserPassword: jest.fn(() => Promise.resolve({data: ''}))},
|
||||
currentUserId: user.id,
|
||||
user,
|
||||
show: true,
|
||||
onModalSubmit: emptyFunction,
|
||||
onModalDismissed: emptyFunction,
|
||||
onHide: jest.fn(),
|
||||
onExited: jest.fn(),
|
||||
passwordConfig: {
|
||||
minimumLength: 10,
|
||||
requireLowercase: true,
|
||||
|
||||
@@ -20,6 +20,7 @@ interface PasswordConfig {
|
||||
}
|
||||
|
||||
type State = {
|
||||
show: boolean;
|
||||
serverErrorNewPass: React.ReactNode;
|
||||
serverErrorCurrentPass: React.ReactNode;
|
||||
}
|
||||
@@ -27,9 +28,8 @@ type State = {
|
||||
export type Props = {
|
||||
user?: UserProfile;
|
||||
currentUserId: string;
|
||||
show: boolean;
|
||||
onModalSubmit: (user?: UserProfile) => void;
|
||||
onModalDismissed: () => void;
|
||||
onSuccess?: () => void;
|
||||
onExited: () => void;
|
||||
passwordConfig: PasswordConfig;
|
||||
actions: {
|
||||
updateUserPassword: (userId: string, currentPassword: string, password: string) => Promise<ActionResult>;
|
||||
@@ -39,14 +39,12 @@ export type Props = {
|
||||
export default class ResetPasswordModal extends React.PureComponent<Props, State> {
|
||||
private currentPasswordRef: React.RefObject<HTMLInputElement>;
|
||||
private passwordRef: React.RefObject<HTMLInputElement>;
|
||||
public static defaultProps: Partial<Props> = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
show: true,
|
||||
serverErrorNewPass: null,
|
||||
serverErrorCurrentPass: null,
|
||||
};
|
||||
@@ -98,15 +96,16 @@ export default class ResetPasswordModal extends React.PureComponent<Props, State
|
||||
this.setState({serverErrorCurrentPass: result.error.message});
|
||||
return;
|
||||
}
|
||||
this.props.onModalSubmit(this.props.user);
|
||||
this.props.onSuccess?.();
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
private doCancel = (): void => {
|
||||
this.setState({
|
||||
show: false,
|
||||
serverErrorNewPass: null,
|
||||
serverErrorCurrentPass: null,
|
||||
});
|
||||
this.props.onModalDismissed();
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -177,8 +176,9 @@ export default class ResetPasswordModal extends React.PureComponent<Props, State
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal'
|
||||
show={this.props.show}
|
||||
show={this.state.show}
|
||||
onHide={this.doCancel}
|
||||
onExited={this.props.onExited}
|
||||
role='dialog'
|
||||
aria-labelledby='resetPasswordModalLabel'
|
||||
>
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,49 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {addUserToTeam} from 'mattermost-redux/actions/teams';
|
||||
import {updateUserActive} from 'mattermost-redux/actions/users';
|
||||
import {updateUserActive, getUser, patchUser, updateUserMfa} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import SystemUserDetail from './system_user_detail';
|
||||
|
||||
type OwnProps = {
|
||||
match: any;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const userId = ownProps.match.params.user_id;
|
||||
const user = getUser(state, userId);
|
||||
|
||||
return {
|
||||
user,
|
||||
mfaEnabled: config.EnableMultifactorAuthentication === 'true',
|
||||
mfaEnabled: config?.EnableMultifactorAuthentication === 'true' || false,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
const apiActions = bindActionCreators({
|
||||
updateUserActive,
|
||||
addUserToTeam,
|
||||
}, dispatch);
|
||||
const uiActions = bindActionCreators({
|
||||
setNavigationBlocked,
|
||||
}, dispatch);
|
||||
const mapDispatchToProps = {
|
||||
getUser,
|
||||
patchUser,
|
||||
updateUserActive,
|
||||
updateUserMfa,
|
||||
addUserToTeam,
|
||||
setNavigationBlocked,
|
||||
openModal,
|
||||
};
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
const props = {
|
||||
actions: Object.assign(apiActions, uiActions),
|
||||
};
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SystemUserDetail);
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
export default connector(SystemUserDetail);
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
@import 'utils/variables';
|
||||
.SystemUserDetail {
|
||||
.AdminUserCard__body {
|
||||
label {
|
||||
display: block;
|
||||
font-weight: unset;
|
||||
|
||||
.SystemUserDetail__field-label {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
> span:nth-child(1) {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.SystemUserDetail__input {
|
||||
display: inline-block;
|
||||
max-width: 320px;
|
||||
height: 40px;
|
||||
}
|
||||
> span:nth-child(2) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.SystemUserDetail .add-team-button {
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
}
|
||||
input {
|
||||
display: inline-block;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.SystemUserDetail .team-btn__content {
|
||||
opacity: 1;
|
||||
}
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 8px 0 0;
|
||||
fill: rgba(var(--sys-color-sidebar-text-rgb), 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.SystemUserDetail__field-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.SystemUserDetail__field-icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 8px 0 0;
|
||||
fill: rgba(61, 60, 64, 0.6);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.SystemUserDetail__error {
|
||||
padding-top: 8px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.SystemUserDetail__actions-menu-icon svg {
|
||||
width: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.SystemUserDetail .MenuItem .SystemUserDetail__action-remove-team {
|
||||
color: #8b0008;
|
||||
.teamlistLoading {
|
||||
display: flex;
|
||||
min-height: 20vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,39 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {RouteComponentProps} from 'react-router-dom';
|
||||
|
||||
import SystemUserDetail from 'components/admin_console/system_user_detail/system_user_detail';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {Constants} from 'utils/constants';
|
||||
import SystemUserDetail, {getUserAuthenticationTextField} from 'components/admin_console/system_user_detail/system_user_detail';
|
||||
import type {
|
||||
Props,
|
||||
Params,
|
||||
} from 'components/admin_console/system_user_detail/system_user_detail';
|
||||
|
||||
jest.mock('actions/admin_actions.jsx');
|
||||
import {shallowWithIntl, type MockIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
describe('components/admin_console/system_user_detail', () => {
|
||||
const defaultProps = {
|
||||
user: {
|
||||
username: 'jim.halpert',
|
||||
first_name: 'Jim',
|
||||
last_name: 'Halpert',
|
||||
nickname: 'Big Tuna',
|
||||
id: '1234',
|
||||
roles: 'system_user',
|
||||
},
|
||||
actions: {
|
||||
updateUserActive: jest.fn(),
|
||||
setNavigationBlocked: jest.fn(),
|
||||
addUserToTeam: jest.fn(),
|
||||
},
|
||||
} as any;
|
||||
describe('SystemUserDetail', () => {
|
||||
const defaultProps: Props = {
|
||||
mfaEnabled: false,
|
||||
patchUser: jest.fn(),
|
||||
updateUserMfa: jest.fn(),
|
||||
getUser: jest.fn(),
|
||||
updateUserActive: jest.fn(),
|
||||
setNavigationBlocked: jest.fn(),
|
||||
addUserToTeam: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
} as MockIntl,
|
||||
...({
|
||||
match: {
|
||||
params: {
|
||||
user_id: 'user_id',
|
||||
},
|
||||
},
|
||||
} as RouteComponentProps<Params>),
|
||||
};
|
||||
|
||||
test('should match default snapshot', () => {
|
||||
const props = defaultProps;
|
||||
@@ -33,76 +42,41 @@ describe('components/admin_console/system_user_detail', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should redirect if user id is not defined', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
user: {
|
||||
id: null,
|
||||
},
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot if user is inactive', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
user: {
|
||||
...defaultProps.user,
|
||||
delete_at: 1561683854166,
|
||||
},
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot if MFA is enabled', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
user: {
|
||||
...defaultProps.user,
|
||||
mfa_active: 'MFA',
|
||||
},
|
||||
mfaEnabled: true,
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot if no nickname is defined', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
user: {
|
||||
...defaultProps.user,
|
||||
nickname: null,
|
||||
},
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot if LDAP Authentication', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
user: {
|
||||
...defaultProps.user,
|
||||
auth_service: Constants.LDAP_SERVICE,
|
||||
},
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot if SAML Authentication', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
user: {
|
||||
...defaultProps.user,
|
||||
auth_service: Constants.SAML_SERVICE,
|
||||
},
|
||||
mfaEnabled: true,
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAuthenticationTextField', () => {
|
||||
const intl = {formatMessage: ({defaultMessage}) => defaultMessage} as MockIntl;
|
||||
|
||||
it('should return empty string if user is not provided', () => {
|
||||
const result = getUserAuthenticationTextField(intl, false, undefined);
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
it('should return email if user has no auth service and MFA is not enabled', () => {
|
||||
const result = getUserAuthenticationTextField(intl, false, {auth_service: '', mfa_active: false} as UserProfile);
|
||||
expect(result).toEqual('Email');
|
||||
});
|
||||
|
||||
it('should return auth service in uppercase if it is LDAP or SAML', () => {
|
||||
const result = getUserAuthenticationTextField(intl, false, {auth_service: 'ldap', mfa_active: false} as UserProfile);
|
||||
expect(result).toEqual('LDAP');
|
||||
});
|
||||
|
||||
it('should return auth service in title case if it is not LDAP or SAML', () => {
|
||||
const result = getUserAuthenticationTextField(intl, true, {auth_service: 'oauth', mfa_active: false} as UserProfile);
|
||||
expect(result).toEqual('Oauth');
|
||||
});
|
||||
|
||||
it('should include MFA if user has MFA enabled', () => {
|
||||
const result = getUserAuthenticationTextField(intl, true, {auth_service: 'oauth', mfa_active: true} as UserProfile);
|
||||
expect(result).toEqual('Oauth, MFA');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Overlay} from 'react-bootstrap';
|
||||
import type {WrappedComponentProps} from 'react-intl';
|
||||
import React, {PureComponent} from 'react';
|
||||
import type {ChangeEvent, MouseEvent} from 'react';
|
||||
import type {IntlShape, WrappedComponentProps} from 'react-intl';
|
||||
import {FormattedMessage, defineMessage, injectIntl} from 'react-intl';
|
||||
import {Redirect} from 'react-router-dom';
|
||||
import type {RouteComponentProps} from 'react-router-dom';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
@@ -15,9 +14,6 @@ import type {UserProfile} from '@mattermost/types/users';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {adminResetMfa, adminResetEmail} from 'actions/admin_actions.jsx';
|
||||
|
||||
import AdminButtonOutline from 'components/admin_console/admin_button_outline/admin_button_outline';
|
||||
import AdminUserCard from 'components/admin_console/admin_user_card/admin_user_card';
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
|
||||
@@ -26,343 +22,269 @@ import ConfirmModal from 'components/confirm_modal';
|
||||
import FormError from 'components/form_error';
|
||||
import SaveButton from 'components/save_button';
|
||||
import TeamSelectorModal from 'components/team_selector_modal';
|
||||
import Tooltip from 'components/tooltip';
|
||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
import AdminPanel from 'components/widgets/admin_console/admin_panel';
|
||||
import AtIcon from 'components/widgets/icons/at_icon';
|
||||
import EmailIcon from 'components/widgets/icons/email_icon';
|
||||
import SheidOutlineIcon from 'components/widgets/icons/shield_outline_icon';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {toTitleCase} from 'utils/utils';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
import './system_user_detail.scss';
|
||||
|
||||
export type Props = {
|
||||
user: UserProfile;
|
||||
mfaEnabled: boolean;
|
||||
isDisabled?: boolean;
|
||||
actions: {
|
||||
updateUserActive: (userId: string, active: boolean) => Promise<ActionResult>;
|
||||
setNavigationBlocked: (blocked: boolean) => void;
|
||||
addUserToTeam: (teamId: string, userId: string) => Promise<unknown>;
|
||||
};
|
||||
} & WrappedComponentProps;
|
||||
export type Params = {
|
||||
user_id?: UserProfile['id'];
|
||||
};
|
||||
|
||||
export type Props = PropsFromRedux & RouteComponentProps<Params> & WrappedComponentProps;
|
||||
|
||||
export type State = {
|
||||
user?: UserProfile;
|
||||
emailField: string;
|
||||
isLoading: boolean;
|
||||
error?: string | null;
|
||||
isSaveNeeded: boolean;
|
||||
isSaving: boolean;
|
||||
teams: TeamMembership[];
|
||||
teamIds: Array<Team['id']>;
|
||||
loading: boolean;
|
||||
searching: boolean;
|
||||
showPasswordModal: boolean;
|
||||
showDeactivateMemberModal: boolean;
|
||||
saveNeeded: boolean;
|
||||
saving: boolean;
|
||||
serverError: string | null;
|
||||
errorTooltip: boolean;
|
||||
customComponentWrapperClass: string;
|
||||
user: UserProfile;
|
||||
addTeamOpen: boolean;
|
||||
refreshTeams: boolean;
|
||||
error: ServerError | null;
|
||||
}
|
||||
showResetPasswordModal: boolean;
|
||||
showDeactivateMemberModal: boolean;
|
||||
showTeamSelectorModal: boolean;
|
||||
};
|
||||
|
||||
class SystemUserDetail extends React.PureComponent<Props & RouteComponentProps, State> {
|
||||
errorMessageRef: React.RefObject<HTMLDivElement>;
|
||||
errorMessageRefCurrent: React.ReactInstance | undefined;
|
||||
|
||||
public static defaultProps = {
|
||||
user: {
|
||||
email: '',
|
||||
} as UserProfile,
|
||||
mfaEnabled: false,
|
||||
};
|
||||
|
||||
constructor(props: Props & RouteComponentProps) {
|
||||
export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
emailField: '',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isSaveNeeded: false,
|
||||
isSaving: false,
|
||||
teams: [],
|
||||
teamIds: [],
|
||||
loading: false,
|
||||
searching: false,
|
||||
showPasswordModal: false,
|
||||
showDeactivateMemberModal: false,
|
||||
saveNeeded: false,
|
||||
saving: false,
|
||||
serverError: null,
|
||||
errorTooltip: false,
|
||||
customComponentWrapperClass: '',
|
||||
user: {
|
||||
email: this.props.user.email,
|
||||
} as UserProfile,
|
||||
addTeamOpen: false,
|
||||
refreshTeams: true,
|
||||
error: null,
|
||||
showResetPasswordModal: false,
|
||||
showDeactivateMemberModal: false,
|
||||
showTeamSelectorModal: false,
|
||||
};
|
||||
|
||||
this.errorMessageRef = React.createRef();
|
||||
}
|
||||
|
||||
setTeamsData = (teams: TeamMembership[]): void => {
|
||||
getUser = async (userId: UserProfile['id']) => {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
try {
|
||||
const {data, error} = await this.props.getUser(userId) as ActionResult<UserProfile, ServerError>;
|
||||
if (data) {
|
||||
this.setState({
|
||||
user: data,
|
||||
emailField: data.email, // Set emailField to the email of the user for editing purposes
|
||||
isLoading: false,
|
||||
});
|
||||
} else {
|
||||
throw new Error(error ? error.message : 'Unknown error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('SystemUserDetails-getUser: ', error); // eslint-disable-line no-console
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
error: this.props.intl.formatMessage({id: 'admin.user_item.userNotFound', defaultMessage: 'Cannot load User'}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const userId = this.props.match.params.user_id ?? '';
|
||||
if (userId) {
|
||||
// We dont have to handle the case of userId being empty here because the redirect will take care of it from the parent components
|
||||
this.getUser(userId);
|
||||
}
|
||||
}
|
||||
|
||||
handleTeamsLoaded = (teams: TeamMembership[]) => {
|
||||
const teamIds = teams.map((team) => team.team_id);
|
||||
this.setState({teams});
|
||||
this.setState({teamIds});
|
||||
this.setState({refreshTeams: false});
|
||||
};
|
||||
|
||||
openAddTeam = (): void => {
|
||||
this.setState({addTeamOpen: true});
|
||||
};
|
||||
handleAddUserToTeams = (teams: Team[]) => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
addTeams = (teams: Team[]): void => {
|
||||
const promises = [];
|
||||
for (const team of teams) {
|
||||
promises.push(this.props.actions.addUserToTeam(team.id, this.props.user.id));
|
||||
promises.push(this.props.addUserToTeam(team.id, this.state.user.id));
|
||||
}
|
||||
Promise.all(promises).finally(() => this.setState({refreshTeams: true}));
|
||||
Promise.all(promises).finally(() =>
|
||||
this.setState({refreshTeams: true}),
|
||||
);
|
||||
};
|
||||
|
||||
closeAddTeam = (): void => {
|
||||
this.setState({addTeamOpen: false});
|
||||
handleActivateUser = async () => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {error} = await this.props.updateUserActive(this.state.user.id, true) as ActionResult<boolean, ServerError>;
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
await this.getUser(this.state.user.id);
|
||||
} catch (err) {
|
||||
console.error('SystemUserDetails-handleActivateUser', err); // eslint-disable-line no-console
|
||||
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.userActivateFailed', defaultMessage: 'Failed to activate user'})});
|
||||
}
|
||||
};
|
||||
|
||||
doPasswordReset = (user: UserProfile): void => {
|
||||
handleDeactivateMember = async () => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {error} = await this.props.updateUserActive(this.state.user.id, false) as ActionResult<boolean, ServerError>;
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
await this.getUser(this.state.user.id);
|
||||
} catch (err) {
|
||||
console.error('SystemUserDetails-handleDeactivateMember', err); // eslint-disable-line no-console
|
||||
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.userDeactivateFailed', defaultMessage: 'Failed to deactivate user'})});
|
||||
}
|
||||
|
||||
this.toggleCloseModalDeactivateMember();
|
||||
};
|
||||
|
||||
handleRemoveMFA = async () => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {error} = await this.props.updateUserMfa(this.state.user.id, false) as ActionResult<boolean, ServerError>;
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
await this.getUser(this.state.user.id);
|
||||
} catch (err) {
|
||||
console.error('SystemUserDetails-handleRemoveMFA', err); // eslint-disable-line no-console
|
||||
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.userMFARemoveFailed', defaultMessage: 'Failed to remove user\'s MFA'})});
|
||||
}
|
||||
};
|
||||
|
||||
handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {target: {value}} = event;
|
||||
|
||||
const didEmailChanged = value !== this.state.user.email;
|
||||
this.setState({
|
||||
showPasswordModal: true,
|
||||
user,
|
||||
emailField: value,
|
||||
isSaveNeeded: didEmailChanged,
|
||||
});
|
||||
|
||||
this.props.setNavigationBlocked(didEmailChanged);
|
||||
};
|
||||
|
||||
doPasswordResetDismiss = (): void => {
|
||||
handleSubmit = async (event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.isLoading || this.state.isSaving || !this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.user.email === this.state.emailField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmail(this.state.user.email)) {
|
||||
this.setState({error: this.props.intl.formatMessage({id: 'admin.user_item.invalidEmail', defaultMessage: 'Invalid email address'})});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUser = Object.assign({}, this.state.user, {email: this.state.emailField.trim().toLowerCase()});
|
||||
|
||||
this.setState({
|
||||
showPasswordModal: false,
|
||||
error: null,
|
||||
isSaving: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const {data, error} = await this.props.patchUser(updatedUser) as ActionResult<UserProfile, ServerError>;
|
||||
if (data) {
|
||||
this.setState({
|
||||
user: data,
|
||||
emailField: data.email,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
isSaveNeeded: false,
|
||||
});
|
||||
} else {
|
||||
throw new Error(error ? error.message : 'Unknown error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SystemUserDetails-handleSubmit', err); // eslint-disable-line no-console
|
||||
|
||||
this.setState({
|
||||
error: this.props.intl.formatMessage({id: 'admin.user_item.userUpdateFailed', defaultMessage: 'Failed to update user'}),
|
||||
isSaving: false,
|
||||
isSaveNeeded: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.props.setNavigationBlocked(false);
|
||||
};
|
||||
|
||||
doPasswordResetSubmit = (): void => {
|
||||
this.setState({
|
||||
showPasswordModal: false,
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Modal close/open handlers
|
||||
*/
|
||||
|
||||
handleMakeActive = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
this.props.actions.updateUserActive(this.props.user.id, true).
|
||||
then((data) => this.onUpdateActiveResult(data.error));
|
||||
};
|
||||
|
||||
handleShowDeactivateMemberModal = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
toggleOpenModalDeactivateMember = () => {
|
||||
this.setState({showDeactivateMemberModal: true});
|
||||
};
|
||||
|
||||
handleDeactivateMember = (): void => {
|
||||
this.props.actions.updateUserActive(this.props.user.id, false).
|
||||
then((data) => this.onUpdateActiveResult(data.error));
|
||||
toggleCloseModalDeactivateMember = () => {
|
||||
this.setState({showDeactivateMemberModal: false});
|
||||
};
|
||||
|
||||
onUpdateActiveResult = (error: ServerError): void => {
|
||||
if (error) {
|
||||
this.setState({error});
|
||||
}
|
||||
};
|
||||
|
||||
handleDeactivateCancel = (): void => {
|
||||
this.setState({showDeactivateMemberModal: false});
|
||||
};
|
||||
|
||||
// TODO: add error handler function
|
||||
handleResetMfa = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
adminResetMfa(this.props.user.id, null, null);
|
||||
};
|
||||
|
||||
handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const emailChanged = e.target.value !== this.props.user.email;
|
||||
this.setState({
|
||||
user: {
|
||||
email: e.target.value,
|
||||
} as UserProfile,
|
||||
saveNeeded: emailChanged,
|
||||
toggleOpenModalResetPassword = () => {
|
||||
this.props.openModal({
|
||||
modalId: ModalIdentifiers.RESET_PASSWORD_MODAL,
|
||||
dialogType: ResetPasswordModal,
|
||||
dialogProps: {user: this.state.user},
|
||||
});
|
||||
this.props.actions.setNavigationBlocked(emailChanged);
|
||||
};
|
||||
|
||||
handleSubmit = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
if (this.state.user.email !== this.props.user.email) {
|
||||
if (!isEmail(this.state.user.email)) {
|
||||
this.setState({serverError: 'Invalid Email address'});
|
||||
return;
|
||||
}
|
||||
const user = Object.assign({}, this.props.user);
|
||||
const email = this.state.user.email.trim().toLowerCase();
|
||||
user.email = email;
|
||||
|
||||
this.setState({serverError: null});
|
||||
|
||||
adminResetEmail(
|
||||
user,
|
||||
() => {
|
||||
this.props.history.push('/admin_console/user_management/users');
|
||||
},
|
||||
(err: Error) => {
|
||||
const serverError = (err.message ? err.message : err) as string;
|
||||
this.setState({serverError});
|
||||
},
|
||||
);
|
||||
|
||||
this.setState({
|
||||
saving: false,
|
||||
saveNeeded: false,
|
||||
serverError: null,
|
||||
});
|
||||
this.props.actions.setNavigationBlocked(false);
|
||||
}
|
||||
toggleCloseModalResetPassword = () => {
|
||||
this.setState({showResetPasswordModal: false});
|
||||
};
|
||||
|
||||
renderDeactivateMemberModal = (user: UserProfile): React.ReactNode => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.title'
|
||||
defaultMessage='Deactivate {username}'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
let warning;
|
||||
if (user.auth_service !== '' && user.auth_service !== Constants.EMAIL_SERVICE) {
|
||||
warning = (
|
||||
<strong>
|
||||
<br/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.sso_warning'
|
||||
defaultMessage='You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.'
|
||||
/>
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
const message = (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc'
|
||||
defaultMessage='This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system. Are you sure you want to deactivate {username}?'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
|
||||
const confirmButtonClass = 'btn btn-danger';
|
||||
const deactivateMemberButton = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.deactivate'
|
||||
defaultMessage='Deactivate'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
show={this.state.showDeactivateMemberModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass={confirmButtonClass}
|
||||
confirmButtonText={deactivateMemberButton}
|
||||
onConfirm={this.handleDeactivateMember}
|
||||
onCancel={this.handleDeactivateCancel}
|
||||
/>
|
||||
);
|
||||
toggleOpenTeamSelectorModal = () => {
|
||||
this.setState({showTeamSelectorModal: true});
|
||||
};
|
||||
|
||||
renderActivateDeactivate = (): React.ReactNode => {
|
||||
if (this.props.user.delete_at > 0) {
|
||||
return (
|
||||
<AdminButtonOutline
|
||||
onClick={this.handleMakeActive}
|
||||
className='admin-btn-default'
|
||||
disabled={this.props.isDisabled}
|
||||
>
|
||||
{this.props.intl.formatMessage({id: 'admin.user_item.makeActive', defaultMessage: 'Activate'})}
|
||||
</AdminButtonOutline>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AdminButtonOutline
|
||||
onClick={this.handleShowDeactivateMemberModal}
|
||||
className='admin-btn-default'
|
||||
disabled={this.props.isDisabled}
|
||||
>
|
||||
{this.props.intl.formatMessage({id: 'admin.user_item.makeInactive', defaultMessage: 'Deactivate'})}
|
||||
</AdminButtonOutline>
|
||||
);
|
||||
toggleCloseTeamSelectorModal = () => {
|
||||
this.setState({showTeamSelectorModal: false});
|
||||
};
|
||||
|
||||
renderRemoveMFA = (): React.ReactNode => {
|
||||
if (this.props.user.mfa_active) {
|
||||
return (
|
||||
<AdminButtonOutline
|
||||
onClick={this.handleResetMfa}
|
||||
className='admin-btn-default'
|
||||
disabled={this.props.isDisabled}
|
||||
>
|
||||
{this.props.intl.formatMessage({id: 'admin.user_item.resetMfa', defaultMessage: 'Remove MFA'})}
|
||||
</AdminButtonOutline>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
getAuthenticationText(): string {
|
||||
const {user, mfaEnabled} = this.props;
|
||||
let authLine;
|
||||
|
||||
if (user.auth_service) {
|
||||
let service;
|
||||
if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
|
||||
service = user.auth_service.toUpperCase();
|
||||
} else {
|
||||
service = Utils.toTitleCase(user.auth_service);
|
||||
}
|
||||
authLine = service;
|
||||
} else {
|
||||
authLine = this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.email', defaultMessage: 'Email'});
|
||||
}
|
||||
if (mfaEnabled) {
|
||||
if (user.mfa_active) {
|
||||
authLine += ', ';
|
||||
authLine += this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.mfa', defaultMessage: 'MFA'});
|
||||
}
|
||||
}
|
||||
return authLine;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (this.errorMessageRef.current) {
|
||||
this.errorMessageRefCurrent = this.errorMessageRef.current;
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const {user} = this.props;
|
||||
let deactivateMemberModal;
|
||||
|
||||
if (!user.id) {
|
||||
return (
|
||||
<Redirect to={{pathname: '/admin_console/user_management/users'}}/>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.id) {
|
||||
deactivateMemberModal = this.renderDeactivateMemberModal(user);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='SystemUserDetail wrapper--fixed'>
|
||||
<AdminHeader withBackButton={true}>
|
||||
@@ -379,58 +301,105 @@ class SystemUserDetail extends React.PureComponent<Props & RouteComponentProps,
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper'>
|
||||
<div className='admin-console__content'>
|
||||
|
||||
{/* User details */}
|
||||
<AdminUserCard
|
||||
user={user}
|
||||
user={this.state.user}
|
||||
isLoading={this.state.isLoading}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<span className='SystemUserDetail__position'>{user.position}</span>
|
||||
<span className='SystemUserDetail__field-label'>{this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.email', defaultMessage: 'Email'})}</span>
|
||||
<div>
|
||||
<EmailIcon className='SystemUserDetail__field-icon'/>
|
||||
<input
|
||||
className='SystemUserDetail__input form-control'
|
||||
type='text'
|
||||
value={this.state.user.email}
|
||||
onChange={this.handleEmailChange}
|
||||
disabled={this.props.isDisabled}
|
||||
<>
|
||||
<span>{this.state?.user?.position ?? ''}</span>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.email'
|
||||
defaultMessage='Email'
|
||||
/>
|
||||
</div>
|
||||
<span className='SystemUserDetail__field-label'>{this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.username', defaultMessage: 'Username'})}</span>
|
||||
<div>
|
||||
<AtIcon className='SystemUserDetail__field-icon'/>
|
||||
<span className='SystemUserDetail__field-text'>{user.username}</span>
|
||||
</div>
|
||||
<span className='SystemUserDetail__field-label'>{this.props.intl.formatMessage({id: 'admin.userManagement.userDetail.authenticationMethod', defaultMessage: 'Authentication Method'})}</span>
|
||||
<div className='SystemUserDetail__field-text'>
|
||||
<SheidOutlineIcon className='SystemUserDetail__field-icon'/>
|
||||
<span className='SystemUserDetail__field-text'>{this.getAuthenticationText()}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<EmailIcon/>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
value={this.state.emailField}
|
||||
onChange={this.handleEmailChange}
|
||||
disabled={this.state.error !== null || this.state.isSaving}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.username'
|
||||
defaultMessage='Username'
|
||||
/>
|
||||
<AtIcon/>
|
||||
<span>{this.state?.user?.username}</span>
|
||||
</label>
|
||||
<label>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.authenticationMethod'
|
||||
defaultMessage='Authentication Method'
|
||||
/>
|
||||
<SheidOutlineIcon/>
|
||||
<span>{getUserAuthenticationTextField(this.props.intl, this.props.mfaEnabled, this.state.user)}</span>
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<React.Fragment>
|
||||
<AdminButtonOutline
|
||||
onClick={this.doPasswordReset}
|
||||
className='admin-btn-default'
|
||||
disabled={this.props.isDisabled}
|
||||
<>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this.toggleOpenModalResetPassword}
|
||||
>
|
||||
{this.props.intl.formatMessage({id: 'admin.user_item.resetPwd', defaultMessage: 'Reset Password'})}
|
||||
</AdminButtonOutline>
|
||||
{this.renderActivateDeactivate()}
|
||||
{this.renderRemoveMFA()}
|
||||
</React.Fragment>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.resetPwd'
|
||||
defaultMessage='Reset Password'
|
||||
/>
|
||||
</button>
|
||||
{this.state.user?.mfa_active && (
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this.handleRemoveMFA}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.resetMfa'
|
||||
defaultMessage='Remove MFA'
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{this.state.user?.delete_at !== 0 && (
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this.handleActivateUser}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.makeActive'
|
||||
defaultMessage='Activate'
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{this.state.user?.delete_at === 0 && (
|
||||
<button
|
||||
className='btn btn-secondary btn-danger'
|
||||
onClick={this.toggleOpenModalDeactivateMember}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.deactivate'
|
||||
defaultMessage='Deactivate'
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* User's team details */}
|
||||
<AdminPanel
|
||||
subtitle={defineMessage({id: 'admin.userManagement.userDetail.teamsSubtitle', defaultMessage: 'Teams to which this user belongs'})}
|
||||
title={defineMessage({id: 'admin.userManagement.userDetail.teamsTitle', defaultMessage: 'Team Membership'})}
|
||||
button={(
|
||||
subtitle={defineMessage({id: 'admin.userManagement.userDetail.teamsSubtitle', defaultMessage: 'Teams to which this user belongs'})}
|
||||
button={
|
||||
<div className='add-team-button'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.openAddTeam}
|
||||
disabled={this.props.isDisabled}
|
||||
onClick={this.toggleOpenTeamSelectorModal}
|
||||
disabled={this.state.isLoading || this.state.error !== null}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.userManagement.userDetail.addTeam'
|
||||
@@ -438,58 +407,121 @@ class SystemUserDetail extends React.PureComponent<Props & RouteComponentProps,
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<TeamList
|
||||
userId={this.props.user.id}
|
||||
userDetailCallback={this.setTeamsData}
|
||||
refreshTeams={this.state.refreshTeams}
|
||||
readOnly={this.props.isDisabled}
|
||||
/>
|
||||
{this.state.isLoading && (
|
||||
<div className='teamlistLoading'>
|
||||
<LoadingSpinner/>
|
||||
</div>
|
||||
)}
|
||||
{!this.state.isLoading && this.state.user?.id && (
|
||||
<TeamList
|
||||
userId={this.state.user.id}
|
||||
userDetailCallback={this.handleTeamsLoaded}
|
||||
refreshTeams={this.state.refreshTeams}
|
||||
/>
|
||||
)}
|
||||
</AdminPanel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='admin-console-save'>
|
||||
<SaveButton
|
||||
saving={this.state.saving}
|
||||
disabled={!this.state.saveNeeded}
|
||||
saving={this.state.isSaving}
|
||||
disabled={!this.state.isSaveNeeded || this.state.isLoading || this.state.error !== null || this.state.isSaving}
|
||||
onClick={this.handleSubmit}
|
||||
savingMessage={this.props.intl.formatMessage({id: 'admin.saving', defaultMessage: 'Saving Config...'})}
|
||||
/>
|
||||
<div
|
||||
className='error-message'
|
||||
ref={this.errorMessageRef}
|
||||
>
|
||||
<FormError error={this.state.serverError}/>
|
||||
<div className='error-message'>
|
||||
<FormError error={this.state.error}/>
|
||||
</div>
|
||||
<Overlay
|
||||
show={this.state.errorTooltip}
|
||||
placement='top'
|
||||
target={this.errorMessageRefCurrent}
|
||||
>
|
||||
<Tooltip id='error-tooltip' >
|
||||
{this.state.serverError}
|
||||
</Tooltip>
|
||||
</Overlay>
|
||||
</div>
|
||||
<ResetPasswordModal
|
||||
user={user}
|
||||
show={this.state.showPasswordModal}
|
||||
onModalSubmit={this.doPasswordResetSubmit}
|
||||
onModalDismissed={this.doPasswordResetDismiss}
|
||||
{/* mounting of Modals */}
|
||||
<ConfirmModal
|
||||
show={this.state.showDeactivateMemberModal}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.title'
|
||||
defaultMessage='Deactivate {username}'
|
||||
values={{
|
||||
username: this.state.user?.username ?? '',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc'
|
||||
defaultMessage='This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system. Are you sure you want to deactivate {username}?'
|
||||
values={{
|
||||
username: this.state.user?.username ?? '',
|
||||
}}
|
||||
/>
|
||||
{this.state.user?.auth_service !== '' && this.state.user?.auth_service !== Constants.EMAIL_SERVICE && (
|
||||
<strong>
|
||||
<br/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.sso_warning'
|
||||
defaultMessage='You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.'
|
||||
/>
|
||||
</strong>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.deactivate'
|
||||
defaultMessage='Deactivate'
|
||||
/>
|
||||
}
|
||||
onConfirm={this.handleDeactivateMember}
|
||||
onCancel={this.toggleCloseModalDeactivateMember}
|
||||
/>
|
||||
{deactivateMemberModal}
|
||||
{this.state.addTeamOpen &&
|
||||
{this.state.showTeamSelectorModal && (
|
||||
<TeamSelectorModal
|
||||
onModalDismissed={this.closeAddTeam}
|
||||
onTeamsSelected={this.addTeams}
|
||||
onModalDismissed={this.toggleCloseTeamSelectorModal}
|
||||
onTeamsSelected={this.handleAddUserToTeams}
|
||||
alreadySelected={this.state.teamIds}
|
||||
excludeGroupConstrained={true}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(SystemUserDetail);
|
||||
|
||||
export function getUserAuthenticationTextField(intl: IntlShape, mfaEnabled: Props['mfaEnabled'], user?: UserProfile): string {
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let authenticationTextField;
|
||||
|
||||
if (user.auth_service) {
|
||||
let service;
|
||||
if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
|
||||
service = user.auth_service.toUpperCase();
|
||||
} else {
|
||||
service = toTitleCase(user.auth_service);
|
||||
}
|
||||
authenticationTextField = service;
|
||||
} else {
|
||||
authenticationTextField = intl.formatMessage({
|
||||
id: 'admin.userManagement.userDetail.email',
|
||||
defaultMessage: 'Email',
|
||||
});
|
||||
}
|
||||
|
||||
if (mfaEnabled) {
|
||||
if (user.mfa_active) {
|
||||
authenticationTextField += ', ';
|
||||
authenticationTextField += intl.formatMessage({id: 'admin.userManagement.userDetail.mfa', defaultMessage: 'MFA'});
|
||||
}
|
||||
}
|
||||
|
||||
return authenticationTextField;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,19 @@ export default class TeamList extends React.PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
private renderRow = (item: TeamWithMembership): JSX.Element => {
|
||||
return (
|
||||
<TeamRow
|
||||
key={item.id}
|
||||
team={item}
|
||||
doRemoveUserFromTeam={this.doRemoveUserFromTeam}
|
||||
doMakeUserTeamAdmin={this.doMakeUserTeamAdmin}
|
||||
doMakeUserTeamMember={this.doMakeUserTeamMember}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
@@ -167,17 +180,4 @@ export default class TeamList extends React.PureComponent<Props, State> {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private renderRow = (item: TeamWithMembership): JSX.Element => {
|
||||
return (
|
||||
<TeamRow
|
||||
key={item.id}
|
||||
team={item}
|
||||
doRemoveUserFromTeam={this.doRemoveUserFromTeam}
|
||||
doMakeUserTeamAdmin={this.doMakeUserTeamAdmin}
|
||||
doMakeUserTeamMember={this.doMakeUserTeamMember}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/admin_console/system_users should match default snapshot 1`] = `
|
||||
<div
|
||||
className="wrapper--fixed"
|
||||
>
|
||||
<AdminHeader>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{siteName} Users"
|
||||
id="admin.system_users.title"
|
||||
values={
|
||||
Object {
|
||||
"siteName": "Site name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<RevokeSessionsButton />
|
||||
</AdminHeader>
|
||||
<div
|
||||
className="admin-console__wrapper ignore-marking"
|
||||
>
|
||||
<div
|
||||
className="admin-console__content"
|
||||
>
|
||||
<div
|
||||
className="more-modal__list member-list-holder"
|
||||
>
|
||||
<div
|
||||
className="system-users__filter-row"
|
||||
>
|
||||
<SystemUsersSearch
|
||||
onChange={[Function]}
|
||||
onSearch={[Function]}
|
||||
value=""
|
||||
/>
|
||||
<SystemUsersFilterTeam
|
||||
onChange={[Function]}
|
||||
onFilter={[Function]}
|
||||
options={Array []}
|
||||
value=""
|
||||
/>
|
||||
<SystemUsersFilterRole
|
||||
onChange={[Function]}
|
||||
onFilter={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<Connect(SystemUsersList)
|
||||
enableUserAccessTokens={false}
|
||||
experimentalEnableAuthenticationTransfer={false}
|
||||
filter=""
|
||||
loading={true}
|
||||
mfaEnabled={false}
|
||||
nextPage={[Function]}
|
||||
onTermChange={[Function]}
|
||||
search={[Function]}
|
||||
teamId=""
|
||||
teams={Array []}
|
||||
term=""
|
||||
total={0}
|
||||
usersPerPage={50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export enum ColumnNames {
|
||||
username = 'usernameColumn',
|
||||
displayName = 'displayNameColumn',
|
||||
email = 'emailColumn',
|
||||
createAt = 'createAtColumn',
|
||||
lastLoginAt = 'lastLoginColumn',
|
||||
lastStatusAt = 'lastStatusAtColumn',
|
||||
lastPostDate = 'lastPostDateColumn',
|
||||
daysActive = 'daysActiveColumn',
|
||||
totalPosts = 'totalPostsColumn',
|
||||
actions = 'actionsColumn',
|
||||
}
|
||||
|
||||
export enum StatusFilter {
|
||||
Any = 'any',
|
||||
Active = 'active',
|
||||
Deactivated = 'deactivated',
|
||||
}
|
||||
|
||||
export enum RoleFilters {
|
||||
Any = 'any',
|
||||
Admin = 'system_admin',
|
||||
Member = 'system_user',
|
||||
Guest = 'system_guest',
|
||||
}
|
||||
|
||||
export enum TeamFilters {
|
||||
AllTeams = 'teams_filter_for_all_teams',
|
||||
NoTeams = 'teams_filter_for_no_teams',
|
||||
}
|
||||
@@ -1,30 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {getTeams, getTeamStats} from 'mattermost-redux/actions/teams';
|
||||
import {
|
||||
getUser,
|
||||
getUserAccessToken,
|
||||
getProfiles,
|
||||
searchProfiles,
|
||||
getFilteredUsersStats,
|
||||
} from 'mattermost-redux/actions/users';
|
||||
import {ReportDuration} from '@mattermost/types/reports';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getFilteredUsersStats as selectFilteredUserStats, getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||
import {get as getPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {loadProfilesAndTeamMembers, loadProfilesWithoutTeam} from 'actions/user_actions';
|
||||
import {setSystemUsersSearch} from 'actions/views/search';
|
||||
|
||||
import {SearchUserTeamFilter} from 'utils/constants';
|
||||
import {getUserCountForReporting, getUserReports, setAdminConsoleUsersManagementTableProperties} from 'actions/views/admin';
|
||||
import {adminConsoleUserManagementTablePropertiesInitialState} from 'reducers/views/admin';
|
||||
import {getAdminConsoleUserManagementTableProperties} from 'selectors/views/admin';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import {RoleFilters, StatusFilter, TeamFilters} from './constants';
|
||||
import SystemUsers from './system_users';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
@@ -34,57 +28,61 @@ function mapStateToProps(state: GlobalState) {
|
||||
const mfaEnabled = config.EnableMultifactorAuthentication === 'true';
|
||||
const enableUserAccessTokens = config.EnableUserAccessTokens === 'true';
|
||||
const experimentalEnableAuthenticationTransfer = config.ExperimentalEnableAuthenticationTransfer === 'true';
|
||||
const isMySql = config.SQLDriverName === 'mysql';
|
||||
const hideMySqlNotification = getPreferences(state, Preferences.CATEGORY_REPORTING, Preferences.HIDE_MYSQL_STATS_NOTIFICATION, '') === 'true';
|
||||
|
||||
const search = state.views.search.systemUsersSearch;
|
||||
let totalUsers = 0;
|
||||
let searchTerm = '';
|
||||
let teamId = '';
|
||||
let filter = '';
|
||||
if (search) {
|
||||
searchTerm = search.term || '';
|
||||
teamId = search.team || '';
|
||||
filter = search.filter || '';
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
if (!teamId || teamId === SearchUserTeamFilter.ALL_USERS) {
|
||||
totalUsers = selectFilteredUserStats(state)?.total_users_count || 0;
|
||||
} else if (teamId === SearchUserTeamFilter.NO_TEAM) {
|
||||
totalUsers = 0;
|
||||
} else {
|
||||
const stats = state.entities.teams.stats[teamId] || {total_member_count: 0};
|
||||
totalUsers = stats.total_member_count;
|
||||
}
|
||||
}
|
||||
const tableProperties = getAdminConsoleUserManagementTableProperties(state);
|
||||
const tablePropertySortColumn = tableProperties?.sortColumn ?? adminConsoleUserManagementTablePropertiesInitialState.sortColumn;
|
||||
const tablePropertySortIsDescending = tableProperties?.sortIsDescending ?? adminConsoleUserManagementTablePropertiesInitialState.sortIsDescending;
|
||||
const tablePropertyPageSize = tableProperties?.pageSize ?? adminConsoleUserManagementTablePropertiesInitialState.pageSize;
|
||||
const tablePropertyPageIndex = tableProperties?.pageIndex ?? adminConsoleUserManagementTablePropertiesInitialState.pageIndex;
|
||||
const tablePropertyCursorDirection = tableProperties?.cursorDirection ?? adminConsoleUserManagementTablePropertiesInitialState.cursorDirection;
|
||||
const tablePropertyCursorUserId = tableProperties?.cursorUserId ?? adminConsoleUserManagementTablePropertiesInitialState.cursorUserId;
|
||||
const tablePropertyCursorColumnValue = tableProperties?.cursorColumnValue ?? adminConsoleUserManagementTablePropertiesInitialState.cursorColumnValue;
|
||||
const tablePropertyColumnVisibility = tableProperties?.columnVisibility ?? adminConsoleUserManagementTablePropertiesInitialState.columnVisibility;
|
||||
const tablePropertySearchTerm = tableProperties?.searchTerm ?? adminConsoleUserManagementTablePropertiesInitialState.searchTerm;
|
||||
const tablePropertyFilterTeam = tableProperties?.filterTeam ?? TeamFilters.AllTeams;
|
||||
const tablePropertyFilterTeamLabel = tableProperties?.filterTeamLabel ?? '';
|
||||
const tablePropertyFilterRole = tableProperties?.filterRole ?? RoleFilters.Any;
|
||||
const tablePropertyFilterStatus = tableProperties?.filterStatus ?? StatusFilter.Any;
|
||||
const tablePropertyDateRange = tableProperties?.dateRange ?? ReportDuration.AllTime;
|
||||
|
||||
return {
|
||||
teams: getTeamsList(state),
|
||||
siteName,
|
||||
mfaEnabled,
|
||||
totalUsers,
|
||||
searchTerm,
|
||||
teamId,
|
||||
filter,
|
||||
enableUserAccessTokens,
|
||||
users: getUsers(state),
|
||||
experimentalEnableAuthenticationTransfer,
|
||||
currentUser,
|
||||
isMySql,
|
||||
hideMySqlNotification,
|
||||
tablePropertySortColumn,
|
||||
tablePropertySortIsDescending,
|
||||
tablePropertyPageSize,
|
||||
tablePropertyPageIndex,
|
||||
tablePropertyCursorDirection,
|
||||
tablePropertyCursorUserId,
|
||||
tablePropertyCursorColumnValue,
|
||||
tablePropertyColumnVisibility,
|
||||
tablePropertySearchTerm,
|
||||
tablePropertyFilterTeam,
|
||||
tablePropertyFilterTeamLabel,
|
||||
tablePropertyFilterRole,
|
||||
tablePropertyFilterStatus,
|
||||
tablePropertyDateRange,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getTeams,
|
||||
getTeamStats,
|
||||
getUser,
|
||||
getUserAccessToken,
|
||||
loadProfilesAndTeamMembers,
|
||||
setSystemUsersSearch,
|
||||
loadProfilesWithoutTeam,
|
||||
getProfiles,
|
||||
searchProfiles,
|
||||
logError,
|
||||
getFilteredUsersStats,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
getUserReports,
|
||||
getUserCountForReporting,
|
||||
savePreferences,
|
||||
setAdminConsoleUsersManagementTableProperties,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SystemUsers);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {emitUserLoggedOutEvent} from 'actions/global_actions';
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
|
||||
function RevokeSessionsButton() {
|
||||
export function RevokeSessionsButton() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -70,5 +70,3 @@ function RevokeSessionsButton() {
|
||||
</SystemPermissionGate>
|
||||
);
|
||||
}
|
||||
|
||||
export default RevokeSessionsButton;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
@mixin textElipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.systemUsersTable {
|
||||
thead {
|
||||
tr {
|
||||
th.actionsColumn {
|
||||
padding-inline-start: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td.usernameColumn > div {
|
||||
$profilePictureSize: 40px;
|
||||
|
||||
display: grid;
|
||||
width: max-content;
|
||||
gap: 0 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"profilePictureContainer displayName"
|
||||
"profilePictureContainer userName"
|
||||
"profilePictureContainer error";
|
||||
grid-template-columns: calc($profilePictureSize + 16px) 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
.profilePictureContainer {
|
||||
width: $profilePictureSize;
|
||||
height: $profilePictureSize;
|
||||
align-self: center;
|
||||
grid-area: profilePictureContainer;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.displayName {
|
||||
align-self: start;
|
||||
color: var(--sys-denim-button-bg);
|
||||
font-weight: 600;
|
||||
grid-area: displayName;
|
||||
|
||||
@include textElipsis;
|
||||
}
|
||||
|
||||
.userName {
|
||||
align-self: start;
|
||||
grid-area: userName;
|
||||
|
||||
@include textElipsis;
|
||||
}
|
||||
|
||||
.error {
|
||||
align-self: start;
|
||||
grid-area: error;
|
||||
|
||||
@include textElipsis;
|
||||
}
|
||||
}
|
||||
|
||||
td.actionsColumn > button {
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.systemUsers__mySqlAlertBanner {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.systemUsers__mySqlAlertBanner-buttons {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import {Constants, SearchUserTeamFilter, UserFilters} from 'utils/constants';
|
||||
|
||||
import SystemUsers from './system_users';
|
||||
import type {SystemUsers as SystemUserClass} from './system_users';
|
||||
|
||||
jest.mock('actions/admin_actions');
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('components/admin_console/system_users', () => {
|
||||
const USERS_PER_PAGE = 50;
|
||||
const defaultProps = {
|
||||
teams: [],
|
||||
siteName: 'Site name',
|
||||
mfaEnabled: false,
|
||||
enableUserAccessTokens: false,
|
||||
experimentalEnableAuthenticationTransfer: false,
|
||||
searchTerm: '',
|
||||
teamId: '',
|
||||
filter: '',
|
||||
totalUsers: 0,
|
||||
users: {},
|
||||
actions: {
|
||||
getTeams: jest.fn().mockResolvedValue({data: []}),
|
||||
getTeamStats: jest.fn().mockResolvedValue({data: []}),
|
||||
getUser: jest.fn().mockResolvedValue({data: {}}),
|
||||
getUserAccessToken: jest.fn().mockResolvedValue({data: ''}),
|
||||
loadProfilesAndTeamMembers: jest.fn().mockResolvedValue({data: true}),
|
||||
setSystemUsersSearch: jest.fn().mockResolvedValue({data: true}),
|
||||
loadProfilesWithoutTeam: jest.fn().mockResolvedValue({data: true}),
|
||||
getProfiles: jest.fn().mockResolvedValue({data: []}),
|
||||
searchProfiles: jest.fn().mockResolvedValue({data: []}),
|
||||
logError: jest.fn(),
|
||||
getFilteredUsersStats: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
test('should match default snapshot', () => {
|
||||
const props = defaultProps;
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('loadDataForTeam() should have called getProfiles', async () => {
|
||||
const getProfiles = jest.fn().mockResolvedValue(undefined);
|
||||
const props = {...defaultProps, actions: {...defaultProps.actions, getProfiles}};
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
|
||||
wrapper.setState({loading: true});
|
||||
|
||||
const instance = wrapper.instance() as SystemUserClass;
|
||||
|
||||
await instance.loadDataForTeam(SearchUserTeamFilter.ALL_USERS, '');
|
||||
|
||||
expect(getProfiles).toHaveBeenCalled();
|
||||
expect(getProfiles).toHaveBeenCalledWith(0, Constants.PROFILE_CHUNK_SIZE, {});
|
||||
expect(wrapper.state('loading')).toEqual(false);
|
||||
});
|
||||
|
||||
test('loadDataForTeam() should have called loadProfilesWithoutTeam', async () => {
|
||||
const loadProfilesWithoutTeam = jest.fn().mockResolvedValue(undefined);
|
||||
const props = {...defaultProps, actions: {...defaultProps.actions, loadProfilesWithoutTeam}};
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
|
||||
wrapper.setState({loading: true});
|
||||
|
||||
const instance = wrapper.instance() as SystemUserClass;
|
||||
|
||||
await instance.loadDataForTeam(SearchUserTeamFilter.NO_TEAM, '');
|
||||
|
||||
expect(loadProfilesWithoutTeam).toHaveBeenCalled();
|
||||
expect(loadProfilesWithoutTeam).toHaveBeenCalledWith(0, Constants.PROFILE_CHUNK_SIZE, {});
|
||||
expect(wrapper.state('loading')).toEqual(false);
|
||||
|
||||
await instance.loadDataForTeam(SearchUserTeamFilter.NO_TEAM, UserFilters.INACTIVE);
|
||||
|
||||
expect(loadProfilesWithoutTeam).toHaveBeenCalled();
|
||||
expect(loadProfilesWithoutTeam).toHaveBeenCalledWith(0, Constants.PROFILE_CHUNK_SIZE, {inactive: true});
|
||||
});
|
||||
|
||||
test('nextPage() should have called getProfiles', async () => {
|
||||
const getProfiles = jest.fn().mockResolvedValue(undefined);
|
||||
const props = {
|
||||
...defaultProps,
|
||||
teamId: SearchUserTeamFilter.ALL_USERS,
|
||||
actions: {...defaultProps.actions, getProfiles},
|
||||
};
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
|
||||
wrapper.setState({loading: true});
|
||||
|
||||
const instance = wrapper.instance() as SystemUserClass;
|
||||
|
||||
await instance.nextPage(0);
|
||||
|
||||
expect(getProfiles).toHaveBeenCalled();
|
||||
expect(getProfiles).toHaveBeenCalledWith(1, USERS_PER_PAGE, {});
|
||||
expect(wrapper.state('loading')).toEqual(false);
|
||||
});
|
||||
|
||||
test('nextPage() should have called loadProfilesWithoutTeam', async () => {
|
||||
const loadProfilesWithoutTeam = jest.fn().mockResolvedValue({data: true});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
teamId: SearchUserTeamFilter.NO_TEAM,
|
||||
actions: {...defaultProps.actions, loadProfilesWithoutTeam},
|
||||
};
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
|
||||
wrapper.setState({loading: true});
|
||||
|
||||
const instance = wrapper.instance() as SystemUserClass;
|
||||
|
||||
await instance.nextPage(0);
|
||||
|
||||
expect(loadProfilesWithoutTeam).toHaveBeenCalled();
|
||||
expect(loadProfilesWithoutTeam).toHaveBeenCalledWith(1, USERS_PER_PAGE, {});
|
||||
expect(wrapper.state('loading')).toEqual(false);
|
||||
});
|
||||
|
||||
test('doSearch() should have called searchProfiles with allow_inactive', async () => {
|
||||
const searchProfiles = jest.fn().mockResolvedValue({data: [{}]});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
teamId: SearchUserTeamFilter.NO_TEAM,
|
||||
actions: {...defaultProps.actions, searchProfiles},
|
||||
};
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
|
||||
const instance = wrapper.instance() as SystemUserClass;
|
||||
|
||||
await instance.doSearch('searchterm', '', '');
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(searchProfiles).toHaveBeenCalled();
|
||||
expect(searchProfiles).toHaveBeenCalledWith('searchterm', {allow_inactive: true});
|
||||
});
|
||||
|
||||
test('doSearch() should have called searchProfiles with allow_inactive and system_admin role', async () => {
|
||||
const searchProfiles = jest.fn().mockResolvedValue({data: [{}]});
|
||||
const props = {
|
||||
...defaultProps,
|
||||
teamId: SearchUserTeamFilter.NO_TEAM,
|
||||
actions: {...defaultProps.actions, searchProfiles},
|
||||
};
|
||||
const wrapper = shallow(<SystemUsers {...props}/>);
|
||||
|
||||
const instance = wrapper.instance() as SystemUserClass;
|
||||
|
||||
await instance.doSearch('searchterm', '', 'system_admin');
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(searchProfiles).toHaveBeenCalled();
|
||||
expect(searchProfiles).toHaveBeenCalledWith('searchterm', {allow_inactive: true, role: 'system_admin'});
|
||||
});
|
||||
});
|
||||
@@ -1,107 +1,61 @@
|
||||
// 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 type {PaginationState, SortingState, VisibilityState, ColumnDef, CellContext, OnChangeFn} from '@tanstack/react-table';
|
||||
import {useReactTable, getCoreRowModel, getSortedRowModel} from '@tanstack/react-table';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl, FormattedMessage, defineMessages} from 'react-intl';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {GetFilteredUsersStatsOpts, UserAccessToken, UserProfile, UsersStats} from '@mattermost/types/users';
|
||||
import {CursorPaginationDirection} from '@mattermost/types/reports';
|
||||
import type {ReportDuration, UserReport} from '@mattermost/types/reports';
|
||||
|
||||
import {debounce} from 'mattermost-redux/actions/helpers';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {AdminConsoleListTable, ElapsedDurationCell, PAGE_SIZES, LoadingStates} from 'components/admin_console/list_table';
|
||||
import type {TableMeta} from 'components/admin_console/list_table';
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
|
||||
import {Constants, UserSearchOptions, SearchUserTeamFilter} from 'utils/constants';
|
||||
import {getUserOptionsFromFilter, searchUserOptionsFromFilter} from 'utils/filter_users';
|
||||
import {getDisplayName, imageURLForUser} from 'utils/utils';
|
||||
|
||||
import RevokeSessionsButton from './revoke_sessions_button';
|
||||
import SystemUsersFilterRole from './system_users_filter_role';
|
||||
import SystemUsersFilterTeam from './system_users_filter_team';
|
||||
import SystemUsersList from './system_users_list';
|
||||
import SystemUsersSearch from './system_users_search';
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
const USER_ID_LENGTH = 26;
|
||||
const USERS_PER_PAGE = 50;
|
||||
import {ColumnNames} from './constants';
|
||||
import {RevokeSessionsButton} from './revoke_sessions_button';
|
||||
import {SystemUsersColumnTogglerMenu} from './system_users_column_toggler_menu';
|
||||
import {SystemUsersDateRangeMenu} from './system_users_date_range_menu';
|
||||
import {SystemUsersExport} from './system_users_export';
|
||||
import {SystemUsersFilterPopover} from './system_users_filters_popover';
|
||||
import {SystemUsersListAction} from './system_users_list_actions';
|
||||
import {SystemUsersSearch} from './system_users_search';
|
||||
import {getSortableColumnValueBySortColumn, getPaginationInfo, convertTableOptionsToUserReportOptions} from './utils';
|
||||
|
||||
type Props = {
|
||||
import './system_users.scss';
|
||||
|
||||
/**
|
||||
* Array of team objects
|
||||
*/
|
||||
teams: Team[];
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
/**
|
||||
* Title of the app or site.
|
||||
*/
|
||||
siteName?: string;
|
||||
type Props = PropsFromRedux;
|
||||
|
||||
/**
|
||||
* Whether or not MFA is licensed and enabled.
|
||||
*/
|
||||
mfaEnabled: boolean;
|
||||
export type TableOptions = {
|
||||
pageSize?: PaginationState['pageSize'];
|
||||
sortColumn?: SortingState[0]['id'];
|
||||
sortIsDescending?: SortingState[0]['desc'];
|
||||
fromColumnValue?: AdminConsoleUserManagementTableProperties['cursorColumnValue'];
|
||||
fromId?: AdminConsoleUserManagementTableProperties['cursorUserId'];
|
||||
direction?: CursorPaginationDirection;
|
||||
searchTerm?: string;
|
||||
filterTeam?: AdminConsoleUserManagementTableProperties['filterTeam'];
|
||||
filterRole?: AdminConsoleUserManagementTableProperties['filterRole'];
|
||||
filterStatus?: AdminConsoleUserManagementTableProperties['filterStatus'];
|
||||
dateRange?: ReportDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not user access tokens are enabled.
|
||||
*/
|
||||
enableUserAccessTokens: boolean;
|
||||
type UserReportWithError = UserReport & {error?: ServerError};
|
||||
|
||||
/**
|
||||
* Whether or not the experimental authentication transfer is enabled.
|
||||
*/
|
||||
experimentalEnableAuthenticationTransfer: boolean;
|
||||
totalUsers: number;
|
||||
searchTerm: string;
|
||||
teamId: string;
|
||||
filter: string;
|
||||
users: Record<string, UserProfile>;
|
||||
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Function to get teams
|
||||
*/
|
||||
getTeams: (startInde: number, endIndex: number) => void;
|
||||
|
||||
/**
|
||||
* Function to get statistics for a team
|
||||
*/
|
||||
getTeamStats: (teamId: string) => Promise<ActionResult>;
|
||||
|
||||
/**
|
||||
* Function to get a user
|
||||
*/
|
||||
getUser: (id: string) => Promise<ActionResult>;
|
||||
|
||||
/**
|
||||
* Function to get a user access token
|
||||
*/
|
||||
getUserAccessToken: (tokenId: string) => Promise<ActionResult<UserAccessToken>>;
|
||||
loadProfilesAndTeamMembers: (page: number, maxItemsPerPage: number, teamId: string, options: Record<string, string | boolean>) => void;
|
||||
loadProfilesWithoutTeam: (page: number, maxItemsPerPage: number, options: Record<string, string | boolean>) => void;
|
||||
getProfiles: (page: number, maxItemsPerPage: number, options: Record<string, string | boolean>) => void;
|
||||
setSystemUsersSearch: (searchTerm: string, teamId: string, filter: string) => void;
|
||||
searchProfiles: (term: string, options?: any) => Promise<ActionResult<UserProfile[]>>;
|
||||
|
||||
/**
|
||||
* Function to log errors
|
||||
*/
|
||||
logError: (error: {type: string; message: string}) => void;
|
||||
getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{
|
||||
data?: UsersStats;
|
||||
error?: ServerError;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
searching: boolean;
|
||||
term?: string;
|
||||
};
|
||||
const tableId = 'systemUsersTable';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {id: 'admin.system_users.title', defaultMessage: '{siteName} Users'},
|
||||
@@ -109,254 +63,496 @@ const messages = defineMessages({
|
||||
|
||||
export const searchableStrings: Array<string|MessageDescriptor|[MessageDescriptor, {[key: string]: any}]> = [[messages.title, {siteName: ''}]];
|
||||
|
||||
export class SystemUsers extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
function SystemUsers(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
searching: false,
|
||||
};
|
||||
}
|
||||
const [userReports, setUserReports] = useState<UserReportWithError[]>([]);
|
||||
const [userCount, setUserCount] = useState<number | undefined>();
|
||||
const [loadingState, setLoadingState] = useState<LoadingStates>(LoadingStates.Loading);
|
||||
const [showMySqlBanner, setShowMySqlBanner] = useState(props.isMySql && !props.hideMySqlNotification);
|
||||
|
||||
componentDidMount() {
|
||||
this.loadDataForTeam(this.props.teamId, this.props.filter);
|
||||
this.props.actions.getTeams(0, 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.actions.setSystemUsersSearch('', '', '');
|
||||
}
|
||||
|
||||
loadDataForTeam = async (teamId: string, filter: string | undefined) => {
|
||||
const {
|
||||
getProfiles,
|
||||
loadProfilesWithoutTeam,
|
||||
loadProfilesAndTeamMembers,
|
||||
getTeamStats,
|
||||
getFilteredUsersStats,
|
||||
} = this.props.actions;
|
||||
|
||||
if (this.props.searchTerm) {
|
||||
this.doSearch(this.props.searchTerm, teamId, filter);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = getUserOptionsFromFilter(filter);
|
||||
|
||||
if (teamId === SearchUserTeamFilter.ALL_USERS) {
|
||||
await Promise.all([
|
||||
getProfiles(0, Constants.PROFILE_CHUNK_SIZE, options),
|
||||
getFilteredUsersStats({include_bots: false, include_deleted: true}),
|
||||
]);
|
||||
} else if (teamId === SearchUserTeamFilter.NO_TEAM) {
|
||||
await loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, options);
|
||||
} else {
|
||||
await Promise.all([
|
||||
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, teamId, options),
|
||||
getTeamStats(teamId),
|
||||
]);
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
handleTeamChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const teamId = e.target.value;
|
||||
this.loadDataForTeam(teamId, this.props.filter);
|
||||
this.props.actions.setSystemUsersSearch(this.props.searchTerm, teamId, this.props.filter);
|
||||
};
|
||||
|
||||
handleFilterChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const filter = e.target.value;
|
||||
this.loadDataForTeam(this.props.teamId, filter);
|
||||
this.props.actions.setSystemUsersSearch(this.props.searchTerm, this.props.teamId, filter);
|
||||
};
|
||||
|
||||
handleTermChange = (term: string) => {
|
||||
this.props.actions.setSystemUsersSearch(term, this.props.teamId, this.props.filter);
|
||||
};
|
||||
|
||||
handleSearchFiltersChange = ({searchTerm, teamId, filter}: {searchTerm?: string; teamId?: string; filter?: string}) => {
|
||||
const changedSearchTerm = typeof searchTerm === 'undefined' ? this.props.searchTerm : searchTerm;
|
||||
const changedTeamId = typeof teamId === 'undefined' ? this.props.teamId : teamId;
|
||||
const changedFilter = typeof filter === 'undefined' ? this.props.filter : filter;
|
||||
|
||||
this.props.actions.setSystemUsersSearch(changedSearchTerm, changedTeamId, changedFilter);
|
||||
};
|
||||
|
||||
nextPage = async (page: number) => {
|
||||
const {teamId, filter} = this.props;
|
||||
|
||||
// Paging isn't supported while searching
|
||||
const {
|
||||
getProfiles,
|
||||
loadProfilesWithoutTeam,
|
||||
loadProfilesAndTeamMembers,
|
||||
} = this.props.actions;
|
||||
|
||||
const options = getUserOptionsFromFilter(filter);
|
||||
|
||||
if (teamId === SearchUserTeamFilter.ALL_USERS) {
|
||||
await getProfiles(page + 1, USERS_PER_PAGE, options);
|
||||
} else if (teamId === SearchUserTeamFilter.NO_TEAM) {
|
||||
await loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, options);
|
||||
} else {
|
||||
await loadProfilesAndTeamMembers(page + 1, USERS_PER_PAGE, teamId, options);
|
||||
}
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
onSearch = async (term: string) => {
|
||||
this.setState({loading: true});
|
||||
|
||||
const options = {
|
||||
...searchUserOptionsFromFilter(this.props.filter),
|
||||
...this.props.teamId && {team_id: this.props.teamId},
|
||||
...this.props.teamId === SearchUserTeamFilter.NO_TEAM && {
|
||||
[UserSearchOptions.WITHOUT_TEAM]: true,
|
||||
},
|
||||
allow_inactive: true,
|
||||
// Effect to get the total user count
|
||||
useEffect(() => {
|
||||
const getUserCount = async (tableOptions?: TableOptions) => {
|
||||
const {data} = await props.getUserCountForReporting(convertTableOptionsToUserReportOptions(tableOptions));
|
||||
setUserCount(data);
|
||||
};
|
||||
|
||||
const {data: profiles} = await this.props.actions.searchProfiles(term, options);
|
||||
if (profiles!.length === 0 && term.length === USER_ID_LENGTH) {
|
||||
await this.getUserByTokenOrId(term);
|
||||
}
|
||||
getUserCount({
|
||||
pageSize: props.tablePropertyPageSize,
|
||||
sortColumn: props.tablePropertySortColumn,
|
||||
sortIsDescending: props.tablePropertySortIsDescending,
|
||||
fromColumnValue: props.tablePropertyCursorColumnValue,
|
||||
fromId: props.tablePropertyCursorUserId,
|
||||
direction: props.tablePropertyCursorDirection,
|
||||
searchTerm: props.tablePropertySearchTerm,
|
||||
filterTeam: props.tablePropertyFilterTeam,
|
||||
filterRole: props.tablePropertyFilterRole,
|
||||
filterStatus: props.tablePropertyFilterStatus,
|
||||
});
|
||||
}, [
|
||||
props.tablePropertyPageSize,
|
||||
props.tablePropertySortColumn,
|
||||
props.tablePropertySortIsDescending,
|
||||
props.tablePropertyCursorDirection,
|
||||
props.tablePropertyCursorColumnValue,
|
||||
props.tablePropertyCursorUserId,
|
||||
props.tablePropertySearchTerm,
|
||||
props.tablePropertyFilterTeam,
|
||||
props.tablePropertyFilterRole,
|
||||
props.tablePropertyFilterStatus,
|
||||
]);
|
||||
|
||||
this.setState({loading: false});
|
||||
};
|
||||
// Effect to get the user reports
|
||||
useEffect(() => {
|
||||
async function fetchUserReportsWithOptions(tableOptions?: TableOptions) {
|
||||
setLoadingState(LoadingStates.Loading);
|
||||
|
||||
onFilter = async ({teamId, filter}: {teamId?: string; filter?: string}) => {
|
||||
if (this.props.searchTerm) {
|
||||
this.onSearch(this.props.searchTerm);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
const newTeamId = typeof teamId === 'undefined' ? this.props.teamId : teamId;
|
||||
const newFilter = typeof filter === 'undefined' ? this.props.filter : filter;
|
||||
|
||||
const options = getUserOptionsFromFilter(newFilter);
|
||||
|
||||
if (newTeamId === SearchUserTeamFilter.ALL_USERS) {
|
||||
await Promise.all([
|
||||
this.props.actions.getProfiles(0, Constants.PROFILE_CHUNK_SIZE, options),
|
||||
this.props.actions.getFilteredUsersStats({include_bots: false, include_deleted: true}),
|
||||
]);
|
||||
} else if (newTeamId === SearchUserTeamFilter.NO_TEAM) {
|
||||
await this.props.actions.loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, options);
|
||||
} else {
|
||||
await Promise.all([
|
||||
this.props.actions.loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, newTeamId, options),
|
||||
this.props.actions.getTeamStats(newTeamId),
|
||||
]);
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
doSearch = debounce(async (term, teamId = this.props.teamId, filter = this.props.filter) => {
|
||||
if (!term) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
const options = {
|
||||
...searchUserOptionsFromFilter(filter),
|
||||
...teamId && {team_id: teamId},
|
||||
...teamId === SearchUserTeamFilter.NO_TEAM && {
|
||||
[UserSearchOptions.WITHOUT_TEAM]: true,
|
||||
},
|
||||
allow_inactive: true,
|
||||
};
|
||||
|
||||
const {data: profiles} = await this.props.actions.searchProfiles(term, options);
|
||||
if (profiles!.length === 0 && term.length === USER_ID_LENGTH) {
|
||||
await this.getUserByTokenOrId(term);
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
}, Constants.SEARCH_TIMEOUT_MILLISECONDS, false, () => {});
|
||||
|
||||
getUserById = async (id: string) => {
|
||||
if (this.props.users[id]) {
|
||||
this.setState({loading: false});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.props.actions.getUser(id);
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
getUserByTokenOrId = async (id: string) => {
|
||||
if (this.props.enableUserAccessTokens) {
|
||||
const {data} = await this.props.actions.getUserAccessToken(id);
|
||||
const {data} = await props.getUserReports(convertTableOptionsToUserReportOptions(tableOptions));
|
||||
|
||||
if (data) {
|
||||
this.setState({term: data.user_id});
|
||||
this.getUserById(data.user_id);
|
||||
return;
|
||||
if (data.length > 0) {
|
||||
setUserReports(data);
|
||||
} else {
|
||||
setUserReports([]);
|
||||
}
|
||||
setLoadingState(LoadingStates.Loaded);
|
||||
} else {
|
||||
setLoadingState(LoadingStates.Failed);
|
||||
}
|
||||
}
|
||||
|
||||
this.getUserById(id);
|
||||
};
|
||||
fetchUserReportsWithOptions({
|
||||
pageSize: props.tablePropertyPageSize,
|
||||
sortColumn: props.tablePropertySortColumn,
|
||||
sortIsDescending: props.tablePropertySortIsDescending,
|
||||
fromColumnValue: props.tablePropertyCursorColumnValue,
|
||||
fromId: props.tablePropertyCursorUserId,
|
||||
direction: props.tablePropertyCursorDirection,
|
||||
searchTerm: props.tablePropertySearchTerm,
|
||||
filterTeam: props.tablePropertyFilterTeam,
|
||||
filterRole: props.tablePropertyFilterRole,
|
||||
filterStatus: props.tablePropertyFilterStatus,
|
||||
dateRange: props.tablePropertyDateRange,
|
||||
});
|
||||
}, [
|
||||
props.tablePropertyPageSize,
|
||||
props.tablePropertySortColumn,
|
||||
props.tablePropertySortIsDescending,
|
||||
props.tablePropertyCursorDirection,
|
||||
props.tablePropertyCursorColumnValue,
|
||||
props.tablePropertyCursorUserId,
|
||||
props.tablePropertySearchTerm,
|
||||
props.tablePropertyFilterRole,
|
||||
props.tablePropertyFilterTeam,
|
||||
props.tablePropertyFilterStatus,
|
||||
props.tablePropertyDateRange,
|
||||
]);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<AdminHeader>
|
||||
<FormattedMessage
|
||||
{...messages.title}
|
||||
values={{siteName: this.props.siteName}}
|
||||
/>
|
||||
<RevokeSessionsButton/>
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper ignore-marking'>
|
||||
<div className='admin-console__content'>
|
||||
<div className='more-modal__list member-list-holder'>
|
||||
<div className='system-users__filter-row'>
|
||||
<SystemUsersSearch
|
||||
value={this.props.searchTerm}
|
||||
onChange={this.handleSearchFiltersChange}
|
||||
onSearch={this.onSearch}
|
||||
/>
|
||||
<SystemUsersFilterTeam
|
||||
options={this.props.teams}
|
||||
value={this.props.teamId}
|
||||
onChange={this.handleSearchFiltersChange}
|
||||
onFilter={this.onFilter}
|
||||
/>
|
||||
<SystemUsersFilterRole
|
||||
value={this.props.filter}
|
||||
onChange={this.handleSearchFiltersChange}
|
||||
onFilter={this.onFilter}
|
||||
function handleDismissMySqlNotice() {
|
||||
setShowMySqlBanner(false);
|
||||
props.savePreferences(props.currentUser.id, [{
|
||||
category: Preferences.CATEGORY_REPORTING,
|
||||
name: Preferences.HIDE_MYSQL_STATS_NOTIFICATION,
|
||||
user_id: props.currentUser.id,
|
||||
value: 'true',
|
||||
}]);
|
||||
}
|
||||
|
||||
// Handlers for table actions
|
||||
|
||||
function handleRowClick(userId: UserReport['id']) {
|
||||
if (userId.length !== 0) {
|
||||
history.push(`/admin_console/user_management/user/${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreviousPageClick() {
|
||||
if (!userReports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.setAdminConsoleUsersManagementTableProperties({
|
||||
pageIndex: props.tablePropertyPageIndex - 1,
|
||||
cursorDirection: CursorPaginationDirection.prev,
|
||||
cursorUserId: userReports[0].id,
|
||||
cursorColumnValue: getSortableColumnValueBySortColumn(userReports[0], props.tablePropertySortColumn),
|
||||
});
|
||||
}
|
||||
|
||||
function handleNextPageClick() {
|
||||
if (!userReports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.setAdminConsoleUsersManagementTableProperties({
|
||||
pageIndex: props.tablePropertyPageIndex + 1,
|
||||
cursorDirection: CursorPaginationDirection.next,
|
||||
cursorUserId: userReports[userReports.length - 1].id,
|
||||
cursorColumnValue: getSortableColumnValueBySortColumn(userReports[userReports.length - 1], props.tablePropertySortColumn),
|
||||
});
|
||||
}
|
||||
|
||||
function handleSortingChange(updateFn: (currentSortingState: SortingState) => SortingState) {
|
||||
const currentSortingState = [{id: props.tablePropertySortColumn, desc: props.tablePropertySortIsDescending}];
|
||||
const [updatedSortingState] = updateFn(currentSortingState);
|
||||
|
||||
if (props.tablePropertySortColumn !== updatedSortingState.id) {
|
||||
// If we are clicking on a new column, we want to sort in descending order
|
||||
updatedSortingState.desc = false;
|
||||
}
|
||||
|
||||
props.setAdminConsoleUsersManagementTableProperties({
|
||||
pageIndex: 0,
|
||||
cursorDirection: undefined, // reset the cursor to the beginning on any filter change
|
||||
cursorUserId: undefined,
|
||||
cursorColumnValue: undefined,
|
||||
sortColumn: updatedSortingState.id,
|
||||
sortIsDescending: updatedSortingState.desc,
|
||||
});
|
||||
}
|
||||
|
||||
function handlePaginationChange(updateFn: (currentPaginationState: PaginationState) => PaginationState) {
|
||||
const currentPaginationState = {pageIndex: 0, pageSize: props.tablePropertyPageSize};
|
||||
const updatedPaginationState = updateFn(currentPaginationState);
|
||||
|
||||
props.setAdminConsoleUsersManagementTableProperties({
|
||||
pageIndex: 0,
|
||||
cursorDirection: undefined, // reset the cursor to the beginning on any filter change
|
||||
cursorUserId: undefined,
|
||||
cursorColumnValue: undefined,
|
||||
pageSize: updatedPaginationState.pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
function handleColumnVisibilityChange(updateFn: (currentVisibilityState: VisibilityState) => VisibilityState) {
|
||||
const updatedVisibilityState = updateFn(props.tablePropertyColumnVisibility);
|
||||
|
||||
props.setAdminConsoleUsersManagementTableProperties({
|
||||
columnVisibility: Object.assign({}, props.tablePropertyColumnVisibility, updatedVisibilityState),
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserReport(userId: string, updatedReport: Partial<UserReportWithError>) {
|
||||
setUserReports(userReports.map((user) => {
|
||||
if (user.id === userId) {
|
||||
return {
|
||||
...user,
|
||||
error: undefined,
|
||||
...updatedReport,
|
||||
};
|
||||
}
|
||||
|
||||
return user;
|
||||
}));
|
||||
}
|
||||
|
||||
const columns: Array<ColumnDef<UserReport, any>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: ColumnNames.username,
|
||||
accessorKey: 'username',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.userDetails',
|
||||
defaultMessage: 'User details',
|
||||
}),
|
||||
cell: (info: CellContext<UserReportWithError, null>) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='profilePictureContainer'>
|
||||
<img
|
||||
className='profilePicture'
|
||||
src={imageURLForUser(info.row.original.id)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
<SystemUsersList
|
||||
loading={this.state.loading}
|
||||
search={this.doSearch}
|
||||
nextPage={this.nextPage}
|
||||
usersPerPage={USERS_PER_PAGE}
|
||||
total={this.props.totalUsers}
|
||||
teams={this.props.teams}
|
||||
teamId={this.props.teamId}
|
||||
filter={this.props.filter}
|
||||
term={this.props.searchTerm}
|
||||
onTermChange={this.handleTermChange}
|
||||
mfaEnabled={this.props.mfaEnabled}
|
||||
enableUserAccessTokens={this.props.enableUserAccessTokens}
|
||||
experimentalEnableAuthenticationTransfer={this.props.experimentalEnableAuthenticationTransfer}
|
||||
/>
|
||||
<div
|
||||
className='displayName'
|
||||
title={getDisplayName(info.row.original)}
|
||||
>
|
||||
{getDisplayName(info.row.original) || ''}
|
||||
</div>
|
||||
<div
|
||||
className='userName'
|
||||
title={info.row.original.username}
|
||||
>
|
||||
{info.row.original.username}
|
||||
</div>
|
||||
{info.row.original.error &&
|
||||
<div
|
||||
className='error'
|
||||
title={info.row.original.error.message}
|
||||
>
|
||||
{info.row.original.error.message}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableHiding: false,
|
||||
enablePinning: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.email,
|
||||
accessorKey: 'email',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.email',
|
||||
defaultMessage: 'Email',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, string>) => info.getValue() || '',
|
||||
enableHiding: true,
|
||||
enablePinning: false,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.createAt,
|
||||
accessorKey: 'create_at',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.memberSince',
|
||||
defaultMessage: 'Member since',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, number>) => <ElapsedDurationCell date={info.getValue()}/>,
|
||||
enableHiding: true,
|
||||
enablePinning: false,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.lastLoginAt,
|
||||
accessorKey: 'last_login_at',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.lastLoginAt',
|
||||
defaultMessage: 'Last login',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, number | undefined>) => <ElapsedDurationCell date={info.getValue()}/>,
|
||||
enableHiding: true,
|
||||
enablePinning: false,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.lastStatusAt,
|
||||
accessorKey: 'last_status_at',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.lastActivity',
|
||||
defaultMessage: 'Last activity',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, number | undefined>) => <ElapsedDurationCell date={info.getValue()}/>,
|
||||
enableHiding: true,
|
||||
enablePinning: false,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.lastPostDate,
|
||||
accessorKey: 'last_post_date',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.lastPost',
|
||||
defaultMessage: 'Last post',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, number | undefined>) => <ElapsedDurationCell date={info.getValue()}/>,
|
||||
enableHiding: !props.isMySql,
|
||||
enablePinning: false,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.daysActive,
|
||||
accessorKey: 'days_active',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.daysActive',
|
||||
defaultMessage: 'Days active',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, number | undefined>) => info.getValue() || null,
|
||||
meta: {
|
||||
isNumeric: true,
|
||||
},
|
||||
enableHiding: !props.isMySql,
|
||||
enablePinning: false,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.totalPosts,
|
||||
accessorKey: 'total_posts',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.totalPosts',
|
||||
defaultMessage: 'Messages posted',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, number | undefined>) => info.getValue() || null,
|
||||
meta: {
|
||||
isNumeric: true,
|
||||
},
|
||||
enableHiding: !props.isMySql,
|
||||
enablePinning: false,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: ColumnNames.actions,
|
||||
accessorKey: 'actions',
|
||||
header: formatMessage({
|
||||
id: 'admin.system_users.list.actions',
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
cell: (info: CellContext<UserReport, null>) => (
|
||||
<SystemUsersListAction
|
||||
rowIndex={info.cell.row.index}
|
||||
tableId={tableId}
|
||||
user={info.row.original}
|
||||
currentUser={props.currentUser}
|
||||
updateUser={(updatedUser) => updateUserReport(info.row.original.id, updatedUser)}
|
||||
onError={(error) => updateUserReport(info.row.original.id, {error})}
|
||||
/>
|
||||
),
|
||||
enableHiding: false,
|
||||
enablePinning: true,
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[props.currentUser, userReports],
|
||||
);
|
||||
|
||||
// Table state which are correctly formatted for the table component
|
||||
|
||||
const sortingTableState = [{
|
||||
id: props && props.tablePropertySortColumn && props.tablePropertySortColumn.length > 0 ? props.tablePropertySortColumn : ColumnNames.username,
|
||||
desc: props?.tablePropertySortIsDescending ?? false,
|
||||
}];
|
||||
|
||||
const paginationTableState = {
|
||||
pageIndex: props?.tablePropertyPageIndex ?? 0,
|
||||
pageSize: props?.tablePropertyPageSize || PAGE_SIZES[0],
|
||||
};
|
||||
|
||||
const columnVisibility = {
|
||||
...props.tablePropertyColumnVisibility,
|
||||
...(props.isMySql ? {
|
||||
[ColumnNames.lastPostDate]: false,
|
||||
[ColumnNames.daysActive]: false,
|
||||
[ColumnNames.totalPosts]: false,
|
||||
} : {}),
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: userReports,
|
||||
columns,
|
||||
state: {
|
||||
sorting: sortingTableState,
|
||||
pagination: paginationTableState,
|
||||
columnVisibility,
|
||||
},
|
||||
meta: {
|
||||
tableId: 'systemUsersTable',
|
||||
tableCaption: formatMessage({id: 'admin.system_users.list.caption', defaultMessage: 'System Users'}),
|
||||
loadingState,
|
||||
disablePrevPage: !props.tablePropertyCursorUserId || props.tablePropertyPageIndex <= 0 || (props.tablePropertyCursorDirection === 'prev' && userReports.length < paginationTableState.pageSize),
|
||||
disableNextPage: props.tablePropertyCursorDirection === 'next' && userReports.length < paginationTableState.pageSize,
|
||||
onRowClick: handleRowClick,
|
||||
onPreviousPageClick: handlePreviousPageClick,
|
||||
onNextPageClick: handleNextPageClick,
|
||||
paginationInfo: getPaginationInfo(paginationTableState.pageIndex, paginationTableState.pageSize, userReports.length, userCount),
|
||||
hasDualSidedPagination: true,
|
||||
} as TableMeta,
|
||||
getCoreRowModel: getCoreRowModel<UserReport>(),
|
||||
getSortedRowModel: getSortedRowModel<UserReport>(),
|
||||
onPaginationChange: handlePaginationChange as OnChangeFn<PaginationState>,
|
||||
onSortingChange: handleSortingChange as OnChangeFn<SortingState>,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange as OnChangeFn<VisibilityState>,
|
||||
manualSorting: true,
|
||||
enableSortingRemoval: false,
|
||||
enableMultiSort: false,
|
||||
manualFiltering: true,
|
||||
manualPagination: true,
|
||||
renderFallbackValue: '',
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className='wrapper--fixed'
|
||||
data-testid='systemUsersSection'
|
||||
>
|
||||
<AdminHeader>
|
||||
<FormattedMessage
|
||||
{...messages.title}
|
||||
values={{siteName: props.siteName}}
|
||||
>
|
||||
{(formatMessageChunk) => (
|
||||
<span id='systemUsersTable-headerId'>{formatMessageChunk}</span>
|
||||
)}
|
||||
</FormattedMessage>
|
||||
<RevokeSessionsButton/>
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper'>
|
||||
{showMySqlBanner &&
|
||||
<AlertBanner
|
||||
className='systemUsers__mySqlAlertBanner'
|
||||
mode='warning'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.mysql_stats.title'
|
||||
defaultMessage='Some statistics are unavailable for servers using MySQL'
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.mysql_stats.desc'
|
||||
defaultMessage='Use of MySQL may limit the availability of some statistics features. We recommend transitioning from MySQL to PostgreSQL to fully leverage improved performance and comprehensive analytics. While you’re still using MySQL, please use the export functionality to view all user statistics.'
|
||||
/>
|
||||
<div className='systemUsers__mySqlAlertBanner-buttons'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={() => window.open('https://mattermost.com/pl/user-stats-learn-more', '_blank')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.mysql_stats.learn_more'
|
||||
defaultMessage='Learn more'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-tertiary'
|
||||
onClick={handleDismissMySqlNotice}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.mysql_stats.dismiss'
|
||||
defaultMessage='Dismiss'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onDismiss={handleDismissMySqlNotice}
|
||||
/>
|
||||
}
|
||||
<div className='admin-console__container ignore-marking'>
|
||||
<div className='admin-console__filters-rows'>
|
||||
<SystemUsersSearch
|
||||
searchTerm={props.tablePropertySearchTerm}
|
||||
/>
|
||||
<SystemUsersFilterPopover
|
||||
filterTeam={props.tablePropertyFilterTeam}
|
||||
filterTeamLabel={props.tablePropertyFilterTeamLabel}
|
||||
filterRole={props.tablePropertyFilterRole}
|
||||
filterStatus={props.tablePropertyFilterStatus}
|
||||
/>
|
||||
<SystemUsersColumnTogglerMenu
|
||||
isMySql={props.isMySql}
|
||||
allColumns={table.getAllLeafColumns()}
|
||||
visibleColumnsLength={table.getVisibleLeafColumns()?.length ?? 0}
|
||||
/>
|
||||
<SystemUsersDateRangeMenu
|
||||
dateRange={props.tablePropertyDateRange}
|
||||
/>
|
||||
<SystemUsersExport
|
||||
currentUserId={props.currentUser.id}
|
||||
dateRange={props.tablePropertyDateRange}
|
||||
/>
|
||||
</div>
|
||||
<AdminConsoleListTable<UserReport>
|
||||
table={table}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemUsers;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Column, CoreColumn} from '@tanstack/react-table';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {UserReport} from '@mattermost/types/reports';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {ColumnNames} from '../constants';
|
||||
|
||||
import './system_users_column_toggler.scss';
|
||||
|
||||
interface Props {
|
||||
isMySql: boolean;
|
||||
allColumns: Array<Column<UserReport>>;
|
||||
visibleColumnsLength: number;
|
||||
}
|
||||
|
||||
export function SystemUsersColumnTogglerMenu(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
function getColumnName(columnId: CoreColumn<UserReport, unknown>['id']) {
|
||||
switch (columnId) {
|
||||
case ColumnNames.username:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.userDetails'
|
||||
defaultMessage='User details'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.email:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.email'
|
||||
defaultMessage='Email'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.createAt:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.memberSince'
|
||||
defaultMessage='Member since'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.lastLoginAt:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.lastLoginAt'
|
||||
defaultMessage='Last login'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.lastStatusAt:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.lastActivity'
|
||||
defaultMessage='Last activity'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.lastPostDate:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.lastPost'
|
||||
defaultMessage='Last post'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.daysActive:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.daysActive'
|
||||
defaultMessage='Days active'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.totalPosts:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.totalPosts'
|
||||
defaultMessage='Messages posted'
|
||||
/>
|
||||
);
|
||||
case ColumnNames.actions:
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions'
|
||||
defaultMessage='Actions'
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <span/>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='systemUsersColumnToggler'>
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: 'systemUsersColumnTogglerMenuButton',
|
||||
class: 'inputWithMenu',
|
||||
'aria-label': formatMessage({
|
||||
id: 'admin.system_users.column_toggler.menuButtonAriaLabel',
|
||||
defaultMessage:
|
||||
'Open menu to select columns to display',
|
||||
}),
|
||||
as: 'div',
|
||||
children: (
|
||||
<Input
|
||||
label={formatMessage({
|
||||
id: 'admin.system_users.column_toggler.placeholder',
|
||||
defaultMessage: 'Columns',
|
||||
})}
|
||||
name='colXC'
|
||||
value={formatMessage(
|
||||
{
|
||||
id: 'admin.system_users.column_toggler.menuButtonText',
|
||||
defaultMessage: '{selectedCount} selected',
|
||||
},
|
||||
{
|
||||
selectedCount: props.visibleColumnsLength,
|
||||
},
|
||||
)}
|
||||
readOnly={true}
|
||||
inputSuffix={
|
||||
<i className='icon icon-chevron-down'/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
menu={{
|
||||
id: 'systemUsersColumnTogglerMenu',
|
||||
'aria-label': formatMessage({
|
||||
id: 'admin.system_users.column_toggler.dropdownAriaLabel',
|
||||
defaultMessage: 'Columns visibility menu',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{props.allColumns.map((column) => {
|
||||
let leadingElement;
|
||||
if (column.getIsVisible()) {
|
||||
leadingElement = (
|
||||
<i className='icon icon-checkbox-marked'/>
|
||||
);
|
||||
} else {
|
||||
leadingElement = (
|
||||
<i className='icon icon-checkbox-blank-outline'/>
|
||||
);
|
||||
}
|
||||
|
||||
const postStatsColumns: string[] = [ColumnNames.lastPostDate, ColumnNames.daysActive, ColumnNames.totalPosts];
|
||||
if (props.isMySql && postStatsColumns.includes(column.id)) {
|
||||
return (
|
||||
<WithTooltip
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
title={formatMessage({id: 'admin.system_users.column_toggler.mysql_unavailable.title', defaultMessage: 'Not available for servers using MySQL'})}
|
||||
hint={formatMessage({id: 'admin.system_users.column_toggler.mysql_unavailable.desc', defaultMessage: 'Please use the export functionality to view these values'})}
|
||||
placement='left'
|
||||
>
|
||||
<Menu.Item
|
||||
className='systemUsersColumnToggler__lockedItem'
|
||||
role='menuitemcheckbox'
|
||||
labels={getColumnName(column.id)}
|
||||
disabled={true}
|
||||
leadingElement={leadingElement}
|
||||
trailingElements={<Tag text={formatMessage({id: 'admin.system_users.column_toggler.mysql_unavailable.label', defaultMessage: 'Not available'})}/>}
|
||||
onClick={column.getToggleVisibilityHandler()}
|
||||
/>
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
role='menuitemcheckbox'
|
||||
labels={getColumnName(column.id)}
|
||||
disabled={!column.getCanHide()}
|
||||
leadingElement={leadingElement}
|
||||
onClick={column.getToggleVisibilityHandler()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu.Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.systemUsersColumnToggler {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
#systemUsersColumnTogglerMenu {
|
||||
i.icon.icon-checkbox-marked {
|
||||
color: var(--button-bg);
|
||||
}
|
||||
|
||||
i.icon.icon-checkbox-blank-outline {
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.24);
|
||||
}
|
||||
|
||||
.systemUsersColumnToggler__lockedItem {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// 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 {useDispatch} from 'react-redux';
|
||||
|
||||
import {ReportDuration} from '@mattermost/types/reports';
|
||||
|
||||
import {setAdminConsoleUsersManagementTableProperties} from 'actions/views/admin';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
|
||||
import './system_users_date_range_menu.scss';
|
||||
|
||||
function get30DaysBack(now: Date) {
|
||||
const prev = new Date(now);
|
||||
prev.setDate(prev.getDate() - 30);
|
||||
return prev;
|
||||
}
|
||||
|
||||
function get6MonthsBack(now: Date) {
|
||||
const prev = new Date(now);
|
||||
prev.setMonth(prev.getMonth() - 6);
|
||||
return prev;
|
||||
}
|
||||
|
||||
function getBeginningOfLastMonth(now: Date) {
|
||||
const beginningOfMonth = new Date(now);
|
||||
beginningOfMonth.setMonth(beginningOfMonth.getMonth() - 1);
|
||||
beginningOfMonth.setDate(1);
|
||||
return beginningOfMonth;
|
||||
}
|
||||
|
||||
function getEndOfLastMonth(now: Date) {
|
||||
const endOfMonth = new Date(now);
|
||||
endOfMonth.setDate(1);
|
||||
endOfMonth.setDate(endOfMonth.getDate() - 1);
|
||||
return endOfMonth;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dateRange: ReportDuration;
|
||||
}
|
||||
|
||||
export function SystemUsersDateRangeMenu(props: Props) {
|
||||
const {formatMessage, formatDate} = useIntl();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const now = new Date();
|
||||
|
||||
function getSelectedDateRange(dateRange: ReportDuration) {
|
||||
if (dateRange === ReportDuration.Last30Days) {
|
||||
return formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.date_range.last_30_days',
|
||||
defaultMessage: 'Last 30 days',
|
||||
});
|
||||
} else if (dateRange === ReportDuration.PreviousMonth) {
|
||||
return formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.date_range.previous_month',
|
||||
defaultMessage: 'Previous month',
|
||||
});
|
||||
} else if (dateRange === ReportDuration.Last6Months) {
|
||||
return formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.date_range.last_6_months',
|
||||
defaultMessage: 'Last 6 months',
|
||||
});
|
||||
}
|
||||
|
||||
return formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.date_range.all_time',
|
||||
defaultMessage: 'All time',
|
||||
});
|
||||
}
|
||||
|
||||
function updateDateRange(value?: ReportDuration) {
|
||||
dispatch(setAdminConsoleUsersManagementTableProperties({dateRange: value}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='systemUsersDateRangeSelector'>
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: 'systemUsersDateRangeSelectorMenuButton',
|
||||
class: 'inputWithMenu',
|
||||
'aria-label': formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.menuButtonAriaLabel',
|
||||
defaultMessage:
|
||||
'Open menu to select columns to display',
|
||||
}),
|
||||
as: 'div',
|
||||
children: (
|
||||
<Input
|
||||
label={formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.label',
|
||||
defaultMessage: 'Duration',
|
||||
})}
|
||||
name='colXC'
|
||||
value={getSelectedDateRange(props.dateRange)}
|
||||
readOnly={true}
|
||||
inputSuffix={
|
||||
<i className='icon icon-chevron-down'/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
menu={{
|
||||
id: 'systemUsersDateRangeSelectorMenu',
|
||||
'aria-label': formatMessage({
|
||||
id: 'admin.system_users.date_range_selector.dropdownAriaLabel',
|
||||
defaultMessage: 'Date range menu',
|
||||
}),
|
||||
width: '250px',
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
key={ReportDuration.AllTime}
|
||||
id={ReportDuration.AllTime}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.all_time'
|
||||
defaultMessage='All time'
|
||||
/>
|
||||
}
|
||||
trailingElements={props.dateRange === ReportDuration.AllTime && <i className='icon icon-check'/>}
|
||||
onClick={() => updateDateRange()}
|
||||
/>
|
||||
<Menu.Item
|
||||
key={ReportDuration.Last30Days}
|
||||
id={ReportDuration.Last30Days}
|
||||
labels={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.last_30_days'
|
||||
defaultMessage='Last 30 days'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.sublabel'
|
||||
defaultMessage='{startDate} - {endDate}'
|
||||
values={{
|
||||
startDate: formatDate(get30DaysBack(now)),
|
||||
endDate: formatDate(now),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
trailingElements={props.dateRange === ReportDuration.Last30Days && <i className='icon icon-check'/>}
|
||||
onClick={() => updateDateRange(ReportDuration.Last30Days)}
|
||||
/>
|
||||
<Menu.Item
|
||||
key={ReportDuration.PreviousMonth}
|
||||
id={ReportDuration.PreviousMonth}
|
||||
labels={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.previous_month'
|
||||
defaultMessage='Previous month'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.sublabel'
|
||||
defaultMessage='{startDate} - {endDate}'
|
||||
values={{
|
||||
startDate: formatDate(getBeginningOfLastMonth(now)),
|
||||
endDate: formatDate(getEndOfLastMonth(now)),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
trailingElements={props.dateRange === ReportDuration.PreviousMonth && <i className='icon icon-check'/>}
|
||||
onClick={() => updateDateRange(ReportDuration.PreviousMonth)}
|
||||
/>
|
||||
<Menu.Item
|
||||
key={ReportDuration.Last6Months}
|
||||
id={ReportDuration.Last6Months}
|
||||
labels={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.last_6_months'
|
||||
defaultMessage='Last 6 months'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.date_range.sublabel'
|
||||
defaultMessage='{startDate} - {endDate}'
|
||||
values={{
|
||||
startDate: formatDate(get6MonthsBack(now), {month: 'numeric', year: 'numeric'}),
|
||||
endDate: formatDate(now, {month: 'numeric', year: 'numeric'}),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
trailingElements={props.dateRange === ReportDuration.Last6Months && <i className='icon icon-check'/>}
|
||||
onClick={() => updateDateRange(ReportDuration.Last6Months)}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
<Menu.Item
|
||||
key='trailing_message'
|
||||
id='trailing_message'
|
||||
className='systemUsersDateRangeSelector__trailing-message'
|
||||
labels={
|
||||
<>
|
||||
<span/>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.date_range_selector.trailing_message'
|
||||
defaultMessage='Note: This filter will only affect values in the <strong>Last Post, Days Active, and Messages Posted</strong> columns.'
|
||||
values={{
|
||||
strong: (chunks: React.ReactNode) => (<strong>{chunks}</strong>),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
disabled={true}
|
||||
/>
|
||||
</Menu.Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.systemUsersDateRangeSelector {
|
||||
margin-inline-end: 8px;
|
||||
|
||||
.inputWithMenu input.form-control {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
#dateRangeSelectorMenu {
|
||||
.systemUsersDateRangeSelector__trailing-message {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
opacity: 1;
|
||||
|
||||
span {
|
||||
max-width: 210px;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
i.icon-check {
|
||||
color: var(--button-bg);
|
||||
}
|
||||
}
|
||||
@@ -1,713 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/admin_console/system_users/system_users_dropdown/system_users_dropdown renderDeactivateMemberModal should not render the bot accounts warning in case the user do not have any bot accounts 1`] = `
|
||||
<div>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\\\\n"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc.confirm"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/system_users/system_users_dropdown/system_users_dropdown renderDeactivateMemberModal should not render the bot accounts warning. owner_id has no enabled bot accounts 1`] = `
|
||||
<div>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\\\\n"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc.confirm"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/system_users/system_users_dropdown/system_users_dropdown renderDeactivateMemberModal should render the bot accounts warning. owner_id has enabled bot accounts 1`] = `
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<ul>
|
||||
<li>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action deactivates {username}"
|
||||
id="deactivate_member_modal.desc.for_users_with_bot_accounts1"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="They will be logged out and not have access to any teams or channels on this system."
|
||||
id="deactivate_member_modal.desc.for_users_with_bot_accounts2"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Bot accounts they manage will be disabled along with their integrations. To enable them again, go to <linkBots>Integrations > Bot Accounts</linkBots>. <linkDocumentation>Learn more about bot accounts</linkDocumentation>."
|
||||
id="deactivate_member_modal.desc.for_users_with_bot_accounts3"
|
||||
values={
|
||||
Object {
|
||||
"linkBots": [Function],
|
||||
"linkDocumentation": [Function],
|
||||
"siteURL": "http://localhost:8065",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p />
|
||||
<p />
|
||||
</React.Fragment>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc.confirm"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/system_users/system_users_dropdown/system_users_dropdown should match snapshot with license 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate"
|
||||
id="deactivate_member_modal.deactivate"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\\\\n"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc.confirm"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate {username}"
|
||||
id="deactivate_member_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Revoke"
|
||||
id="revoke_user_sessions_modal.revoke"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action revokes all sessions for {username}. They will be logged out from all devices. Are you sure you want to revoke all sessions for {username}?"
|
||||
id="revoke_user_sessions_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Revoke Sessions for {username}"
|
||||
id="revoke_user_sessions_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Promote"
|
||||
id="promote_to_user_modal.promote"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?"
|
||||
id="promote_to_user_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Promote guest {username} to member"
|
||||
id="promote_to_user_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Demote"
|
||||
id="demote_to_user_modal.demote"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action demotes the user {username} to a guest. It will restrict the user's ability to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to demote user {username} to guest?"
|
||||
id="demote_to_user_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Demote User {username} to Guest"
|
||||
id="demote_to_user_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
cancelButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="No"
|
||||
id="create_group_memberships_modal.cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Yes"
|
||||
id="create_group_memberships_modal.create"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time."
|
||||
id="create_group_memberships_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Re-add {username} to teams and channels"
|
||||
id="create_group_memberships_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
isDisabled={false}
|
||||
>
|
||||
<div
|
||||
className="text-right"
|
||||
>
|
||||
<a>
|
||||
<span>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Member"
|
||||
id="admin.user_item.member"
|
||||
/>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<Menu
|
||||
ariaLabel="User Actions Menu"
|
||||
openLeft={true}
|
||||
openUp={false}
|
||||
>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Activate"
|
||||
/>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Deactivate"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Manage Roles"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Manage Teams"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Manage Tokens"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Remove MFA"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Switch to Email/Password"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Reset Password"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Update Email"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Promote to Member"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Demote to Guest"
|
||||
/>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"revoke_user_access_token",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Revoke Sessions"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"sysconsole_write_user_management_groups",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Re-sync user via LDAP groups"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/system_users/system_users_dropdown/system_users_dropdown should match snapshot without license 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate"
|
||||
id="deactivate_member_modal.deactivate"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\\\\n"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc.confirm"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate {username}"
|
||||
id="deactivate_member_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Revoke"
|
||||
id="revoke_user_sessions_modal.revoke"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action revokes all sessions for {username}. They will be logged out from all devices. Are you sure you want to revoke all sessions for {username}?"
|
||||
id="revoke_user_sessions_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Revoke Sessions for {username}"
|
||||
id="revoke_user_sessions_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Promote"
|
||||
id="promote_to_user_modal.promote"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?"
|
||||
id="promote_to_user_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Promote guest {username} to member"
|
||||
id="promote_to_user_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Demote"
|
||||
id="demote_to_user_modal.demote"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action demotes the user {username} to a guest. It will restrict the user's ability to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to demote user {username} to guest?"
|
||||
id="demote_to_user_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Demote User {username} to Guest"
|
||||
id="demote_to_user_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfirmModal
|
||||
cancelButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="No"
|
||||
id="create_group_memberships_modal.cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Yes"
|
||||
id="create_group_memberships_modal.create"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time."
|
||||
id="create_group_memberships_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Re-add {username} to teams and channels"
|
||||
id="create_group_memberships_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "some-user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
isDisabled={false}
|
||||
>
|
||||
<div
|
||||
className="text-right"
|
||||
>
|
||||
<a>
|
||||
<span>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Member"
|
||||
id="admin.user_item.member"
|
||||
/>
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<Menu
|
||||
ariaLabel="User Actions Menu"
|
||||
openLeft={true}
|
||||
openUp={false}
|
||||
>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Activate"
|
||||
/>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Deactivate"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Manage Roles"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Manage Teams"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Manage Tokens"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Remove MFA"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Switch to Email/Password"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Reset Password"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Update Email"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Promote to Member"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Demote to Guest"
|
||||
/>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"revoke_user_access_token",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Revoke Sessions"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"sysconsole_write_user_management_groups",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Re-sync user via LDAP groups"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {loadBots} from 'mattermost-redux/actions/bots';
|
||||
import {createGroupTeamsAndChannels} from 'mattermost-redux/actions/groups';
|
||||
import {updateUserActive, revokeAllSessionsForUser, promoteGuestToUser, demoteUserToGuest} from 'mattermost-redux/actions/users';
|
||||
import * as Selectors from 'mattermost-redux/selectors/entities/admin';
|
||||
import {getExternalBotAccounts} from 'mattermost-redux/selectors/entities/bots';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import SystemUsersDropdown from './system_users_dropdown';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const bots = getExternalBotAccounts(state);
|
||||
const license = getLicense(state);
|
||||
return {
|
||||
isLicensed: license && license.IsLicensed === 'true',
|
||||
config: Selectors.getConfig(state),
|
||||
currentUser: getCurrentUser(state),
|
||||
bots,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
updateUserActive,
|
||||
revokeAllSessionsForUser,
|
||||
promoteGuestToUser,
|
||||
demoteUserToGuest,
|
||||
loadBots,
|
||||
createGroupTeamsAndChannels,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SystemUsersDropdown);
|
||||
@@ -1,257 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import SystemUsersDropdown from './system_users_dropdown';
|
||||
import type {Props} from './system_users_dropdown';
|
||||
|
||||
describe('components/admin_console/system_users/system_users_dropdown/system_users_dropdown', () => {
|
||||
const user: UserProfile & {mfa_active: boolean} = Object.assign(TestHelper.getUserMock(), {mfa_active: true});
|
||||
|
||||
const otherUser = TestHelper.getUserMock({
|
||||
id: 'other_user_id',
|
||||
roles: '',
|
||||
username: 'other-user',
|
||||
});
|
||||
|
||||
const mockMouseEvent = TestHelper.getMockMouseButtonEvent();
|
||||
|
||||
const requiredProps: Props = {
|
||||
user,
|
||||
mfaEnabled: true,
|
||||
isLicensed: true,
|
||||
enableUserAccessTokens: true,
|
||||
experimentalEnableAuthenticationTransfer: true,
|
||||
doPasswordReset: jest.fn(),
|
||||
doEmailReset: jest.fn(),
|
||||
doManageTeams: jest.fn(),
|
||||
doManageRoles: jest.fn(),
|
||||
doManageTokens: jest.fn(),
|
||||
onError: jest.fn(),
|
||||
currentUser: otherUser,
|
||||
index: 0,
|
||||
totalUsers: 10,
|
||||
actions: {
|
||||
updateUserActive: jest.fn().mockResolvedValue({data: true}),
|
||||
revokeAllSessionsForUser: jest.fn().mockResolvedValue({data: true}),
|
||||
promoteGuestToUser: jest.fn().mockResolvedValue({data: true}),
|
||||
demoteUserToGuest: jest.fn().mockResolvedValue({data: true}),
|
||||
loadBots: jest.fn(() => Promise.resolve([])),
|
||||
createGroupTeamsAndChannels: jest.fn().mockResolvedValue({data: true}),
|
||||
},
|
||||
config: {
|
||||
GuestAccountsSettings: {
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
bots: {},
|
||||
};
|
||||
|
||||
test('handleMakeActive() should have called updateUserActive', async () => {
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...requiredProps}/>);
|
||||
|
||||
wrapper.instance().handleMakeActive(mockMouseEvent, false);
|
||||
|
||||
expect(requiredProps.actions.updateUserActive).toHaveBeenCalledTimes(1);
|
||||
expect(requiredProps.actions.updateUserActive).toHaveBeenCalledWith(requiredProps.user.id, true);
|
||||
});
|
||||
|
||||
test('handleMakeActive() should not have called updateUserActive if user auth service is LDAP', async () => {
|
||||
const ldapUserProps = {...requiredProps, user: {...requiredProps.user, auth_service: 'ldap'}};
|
||||
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...ldapUserProps}/>);
|
||||
|
||||
wrapper.instance().handleMakeActive(mockMouseEvent, true);
|
||||
|
||||
expect(requiredProps.actions.updateUserActive).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('handleMakeActive() should have called onError', async () => {
|
||||
const retVal = {error: {server_error_id: 'id', message: 'error'}};
|
||||
const updateUserActive = jest.fn().mockResolvedValue(retVal);
|
||||
const props = {...requiredProps, actions: {...requiredProps.actions, updateUserActive}};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...props}/>);
|
||||
|
||||
await wrapper.instance().handleMakeActive(mockMouseEvent, false);
|
||||
|
||||
expect(requiredProps.onError).toHaveBeenCalledTimes(1);
|
||||
expect(requiredProps.onError).toHaveBeenCalledWith({id: retVal.error.server_error_id, ...retVal.error});
|
||||
});
|
||||
|
||||
test('handleDeactivateMember() should have called updateUserActive', async () => {
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...requiredProps}/>);
|
||||
|
||||
await wrapper.instance().handleDeactivateMember();
|
||||
|
||||
expect(requiredProps.actions.updateUserActive).toHaveBeenCalledTimes(1);
|
||||
expect(requiredProps.actions.updateUserActive).toHaveBeenCalledWith(requiredProps.user.id, false);
|
||||
});
|
||||
|
||||
test('handleShowDeactivateMemberModal() should not have show the deactivation modal if user auth service is LDAP', async () => {
|
||||
const ldapUserProps = {...requiredProps, user: {...requiredProps.user, auth_service: 'ldap'}};
|
||||
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...ldapUserProps}/>);
|
||||
|
||||
await wrapper.instance().handleShowDeactivateMemberModal(mockMouseEvent, true);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.state('showDeactivateMemberModal')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('handleDeactivateMember() should have called onError', async () => {
|
||||
const retVal = {error: {server_error_id: 'id', message: 'error'}};
|
||||
const updateUserActive = jest.fn().mockResolvedValue(retVal);
|
||||
const props = {...requiredProps, actions: {...requiredProps.actions, updateUserActive}};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...props}/>);
|
||||
|
||||
await wrapper.instance().handleDeactivateMember();
|
||||
|
||||
expect(requiredProps.onError).toHaveBeenCalledTimes(1);
|
||||
expect(requiredProps.onError).toHaveBeenCalledWith({id: retVal.error.server_error_id, ...retVal.error});
|
||||
});
|
||||
|
||||
test('handleRevokeSessions() should have called revokeAllSessions', async () => {
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...requiredProps}/>);
|
||||
|
||||
await wrapper.instance().handleRevokeSessions();
|
||||
|
||||
expect(requiredProps.actions.revokeAllSessionsForUser).toHaveBeenCalled();
|
||||
expect(requiredProps.actions.revokeAllSessionsForUser).toHaveBeenCalledWith(requiredProps.user.id);
|
||||
});
|
||||
|
||||
test('handleRevokeSessions() should have called onError', async () => {
|
||||
const revokeAllSessionsForUser = jest.fn().mockResolvedValue({error: {}});
|
||||
const onError = jest.fn();
|
||||
const props = {...requiredProps, onError, actions: {...requiredProps.actions, revokeAllSessionsForUser}};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...props}/>);
|
||||
|
||||
await wrapper.instance().handleRevokeSessions();
|
||||
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handleShowDeactivateMemberModal should not call the loadBots if the setting is not true', async () => {
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...requiredProps}/>);
|
||||
|
||||
await wrapper.instance().handleShowDeactivateMemberModal(mockMouseEvent, false);
|
||||
|
||||
expect(requiredProps.actions.loadBots).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('handleShowDeactivateMemberModal should call the loadBots only if the setting is true', async () => {
|
||||
const overrideConfig = {
|
||||
ServiceSettings: {
|
||||
DisableBotsWhenOwnerIsDeactivated: true,
|
||||
},
|
||||
};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...{...requiredProps, config: overrideConfig, bots: {}}}/>);
|
||||
|
||||
await wrapper.instance().handleShowDeactivateMemberModal(mockMouseEvent, false);
|
||||
|
||||
expect(requiredProps.actions.loadBots).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renderDeactivateMemberModal should not render the bot accounts warning in case the user do not have any bot accounts', async () => {
|
||||
const overrideProps = {
|
||||
config: {
|
||||
ServiceSettings: {
|
||||
DisableBotsWhenOwnerIsDeactivated: true,
|
||||
},
|
||||
},
|
||||
bots: {
|
||||
1: TestHelper.getBotMock({owner_id: '1'}),
|
||||
2: TestHelper.getBotMock({owner_id: '1'}),
|
||||
3: TestHelper.getBotMock({owner_id: '2'}),
|
||||
},
|
||||
};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...{...requiredProps, ...overrideProps}}/>);
|
||||
const ConfirmModal = () => wrapper.instance().renderDeactivateMemberModal();
|
||||
const modal = shallow(<ConfirmModal/>);
|
||||
expect(modal.prop('message')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renderDeactivateMemberModal should render the bot accounts warning. owner_id has enabled bot accounts', async () => {
|
||||
const overrideProps = {
|
||||
config: {
|
||||
ServiceSettings: {
|
||||
DisableBotsWhenOwnerIsDeactivated: true,
|
||||
},
|
||||
},
|
||||
bots: {
|
||||
1: TestHelper.getBotMock({owner_id: '1', delete_at: 0}),
|
||||
2: TestHelper.getBotMock({owner_id: '1', delete_at: 0}),
|
||||
3: TestHelper.getBotMock({owner_id: 'user_id', delete_at: 0}),
|
||||
},
|
||||
};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...{...requiredProps, ...overrideProps}}/>);
|
||||
wrapper.setState({showDeactivateMemberModal: true});
|
||||
const ConfirmModal = () => wrapper.instance().renderDeactivateMemberModal();
|
||||
const modal = shallow(<ConfirmModal/>);
|
||||
expect(modal.prop('message')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renderDeactivateMemberModal should not render the bot accounts warning. owner_id has no enabled bot accounts', async () => {
|
||||
const overrideProps = {
|
||||
config: {
|
||||
ServiceSettings: {
|
||||
DisableBotsWhenOwnerIsDeactivated: true,
|
||||
},
|
||||
},
|
||||
bots: {
|
||||
1: TestHelper.getBotMock({owner_id: '1', delete_at: 0}),
|
||||
2: TestHelper.getBotMock({owner_id: '1', delete_at: 0}),
|
||||
3: TestHelper.getBotMock({owner_id: 'user_id', delete_at: 1234}),
|
||||
},
|
||||
};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...{...requiredProps, ...overrideProps}}/>);
|
||||
wrapper.setState({showDeactivateMemberModal: true});
|
||||
const ConfirmModal = () => wrapper.instance().renderDeactivateMemberModal();
|
||||
const modal = shallow(<ConfirmModal/>);
|
||||
expect(modal.prop('message')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Manage Roles button should be hidden for system manager', async () => {
|
||||
const systemManager = TestHelper.getUserMock({
|
||||
id: 'system_manager_id',
|
||||
roles: 'system_user system_manager',
|
||||
username: 'system-manager',
|
||||
});
|
||||
const overrideProps = {
|
||||
currentUser: systemManager,
|
||||
};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...{...requiredProps, ...overrideProps}}/>);
|
||||
expect(wrapper.find(Menu.ItemAction).find({text: 'Manage Roles'}).props().show).toBe(false);
|
||||
});
|
||||
|
||||
test('Manage Roles button should be visible for system admin', async () => {
|
||||
const systemAdmin = TestHelper.getUserMock({
|
||||
id: 'system_admin_id',
|
||||
roles: 'system_user system_admin',
|
||||
username: 'system-admin',
|
||||
});
|
||||
const overrideProps = {
|
||||
currentUser: systemAdmin,
|
||||
};
|
||||
const wrapper = shallow<SystemUsersDropdown>(<SystemUsersDropdown {...{...requiredProps, ...overrideProps}}/>);
|
||||
expect(wrapper.find(Menu.ItemAction).find({text: 'Manage Roles'}).props().show).toBe(true);
|
||||
});
|
||||
|
||||
test('should match snapshot with license', async () => {
|
||||
const wrapper = shallow(<SystemUsersDropdown {...requiredProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot without license', async () => {
|
||||
const wrapper = shallow(<SystemUsersDropdown {...{...requiredProps, isLicensed: false}}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,751 +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 type {Bot} from '@mattermost/types/bots';
|
||||
import type {AdminConfig} from '@mattermost/types/config';
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import * as UserUtils from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {adminResetMfa} from 'actions/admin_actions.jsx';
|
||||
import {emitUserLoggedOutEvent} from 'actions/global_actions';
|
||||
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
const ROWS_FROM_BOTTOM_TO_OPEN_UP = 3;
|
||||
const TOTAL_USERS_TO_OPEN_UP = 5;
|
||||
|
||||
export type Props = {
|
||||
user: UserProfile;
|
||||
currentUser: UserProfile;
|
||||
mfaEnabled: boolean;
|
||||
enableUserAccessTokens: boolean;
|
||||
experimentalEnableAuthenticationTransfer: boolean;
|
||||
index: number;
|
||||
totalUsers: number;
|
||||
config: DeepPartial<AdminConfig>;
|
||||
bots: Record<string, Bot>;
|
||||
isLicensed: boolean;
|
||||
actions: {
|
||||
updateUserActive: (id: string, active: boolean) => Promise<ActionResult>;
|
||||
revokeAllSessionsForUser: (id: string) => Promise<ActionResult<boolean>>;
|
||||
promoteGuestToUser: (id: string) => Promise<ActionResult>;
|
||||
demoteUserToGuest: (id: string) => Promise<ActionResult>;
|
||||
loadBots: (page?: number, size?: number) => Promise<unknown>;
|
||||
createGroupTeamsAndChannels: (userId: string) => Promise<ActionResult>;
|
||||
};
|
||||
doPasswordReset: (user: UserProfile) => void;
|
||||
doEmailReset: (user: UserProfile) => void;
|
||||
doManageTeams: (user: UserProfile) => void;
|
||||
doManageRoles: (user: UserProfile) => void;
|
||||
doManageTokens: (user: UserProfile) => void;
|
||||
onError: (error: ServerError | {id: string}) => void;
|
||||
}
|
||||
|
||||
type State = {
|
||||
showDeactivateMemberModal: boolean;
|
||||
showRevokeSessionsModal: boolean;
|
||||
showPromoteToUserModal: boolean;
|
||||
showDemoteToGuestModal: boolean;
|
||||
showCreateGroupMembershipsModal: boolean;
|
||||
user: UserProfile | null;
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
export default class SystemUsersDropdown extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showDeactivateMemberModal: false,
|
||||
showRevokeSessionsModal: false,
|
||||
showPromoteToUserModal: false,
|
||||
showDemoteToGuestModal: false,
|
||||
showCreateGroupMembershipsModal: false,
|
||||
user: null,
|
||||
role: null,
|
||||
};
|
||||
}
|
||||
|
||||
handleMakeActive = (e: React.MouseEvent<HTMLButtonElement>, disableActivationToggle: boolean) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (disableActivationToggle) {
|
||||
return;
|
||||
}
|
||||
this.props.actions.updateUserActive(this.props.user.id, true).
|
||||
then(this.onUpdateActiveResult);
|
||||
};
|
||||
|
||||
handleManageTeams = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.doManageTeams(this.props.user);
|
||||
};
|
||||
|
||||
handleManageRoles = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.doManageRoles(this.props.user);
|
||||
};
|
||||
|
||||
handleManageTokens = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.doManageTokens(this.props.user);
|
||||
};
|
||||
|
||||
handleResetPassword = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
this.props.doPasswordReset(this.props.user);
|
||||
};
|
||||
|
||||
handleResetEmail = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
this.props.doEmailReset(this.props.user);
|
||||
};
|
||||
|
||||
handleResetMfa = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
adminResetMfa(this.props.user.id, null, this.props.onError);
|
||||
};
|
||||
|
||||
handleShowDeactivateMemberModal = async (e: React.MouseEvent<HTMLButtonElement>, disableActivationToggle: boolean) => {
|
||||
e.preventDefault();
|
||||
if (disableActivationToggle) {
|
||||
return;
|
||||
}
|
||||
if (this.shouldDisableBotsWhenOwnerIsDeactivated()) {
|
||||
await this.props.actions.loadBots(
|
||||
Constants.Integrations.START_PAGE_NUM,
|
||||
Constants.Integrations.PAGE_SIZE,
|
||||
);
|
||||
}
|
||||
this.setState({showDeactivateMemberModal: true});
|
||||
};
|
||||
|
||||
handleDeactivateMember = () => {
|
||||
this.props.actions.updateUserActive(this.props.user.id, false).
|
||||
then(this.onUpdateActiveResult);
|
||||
this.setState({showDeactivateMemberModal: false});
|
||||
};
|
||||
|
||||
onUpdateActiveResult = ({error}: ActionResult) => {
|
||||
if (error) {
|
||||
this.props.onError({id: error.server_error_id, ...error});
|
||||
}
|
||||
};
|
||||
|
||||
handleDeactivateCancel = () => {
|
||||
this.setState({showDeactivateMemberModal: false});
|
||||
};
|
||||
|
||||
renderDeactivateMemberModal = () => {
|
||||
const user = this.props.user;
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.title'
|
||||
defaultMessage='Deactivate {username}'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const defaultMessage = (
|
||||
<FormattedMarkdownMessage
|
||||
id='deactivate_member_modal.desc'
|
||||
defaultMessage='This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\n'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>);
|
||||
|
||||
let warning;
|
||||
if (user.auth_service !== '' && user.auth_service !== Constants.EMAIL_SERVICE) {
|
||||
warning = (
|
||||
<strong>
|
||||
<br/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.sso_warning'
|
||||
defaultMessage='You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.'
|
||||
/>
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
const confirmationMessage = (
|
||||
<FormattedMarkdownMessage
|
||||
id='deactivate_member_modal.desc.confirm'
|
||||
defaultMessage='Are you sure you want to deactivate {username}?'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>);
|
||||
let messageForUsersWithBotAccounts;
|
||||
if (this.shouldDisableBotsWhenOwnerIsDeactivated()) {
|
||||
for (const bot of Object.values(this.props.bots)) {
|
||||
if ((bot.owner_id === user.id) && this.state.showDeactivateMemberModal && (bot.delete_at === 0)) {
|
||||
messageForUsersWithBotAccounts = (
|
||||
<>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.for_users_with_bot_accounts1'
|
||||
defaultMessage='This action deactivates {username}'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.for_users_with_bot_accounts2'
|
||||
defaultMessage='They will be logged out and not have access to any teams or channels on this system.'
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.for_users_with_bot_accounts3'
|
||||
defaultMessage='Bot accounts they manage will be disabled along with their integrations. To enable them again, go to <linkBots>Integrations > Bot Accounts</linkBots>. <linkDocumentation>Learn more about bot accounts</linkDocumentation>.'
|
||||
values={{
|
||||
siteURL: getSiteURL(),
|
||||
linkBots: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href={`${getSiteURL()}/_redirect/integrations/bots`}
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
linkDocumentation: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href='https://mattermost.com/pl/default-bot-accounts'
|
||||
location='system_users_dropdown'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p/>
|
||||
<p/>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const message = (
|
||||
<div>
|
||||
{messageForUsersWithBotAccounts || defaultMessage}
|
||||
{confirmationMessage}
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
|
||||
const confirmButtonClass = 'btn btn-danger';
|
||||
const deactivateMemberButton = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.deactivate'
|
||||
defaultMessage='Deactivate'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
show={this.state.showDeactivateMemberModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass={confirmButtonClass}
|
||||
confirmButtonText={deactivateMemberButton}
|
||||
onConfirm={this.handleDeactivateMember}
|
||||
onCancel={this.handleDeactivateCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
shouldDisableBotsWhenOwnerIsDeactivated() {
|
||||
return this.props.config &&
|
||||
this.props.config.ServiceSettings &&
|
||||
this.props.config.ServiceSettings.DisableBotsWhenOwnerIsDeactivated;
|
||||
}
|
||||
|
||||
handleShowRevokeSessionsModal = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
this.setState({showRevokeSessionsModal: true});
|
||||
};
|
||||
|
||||
handleShowCreateGroupSyncableMembershipsModal = (e: {preventDefault: () => void}) => {
|
||||
e.preventDefault();
|
||||
this.setState({showCreateGroupMembershipsModal: true});
|
||||
};
|
||||
|
||||
handleCreateGroupSyncableMemberships = async () => {
|
||||
const {error} = await this.props.actions.createGroupTeamsAndChannels(this.props.user.id);
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
}
|
||||
|
||||
this.setState({showCreateGroupMembershipsModal: false});
|
||||
};
|
||||
|
||||
handleCreateGroupSyncableMembershipsCancel = () => {
|
||||
this.setState({showCreateGroupMembershipsModal: false});
|
||||
};
|
||||
|
||||
handleRevokeSessions = async () => {
|
||||
const me = this.props.currentUser;
|
||||
|
||||
const {data, error} = await this.props.actions.revokeAllSessionsForUser(this.props.user.id);
|
||||
if (data && this.props.user.id === me.id) {
|
||||
emitUserLoggedOutEvent();
|
||||
} else if (error) {
|
||||
this.props.onError(error);
|
||||
}
|
||||
|
||||
this.setState({showRevokeSessionsModal: false});
|
||||
};
|
||||
|
||||
handleRevokeSessionsCancel = () => {
|
||||
this.setState({showRevokeSessionsModal: false});
|
||||
};
|
||||
|
||||
handlePromoteToUser = () => {
|
||||
this.setState({showPromoteToUserModal: true});
|
||||
};
|
||||
|
||||
handlePromoteToUserConfirm = async () => {
|
||||
const {error} = await this.props.actions.promoteGuestToUser(this.props.user.id);
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
}
|
||||
|
||||
this.setState({showPromoteToUserModal: false});
|
||||
};
|
||||
|
||||
handlePromoteToUserCancel = () => {
|
||||
this.setState({showPromoteToUserModal: false});
|
||||
};
|
||||
|
||||
handleDemoteToGuest = () => {
|
||||
this.setState({showDemoteToGuestModal: true});
|
||||
};
|
||||
|
||||
handleDemoteToGuestConfirm = async () => {
|
||||
const {error} = await this.props.actions.demoteUserToGuest(this.props.user.id);
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
}
|
||||
this.setState({showDemoteToGuestModal: false});
|
||||
};
|
||||
|
||||
handleDemoteToGuestCancel = () => {
|
||||
this.setState({showDemoteToGuestModal: false});
|
||||
};
|
||||
|
||||
renderPromoteToUserModal = () => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='promote_to_user_modal.title'
|
||||
defaultMessage='Promote guest {username} to member'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='promote_to_user_modal.desc'
|
||||
defaultMessage='This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const promoteUserButton = (
|
||||
<FormattedMessage
|
||||
id='promote_to_user_modal.promote'
|
||||
defaultMessage='Promote'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
show={this.state.showPromoteToUserModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={promoteUserButton}
|
||||
onConfirm={this.handlePromoteToUserConfirm}
|
||||
onCancel={this.handlePromoteToUserCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderDemoteToGuestModal = () => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='demote_to_user_modal.title'
|
||||
defaultMessage='Demote User {username} to Guest'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='demote_to_user_modal.desc'
|
||||
defaultMessage={'This action demotes the user {username} to a guest. It will restrict the user\'s ability to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to demote user {username} to guest?'}
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const demoteGuestButton = (
|
||||
<FormattedMessage
|
||||
id='demote_to_user_modal.demote'
|
||||
defaultMessage='Demote'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
show={this.state.showDemoteToGuestModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={demoteGuestButton}
|
||||
onConfirm={this.handleDemoteToGuestConfirm}
|
||||
onCancel={this.handleDemoteToGuestCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderRevokeSessionsModal = () => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='revoke_user_sessions_modal.title'
|
||||
defaultMessage='Revoke Sessions for {username}'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='revoke_user_sessions_modal.desc'
|
||||
defaultMessage='This action revokes all sessions for {username}. They will be logged out from all devices. Are you sure you want to revoke all sessions for {username}?'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const revokeUserButtonButton = (
|
||||
<FormattedMessage
|
||||
id='revoke_user_sessions_modal.revoke'
|
||||
defaultMessage='Revoke'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
show={this.state.showRevokeSessionsModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={revokeUserButtonButton}
|
||||
onConfirm={this.handleRevokeSessions}
|
||||
onCancel={this.handleRevokeSessionsCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderCreateGroupSyncablesMembershipsModal = () => {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.title'
|
||||
defaultMessage='Re-add {username} to teams and channels'
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.desc'
|
||||
defaultMessage="You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time."
|
||||
values={{
|
||||
username: this.props.user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const createGroupMembershipsButton = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.create'
|
||||
defaultMessage='Yes'
|
||||
/>
|
||||
);
|
||||
|
||||
const cancelGroupMembershipsButton = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.cancel'
|
||||
defaultMessage='No'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
show={this.state.showCreateGroupMembershipsModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
cancelButtonText={cancelGroupMembershipsButton}
|
||||
confirmButtonText={createGroupMembershipsButton}
|
||||
onConfirm={this.handleCreateGroupSyncableMemberships}
|
||||
onCancel={this.handleCreateGroupSyncableMembershipsCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderAccessToken = () => {
|
||||
const userAccessTokensEnabled = this.props.enableUserAccessTokens;
|
||||
if (!userAccessTokensEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = this.props.user;
|
||||
const hasPostAllRole = UserUtils.hasPostAllRole(user.roles);
|
||||
const hasPostAllPublicRole = UserUtils.hasPostAllPublicRole(user.roles);
|
||||
const hasUserAccessTokenRole = UserUtils.hasUserAccessTokenRole(user.roles);
|
||||
const isSystemAdmin = UserUtils.isSystemAdmin(user.roles);
|
||||
|
||||
let messageId = '';
|
||||
if (hasUserAccessTokenRole || isSystemAdmin) {
|
||||
if (hasPostAllRole) {
|
||||
messageId = t('admin.user_item.userAccessTokenPostAll');
|
||||
} else if (hasPostAllPublicRole) {
|
||||
messageId = t('admin.user_item.userAccessTokenPostAllPublic');
|
||||
} else {
|
||||
messageId = t('admin.user_item.userAccessTokenYes');
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='light mt-1'>
|
||||
<FormattedMessage
|
||||
key='admin.user_item.userAccessToken'
|
||||
id={messageId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentUser, user, isLicensed, config} = this.props;
|
||||
|
||||
// Disable if SystemAdmin being edited by non SystemAdmin eg. userManager with EditOtherUsers permissions
|
||||
const isDisabled = UserUtils.isSystemAdmin(user.roles) && !UserUtils.isSystemAdmin(currentUser.roles);
|
||||
|
||||
const isGuest = UserUtils.isGuest(user.roles);
|
||||
if (!user) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
let currentRoles = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.member'
|
||||
defaultMessage='Member'
|
||||
/>
|
||||
);
|
||||
|
||||
if (isGuest) {
|
||||
currentRoles = (
|
||||
<FormattedMessage
|
||||
id='team_members_dropdown.guest'
|
||||
defaultMessage='Guest'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.roles.length > 0 && UserUtils.isSystemAdmin(user.roles)) {
|
||||
currentRoles = (
|
||||
<FormattedMessage
|
||||
id='team_members_dropdown.systemAdmin'
|
||||
defaultMessage='System Admin'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let showMakeActive = false;
|
||||
let showMakeNotActive = !UserUtils.isSystemAdmin(user.roles);
|
||||
let showManageTeams = true;
|
||||
let showRevokeSessions = true;
|
||||
const showMfaReset = this.props.mfaEnabled && Boolean(user.mfa_active);
|
||||
const showManageRoles = UserUtils.isSystemAdmin(currentUser.roles);
|
||||
|
||||
if (user.delete_at > 0) {
|
||||
currentRoles = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.inactive'
|
||||
defaultMessage='Deactivated'
|
||||
/>
|
||||
);
|
||||
showMakeActive = true;
|
||||
showMakeNotActive = false;
|
||||
showManageTeams = false;
|
||||
showRevokeSessions = false;
|
||||
}
|
||||
|
||||
let disableActivationToggle = false;
|
||||
if (user.auth_service === Constants.LDAP_SERVICE) {
|
||||
disableActivationToggle = true;
|
||||
}
|
||||
|
||||
const deactivateMemberModal = this.renderDeactivateMemberModal();
|
||||
const revokeSessionsModal = this.renderRevokeSessionsModal();
|
||||
const promoteToUserModal = this.renderPromoteToUserModal();
|
||||
const demoteToGuestModal = this.renderDemoteToGuestModal();
|
||||
const createGroupSyncablesMembershipsModal = this.renderCreateGroupSyncablesMembershipsModal();
|
||||
|
||||
const getExtraText = (disableActivationToggle: boolean) => {
|
||||
return disableActivationToggle ? {
|
||||
extraText: Utils.localizeMessage('admin.user_item.managedByLdap', 'Managed by LDAP'),
|
||||
} : {};
|
||||
};
|
||||
|
||||
const {index, totalUsers} = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{deactivateMemberModal}
|
||||
{revokeSessionsModal}
|
||||
{promoteToUserModal}
|
||||
{demoteToGuestModal}
|
||||
{createGroupSyncablesMembershipsModal}
|
||||
<MenuWrapper
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<div className='text-right'>
|
||||
<a>
|
||||
<span>{currentRoles} </span>
|
||||
{!isDisabled &&
|
||||
<span className='caret'/>
|
||||
}
|
||||
</a>
|
||||
{this.renderAccessToken()}
|
||||
</div>
|
||||
<Menu
|
||||
openLeft={true}
|
||||
openUp={totalUsers > TOTAL_USERS_TO_OPEN_UP && totalUsers - index <= ROWS_FROM_BOTTOM_TO_OPEN_UP}
|
||||
ariaLabel={Utils.localizeMessage('admin.user_item.menuAriaLabel', 'User Actions Menu')}
|
||||
>
|
||||
<Menu.ItemAction
|
||||
show={showMakeActive}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => this.handleMakeActive(e, disableActivationToggle)}
|
||||
text={Utils.localizeMessage('admin.user_item.makeActive', 'Activate')}
|
||||
disabled={disableActivationToggle}
|
||||
{...getExtraText(disableActivationToggle)}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={showMakeNotActive}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => this.handleShowDeactivateMemberModal(e, disableActivationToggle)}
|
||||
text={Utils.localizeMessage('admin.user_item.makeInactive', 'Deactivate')}
|
||||
disabled={disableActivationToggle}
|
||||
{...getExtraText(disableActivationToggle)}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={showManageRoles}
|
||||
onClick={this.handleManageRoles}
|
||||
text={Utils.localizeMessage('admin.user_item.manageRoles', 'Manage Roles')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={showManageTeams}
|
||||
onClick={this.handleManageTeams}
|
||||
text={Utils.localizeMessage('admin.user_item.manageTeams', 'Manage Teams')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={this.props.enableUserAccessTokens}
|
||||
onClick={this.handleManageTokens}
|
||||
text={Utils.localizeMessage('admin.user_item.manageTokens', 'Manage Tokens')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={showMfaReset}
|
||||
onClick={this.handleResetMfa}
|
||||
text={Utils.localizeMessage('admin.user_item.resetMfa', 'Remove MFA')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={Boolean(user.auth_service) && this.props.experimentalEnableAuthenticationTransfer}
|
||||
onClick={this.handleResetPassword}
|
||||
text={Utils.localizeMessage('admin.user_item.switchToEmail', 'Switch to Email/Password')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={!user.auth_service}
|
||||
onClick={this.handleResetPassword}
|
||||
text={Utils.localizeMessage('admin.user_item.resetPwd', 'Reset Password')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={!user.auth_service}
|
||||
onClick={this.handleResetEmail}
|
||||
text={Utils.localizeMessage('admin.user_item.resetEmail', 'Update Email')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={isGuest}
|
||||
onClick={this.handlePromoteToUser}
|
||||
text={Utils.localizeMessage('admin.user_item.promoteToMember', 'Promote to Member')}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
show={!isGuest && user.id !== currentUser.id && isLicensed && config.GuestAccountsSettings?.Enable}
|
||||
onClick={this.handleDemoteToGuest}
|
||||
text={Utils.localizeMessage('admin.user_item.demoteToGuest', 'Demote to Guest')}
|
||||
/>
|
||||
<SystemPermissionGate permissions={[Permissions.REVOKE_USER_ACCESS_TOKEN]}>
|
||||
<Menu.ItemAction
|
||||
show={showRevokeSessions}
|
||||
onClick={this.handleShowRevokeSessionsModal}
|
||||
text={Utils.localizeMessage('admin.user_item.revokeSessions', 'Revoke Sessions')}
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
<SystemPermissionGate permissions={[Permissions.SYSCONSOLE_WRITE_USERMANAGEMENT_GROUPS]}>
|
||||
<Menu.ItemAction
|
||||
show={user.auth_service === Constants.LDAP_SERVICE}
|
||||
onClick={this.handleShowCreateGroupSyncableMembershipsModal}
|
||||
text={Utils.localizeMessage('admin.user_item.createGroupTeamChannelMemberships', 'Re-sync user via LDAP groups')}
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 type {ServerError} from '@mattermost/types/errors';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
error: ServerError;
|
||||
onExited: () => void;
|
||||
}
|
||||
|
||||
export function ExportErrorModal({error, onExited}: Props) {
|
||||
const isInProgress = error.status_code === 400 &&
|
||||
error.server_error_id === 'app.report.start_users_batch_export.job_exists';
|
||||
|
||||
let title = (
|
||||
<FormattedMessage
|
||||
id='export_error_modal.title'
|
||||
defaultMessage='Export could not be initiated'
|
||||
/>
|
||||
);
|
||||
|
||||
let message = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='export_error_modal.desc'
|
||||
defaultMessage='We’re not able to initiate an export of this data at the moment. Please wait a few minutes and try again.'
|
||||
/>
|
||||
<div className='error'>{error.message}</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isInProgress) {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='export_error_modal.inProgress.title'
|
||||
defaultMessage='Export is in progress'
|
||||
/>
|
||||
);
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='export_error_modal.inProgress.desc'
|
||||
defaultMessage={'You\'ve already started an export of this data. Please wait a few more minutes to access the CSV file or to generate the report again.'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id='generic.okay'
|
||||
defaultMessage='Okay'
|
||||
/>
|
||||
}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 {useSelector} from 'react-redux';
|
||||
|
||||
import {ReportDuration} from '@mattermost/types/reports';
|
||||
|
||||
import {getAdminConsoleUserManagementTableProperties} from 'selectors/views/admin';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
onConfirm: (checked: boolean) => void;
|
||||
onExited: () => void;
|
||||
}
|
||||
|
||||
export function ExportUserDataModal({onConfirm, onExited}: Props) {
|
||||
const dateRange = useSelector(getAdminConsoleUserManagementTableProperties).dateRange ?? ReportDuration.AllTime;
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.title'
|
||||
defaultMessage='Export user data'
|
||||
/>
|
||||
);
|
||||
|
||||
let message = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.dange_range.all_time'
|
||||
defaultMessage={'You\'re about to export user data for all time. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.'}
|
||||
/>
|
||||
);
|
||||
if (dateRange === ReportDuration.Last30Days) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.dange_range.last_30_days'
|
||||
defaultMessage={'You\'re about to export user data for the last 30 days. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.'}
|
||||
/>
|
||||
);
|
||||
} else if (dateRange === ReportDuration.PreviousMonth) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.dange_range.previous_month'
|
||||
defaultMessage={'You\'re about to export user data for the previous month. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.'}
|
||||
/>
|
||||
);
|
||||
} else if (dateRange === ReportDuration.Last6Months) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.dange_range.last_6_months'
|
||||
defaultMessage={'You\'re about to export user data for the last 6 months. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const exportDataButton = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.export_data'
|
||||
defaultMessage='Export data'
|
||||
/>
|
||||
);
|
||||
|
||||
const checkboxText = (
|
||||
<FormattedMessage
|
||||
id='export_user_data_modal.do_not_show'
|
||||
defaultMessage='Do not show this again'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonText={exportDataButton}
|
||||
showCheckbox={true}
|
||||
checkboxText={checkboxText}
|
||||
checkboxInFooter={true}
|
||||
onConfirm={onConfirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {ReportDuration} from '@mattermost/types/reports';
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {startUsersBatchExport} from 'actions/views/admin';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {LicenseSkus, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import {ExportErrorModal} from './export_error_modal';
|
||||
import {ExportUserDataModal} from './export_user_data_modal';
|
||||
import {UpgradeExportDataModal} from './upgrade_export_data_modal';
|
||||
|
||||
import './system_users_export.scss';
|
||||
|
||||
interface Props {
|
||||
currentUserId: UserProfile['id'];
|
||||
dateRange: ReportDuration;
|
||||
}
|
||||
|
||||
export function SystemUsersExport(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const skipDialog = useSelector((state: GlobalState) => get(state, Preferences.CATEGORY_REPORTING, Preferences.HIDE_BATCH_EXPORT_CONFIRM_MODAL, '')) === 'true';
|
||||
|
||||
const license = useSelector(getLicense);
|
||||
const isLicensed = license.IsLicensed === 'true' && (license.SkuShortName === LicenseSkus.Professional || license.SkuShortName === LicenseSkus.Enterprise);
|
||||
|
||||
async function doExport(checked?: boolean) {
|
||||
const {error} = await dispatch(startUsersBatchExport(props.dateRange));
|
||||
if (error) {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.EXPORT_ERROR_MODAL,
|
||||
dialogType: ExportErrorModal,
|
||||
dialogProps: {error},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
dispatch(savePreferences(props.currentUserId, [{
|
||||
category: Preferences.CATEGORY_REPORTING,
|
||||
name: Preferences.HIDE_BATCH_EXPORT_CONFIRM_MODAL,
|
||||
user_id: props.currentUserId,
|
||||
value: 'true',
|
||||
}]));
|
||||
}
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
if (!isLicensed) {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.UPGRADE_EXPORT_DATA_MODAL,
|
||||
dialogType: UpgradeExportDataModal,
|
||||
dialogProps: {},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipDialog) {
|
||||
doExport();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.EXPORT_USER_DATA_MODAL,
|
||||
dialogType: ExportUserDataModal,
|
||||
dialogProps: {onConfirm: doExport},
|
||||
}));
|
||||
}
|
||||
|
||||
const button = (
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className='btn btn-md btn-tertiary'
|
||||
>
|
||||
<span className='icon icon-download-outline'/>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.exportButton'
|
||||
defaultMessage='Export'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!isLicensed) {
|
||||
return (
|
||||
<>
|
||||
<WithTooltip
|
||||
id='sharedTooltip'
|
||||
title={formatMessage({id: 'admin.system_users.exportButton.notLicensed.title', defaultMessage: 'Professional feature'})}
|
||||
hint={formatMessage({id: 'admin.system_users.exportButton.notLicensed.hint', defaultMessage: 'This feature is available on the professional plan'})}
|
||||
placement='top'
|
||||
>
|
||||
{button}
|
||||
</WithTooltip>
|
||||
<div className='system-users-export__keyIndicator'>
|
||||
<i className='icon icon-key-variant'/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.system-users-export__keyIndicator {
|
||||
position: relative;
|
||||
top: -6px;
|
||||
right: 14px;
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
align-self: flex-start;
|
||||
justify-content: center;
|
||||
background: var(--online-indicator, #3db887);
|
||||
border-radius: 12px;
|
||||
|
||||
i {
|
||||
color: var(--center-channel-bg, #fff);
|
||||
font-size: 12px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.upgrade-export-data-modal__svg-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import WomanUpArrowsAndCloudsSvg from 'components/common/svg_images_components/woman_up_arrows_and_clouds_svg';
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
import './upgrade_export_data_modal.scss';
|
||||
|
||||
type Props = {
|
||||
onExited: () => void;
|
||||
}
|
||||
|
||||
export function UpgradeExportDataModal({onExited}: Props) {
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
|
||||
const confirm = () => {
|
||||
openPricingModal();
|
||||
};
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='upgrade_export_data_modal.title'
|
||||
defaultMessage='Upgrade to export data reports'
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='upgrade_export_data_modal.desc'
|
||||
defaultMessage='Export detailed data reports with ease and analyse user statistics conveniently. Upgrade to the Professional plan to gain access to data export.'
|
||||
/>
|
||||
<div className='upgrade-export-data-modal__svg-image'>
|
||||
<WomanUpArrowsAndCloudsSvg
|
||||
width={250}
|
||||
height={188}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const viewPlansButton = (
|
||||
<FormattedMessage
|
||||
id='upgrade_export_data_modal.view_plans'
|
||||
defaultMessage='View Plans'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonText={viewPlansButton}
|
||||
onConfirm={confirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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 {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {UserFilters} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onChange: ({searchTerm, teamId, filter}: {searchTerm?: string; teamId?: string; filter?: string}) => void;
|
||||
onFilter: ({teamId, filter}: {teamId?: string; filter?: string}) => Promise<void>;
|
||||
};
|
||||
|
||||
function SystemUsersFilterRole(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLSelectElement>) {
|
||||
const filter = e?.target?.value ?? '';
|
||||
props.onChange({filter});
|
||||
props.onFilter({filter});
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span className='system-users__filter-label'>
|
||||
<FormattedMessage
|
||||
id='filtered_user_list.userStatus'
|
||||
defaultMessage='User Status:'
|
||||
/>
|
||||
</span>
|
||||
<select
|
||||
id='selectUserStatus'
|
||||
className='form-control system-users__filter'
|
||||
value={props.value}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value=''>
|
||||
{formatMessage({id: 'admin.system_users.allUsers', defaultMessage: 'All Users'})}
|
||||
</option>
|
||||
<option value={UserFilters.SYSTEM_ADMIN}>
|
||||
{formatMessage({id: 'admin.system_users.system_admin', defaultMessage: 'System Admin'})}
|
||||
</option>
|
||||
<option value={UserFilters.SYSTEM_GUEST}>
|
||||
{formatMessage({id: 'admin.system_users.guest', defaultMessage: 'Guest'})}
|
||||
</option>
|
||||
<option value={UserFilters.ACTIVE}>
|
||||
{formatMessage({id: 'admin.system_users.active', defaultMessage: 'Activated'})}
|
||||
</option>
|
||||
<option value={UserFilters.INACTIVE}>
|
||||
{formatMessage({id: 'admin.system_users.inactive', defaultMessage: 'Deactivated'})}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemUsersFilterRole;
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChangeEvent} from 'react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {SearchUserTeamFilter} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
options?: Team[];
|
||||
value?: string;
|
||||
onChange: ({searchTerm, teamId, filter}: {searchTerm?: string; teamId?: string; filter?: string}) => void;
|
||||
onFilter: ({teamId, filter}: {teamId?: string; filter?: string}) => Promise<void>;
|
||||
};
|
||||
|
||||
function SystemUsersFilterTeam(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLSelectElement>) {
|
||||
const teamId = e?.target?.value ?? '';
|
||||
props.onChange({teamId});
|
||||
props.onFilter({teamId});
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span className='system-users__team-filter-label'>
|
||||
<FormattedMessage
|
||||
id='filtered_user_list.team'
|
||||
defaultMessage='Team:'
|
||||
/>
|
||||
</span>
|
||||
<select
|
||||
className='form-control system-users__team-filter'
|
||||
value={props.value}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={SearchUserTeamFilter.ALL_USERS}>
|
||||
{formatMessage({
|
||||
id: 'admin.system_users.allUsers',
|
||||
defaultMessage: 'All Users',
|
||||
})}
|
||||
</option>
|
||||
<option value={SearchUserTeamFilter.NO_TEAM}>
|
||||
{formatMessage({
|
||||
id: 'admin.system_users.noTeams',
|
||||
defaultMessage: 'No Teams',
|
||||
})}
|
||||
</option>
|
||||
{props.options?.map((team) => (
|
||||
<option
|
||||
key={team.id}
|
||||
value={team.id}
|
||||
>
|
||||
{team.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemUsersFilterTeam;
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {autoUpdate, useClick, useDismiss, useFloating, useInteractions, useRole, FloatingFocusManager, useTransitionStyles, autoPlacement, offset} from '@floating-ui/react';
|
||||
import React, {useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {setAdminConsoleUsersManagementTableProperties} from 'actions/views/admin';
|
||||
|
||||
import {StyledPopoverContainer} from 'components/styled_popover_container';
|
||||
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
import {SystemUsersFiltersStatus} from './styled_users_filters_status';
|
||||
import {SystemUsersFilterRole} from './system_users_filter_role';
|
||||
import {SystemUsersFilterTeam} from './system_users_filter_team';
|
||||
|
||||
import './system_users_filter_popover.scss';
|
||||
import {RoleFilters, StatusFilter, TeamFilters} from '../constants';
|
||||
|
||||
type FiltersState = Partial<Pick<AdminConsoleUserManagementTableProperties, 'filterTeam' | 'filterTeamLabel' | 'filterRole' | 'filterStatus'>>;
|
||||
|
||||
interface Props {
|
||||
filterTeam: AdminConsoleUserManagementTableProperties['filterTeam'];
|
||||
filterTeamLabel: AdminConsoleUserManagementTableProperties['filterTeamLabel'];
|
||||
filterRole: AdminConsoleUserManagementTableProperties['filterRole'];
|
||||
filterStatus: AdminConsoleUserManagementTableProperties['filterStatus'];
|
||||
}
|
||||
|
||||
export function SystemUsersFilterPopover(props: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [isPopoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
const [filterState, setFilterState] = useState<FiltersState>({});
|
||||
|
||||
const {context: floatingContext, refs: floatingRefs, floatingStyles} = useFloating({
|
||||
open: isPopoverOpen,
|
||||
onOpenChange: setPopoverOpen,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [
|
||||
offset(10),
|
||||
autoPlacement({
|
||||
allowedPlacements: ['bottom-start', 'top-start'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
const {isMounted, styles: floatingTransistionStyles} = useTransitionStyles(floatingContext);
|
||||
const floatingContextClick = useClick(floatingContext);
|
||||
const floatingContextDismiss = useDismiss(floatingContext);
|
||||
const floatingContextRole = useRole(floatingContext);
|
||||
const {getReferenceProps, getFloatingProps} = useInteractions([
|
||||
floatingContextClick,
|
||||
floatingContextDismiss,
|
||||
floatingContextRole,
|
||||
]);
|
||||
|
||||
function handleTeamFilterChange(teamFilter: string, teamFilterLabel?: string) {
|
||||
let filterTeam;
|
||||
let filterTeamLabel;
|
||||
if (teamFilter === TeamFilters.AllTeams) {
|
||||
filterTeam = '';
|
||||
filterTeamLabel = '';
|
||||
} else if (teamFilter === TeamFilters.NoTeams) {
|
||||
filterTeam = TeamFilters.NoTeams;
|
||||
filterTeamLabel = '';
|
||||
} else {
|
||||
filterTeam = teamFilter;
|
||||
|
||||
// We need to set the label for selected team,
|
||||
// since we might not have the selected team label in the list of teams when we are navigating back from the user details page.
|
||||
filterTeamLabel = teamFilterLabel;
|
||||
}
|
||||
|
||||
setFilterState({...filterState, filterTeam, filterTeamLabel});
|
||||
}
|
||||
|
||||
function handleRoleFilterChange(roleFilter: string) {
|
||||
let filterRole = '';
|
||||
if (roleFilter === RoleFilters.Admin) {
|
||||
filterRole = RoleFilters.Admin;
|
||||
} else if (roleFilter === RoleFilters.Member) {
|
||||
filterRole = RoleFilters.Member;
|
||||
} else if (roleFilter === RoleFilters.Guest) {
|
||||
filterRole = RoleFilters.Guest;
|
||||
}
|
||||
|
||||
setFilterState({...filterState, filterRole});
|
||||
}
|
||||
|
||||
function handleStatusFilterChange(statusFilter: string) {
|
||||
let filterStatus = '';
|
||||
if (statusFilter === StatusFilter.Active) {
|
||||
filterStatus = 'active';
|
||||
} else if (statusFilter === StatusFilter.Deactivated) {
|
||||
filterStatus = 'deactivated';
|
||||
}
|
||||
|
||||
setFilterState({...filterState, filterStatus});
|
||||
}
|
||||
|
||||
function handleApplyFilters() {
|
||||
dispatch(setAdminConsoleUsersManagementTableProperties(filterState));
|
||||
setPopoverOpen(false);
|
||||
}
|
||||
|
||||
const filterStatusApplied = props.filterStatus.length > 0 ? 1 : 0;
|
||||
const filterRoleApplied = props.filterRole.length > 0 ? 1 : 0;
|
||||
const filterTeamApplied = props.filterTeam.length > 0 ? 1 : 0;
|
||||
const filtersCount = filterStatusApplied + filterRoleApplied + filterTeamApplied;
|
||||
|
||||
return (
|
||||
<div className='systemUsersFilterContainer'>
|
||||
<button
|
||||
{...getReferenceProps()}
|
||||
ref={floatingRefs.setReference}
|
||||
className='btn btn-md btn-tertiary'
|
||||
aria-controls='systemUsersFilterPopover'
|
||||
>
|
||||
<i className='icon icon-filter-variant'/>
|
||||
{formatMessage({id: 'admin.system_users.filtersMenu', defaultMessage: 'Filters ({count})'}, {count: filtersCount})}
|
||||
</button>
|
||||
{isMounted && (
|
||||
<FloatingFocusManager
|
||||
context={floatingContext}
|
||||
>
|
||||
<StyledPopoverContainer
|
||||
{...getFloatingProps()}
|
||||
id='systemUsersFilterPopover'
|
||||
ref={floatingRefs.setFloating}
|
||||
style={Object.assign({}, floatingStyles, floatingTransistionStyles)}
|
||||
className='systemUsersFilterPopoverContainer'
|
||||
aria-labelledby='header'
|
||||
>
|
||||
<h4 id='header'>
|
||||
{formatMessage({id: 'admin.system_users.filtersPopover.title', defaultMessage: 'Filter by'})}
|
||||
</h4>
|
||||
<div className='body'>
|
||||
<SystemUsersFilterTeam
|
||||
initialValue={props.filterTeam}
|
||||
initialLabel={props.filterTeamLabel}
|
||||
onChange={handleTeamFilterChange}
|
||||
/>
|
||||
<SystemUsersFilterRole
|
||||
initialValue={props.filterRole}
|
||||
onChange={handleRoleFilterChange}
|
||||
/>
|
||||
<SystemUsersFiltersStatus
|
||||
initialValue={props.filterStatus}
|
||||
onChange={handleStatusFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='footer'>
|
||||
<button
|
||||
className='btn btn-md btn-primary'
|
||||
onClick={handleApplyFilters}
|
||||
type='submit'
|
||||
>
|
||||
{formatMessage({id: 'admin.system_users.filtersPopover.apply', defaultMessage: 'Apply'})}
|
||||
</button>
|
||||
</div>
|
||||
</StyledPopoverContainer>
|
||||
</FloatingFocusManager>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import DropdownInput from 'components/dropdown_input';
|
||||
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
import {StatusFilter} from '../../constants';
|
||||
import {getDefaultSelectedValueFromList} from '../../utils';
|
||||
|
||||
type OptionType = {
|
||||
label: string;
|
||||
value: StatusFilter;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
initialValue: AdminConsoleUserManagementTableProperties['filterStatus'];
|
||||
onChange: (value: AdminConsoleUserManagementTableProperties['filterStatus']) => void;
|
||||
}
|
||||
|
||||
export function SystemUsersFiltersStatus(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: StatusFilter.Any,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.status.any',
|
||||
defaultMessage: 'Any',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: StatusFilter.Active,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.status.active',
|
||||
defaultMessage: 'Activated users',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: StatusFilter.Deactivated,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.status.deactive',
|
||||
defaultMessage: 'Deactivated users',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [value, setValue] = useState(() => getDefaultSelectedValueFromList(props.initialValue, options));
|
||||
|
||||
function handleChange(value: OptionType) {
|
||||
setValue(value);
|
||||
|
||||
props.onChange(value.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownInput<OptionType>
|
||||
name='filterStatus'
|
||||
showLegend={true}
|
||||
isSearchable={false}
|
||||
legend={formatMessage({id: 'admin.system_users.filters.status.title', defaultMessage: 'Status'})}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.systemUsersFilterContainer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.systemUsersFilterPopoverContainer {
|
||||
& > #header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
|
||||
margin: unset;
|
||||
color: var(--sys-center-channel-color);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
& > .body {
|
||||
width: 280px;
|
||||
padding: 20px 24px 0 20px;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.DropdownInput.Input_container {
|
||||
margin-top: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
height: unset;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: 20px;
|
||||
margin-inline-end: 24px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import DropdownInput from 'components/dropdown_input';
|
||||
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
import {RoleFilters} from '../../constants';
|
||||
import {getDefaultSelectedValueFromList} from '../../utils';
|
||||
|
||||
type OptionType = {
|
||||
label: string;
|
||||
value: RoleFilters;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
initialValue: AdminConsoleUserManagementTableProperties['filterRole'];
|
||||
onChange: (value: AdminConsoleUserManagementTableProperties['filterRole']) => void;
|
||||
};
|
||||
|
||||
export function SystemUsersFilterRole(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: RoleFilters.Any,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.role.any',
|
||||
defaultMessage: 'Any',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: RoleFilters.Admin,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.role.system_admin',
|
||||
defaultMessage: 'System Admin',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: RoleFilters.Member,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.role.system_user',
|
||||
defaultMessage: 'Member',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: RoleFilters.Guest,
|
||||
label: formatMessage({
|
||||
id: 'admin.system_users.filters.role.system_guest',
|
||||
defaultMessage: 'Guest',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [value, setValue] = useState(() => getDefaultSelectedValueFromList(props.initialValue, options));
|
||||
|
||||
function handleChange(value: OptionType) {
|
||||
setValue(value);
|
||||
|
||||
props.onChange(value.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownInput<OptionType>
|
||||
name='filterRole'
|
||||
showLegend={true}
|
||||
isSearchable={false}
|
||||
legend={formatMessage({id: 'admin.system_users.filters.role.title', defaultMessage: 'Role'})}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.DropdownInput {
|
||||
.Input_wrapper {
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.Input {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
&.Input___focus .a11y--focused {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Input_legend {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.asyncTeamSelectInput__controlContainer {
|
||||
.DropDown__control {
|
||||
background-color: var(--center-channel-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.DropDown__menu {
|
||||
background-color: var(--center-channel-bg) !important;
|
||||
}
|
||||
|
||||
.DropDown__single-value {
|
||||
color: var(--center-channel-color) !important;
|
||||
}
|
||||
|
||||
.asyncTeamSelectInput__indicatorsContainer {
|
||||
margin-right: 8px;
|
||||
|
||||
i {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-weight: normal;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asyncTeamSelectInput__option > div {
|
||||
padding: 10px 24px;
|
||||
cursor: pointer;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.asyncTeamSelectInput__option.selected > div {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
|
||||
.asyncTeamSelectInput__option.focused > div {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import type {CSSProperties, ReactElement} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {components} from 'react-select';
|
||||
import type {IndicatorContainerProps, ControlProps, OptionProps, OptionsType, ValueType, StylesConfig} from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
import type {Team, TeamSearchOpts} from '@mattermost/types/teams';
|
||||
|
||||
import {getTeams, searchTeams} from 'mattermost-redux/actions/teams';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import InputError from 'components/input_error';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
import {TeamFilters} from '../../constants';
|
||||
import {getDefaultSelectedTeam} from '../../utils';
|
||||
|
||||
import './async_team_select.scss';
|
||||
|
||||
const TEAMS_PER_PAGE = 50;
|
||||
|
||||
export type OptionType = {
|
||||
label: string | ReactElement;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
error?: string;
|
||||
initialValue: Team['id'];
|
||||
initialLabel?: string;
|
||||
onChange: (value: Team['id'], label?: string) => void;
|
||||
}
|
||||
|
||||
export function SystemUsersFilterTeam(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [list, setList] = useState<OptionsType<OptionType>>();
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [value, setValue] = useState<ValueType<OptionType>>(getDefaultSelectedTeam(props.initialValue, props.initialLabel));
|
||||
|
||||
async function loadListInPageNumber(page: number) {
|
||||
try {
|
||||
const response = await dispatch(getTeams(page, TEAMS_PER_PAGE, true)) as ActionResult<{teams: Team[]}>;
|
||||
if (response && response.data && response.data.teams && response.data.teams.length > 0) {
|
||||
const list = response.data.teams.
|
||||
map((team: Team) => ({
|
||||
value: team.id,
|
||||
label: team.display_name,
|
||||
})).
|
||||
sort((a: OptionType, b: OptionType) => (a.label as string).localeCompare(b.label as string));
|
||||
|
||||
if (page === 0) {
|
||||
const initialOptions = [
|
||||
{
|
||||
label: formatMessage({id: 'admin.system_users.filters.team.allTeams', defaultMessage: 'All teams'}),
|
||||
value: TeamFilters.AllTeams,
|
||||
},
|
||||
{
|
||||
label: formatMessage({id: 'admin.system_users.filters.team.noTeams', defaultMessage: 'No teams'}),
|
||||
value: TeamFilters.NoTeams,
|
||||
},
|
||||
];
|
||||
setList([...initialOptions, ...list]);
|
||||
} else {
|
||||
setList((existingList) => [...(existingList ?? []), ...list]);
|
||||
}
|
||||
|
||||
setPageNumber(page + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(formatMessage({id: 'admin.system_users.filters.team.errorLoading', defaultMessage: 'Error while loading teams'}));
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
async function searchInList(term: string, callBack: (options: OptionsType<{label: string; value: string}>) => void) {
|
||||
try {
|
||||
const response = await dispatch(searchTeams(term, {page: 0, per_page: TEAMS_PER_PAGE} as TeamSearchOpts));
|
||||
if (response && response.data && response.data.teams && response.data.teams.length > 0) {
|
||||
const teams = response.data.teams.map((team: Team) => ({
|
||||
value: team.id,
|
||||
label: team.display_name,
|
||||
}));
|
||||
|
||||
callBack(teams);
|
||||
}
|
||||
|
||||
callBack([]);
|
||||
} catch (error) {
|
||||
setError(formatMessage({id: 'admin.system_users.filters.team.errorSearching', defaultMessage: 'Error while searching teams'}));
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
callBack([]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMenuScrolledToBottom() {
|
||||
loadListInPageNumber(pageNumber);
|
||||
}
|
||||
|
||||
function handleOnChange(value: ValueType<OptionType>) {
|
||||
setValue(value);
|
||||
props.onChange((value as OptionType).value as string, (value as OptionType).label as string);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadListInPageNumber(0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className='DropdownInput Input_container'
|
||||
>
|
||||
<fieldset
|
||||
className={classNames('Input_fieldset Input_fieldset___legend', props.className, {
|
||||
Input_fieldset___error: props.error || error,
|
||||
})}
|
||||
>
|
||||
<legend className='Input_legend Input_legend___focus'>
|
||||
{formatMessage({id: 'admin.system_users.filters.team.title', defaultMessage: 'Team'})}
|
||||
</legend>
|
||||
<div className='Input_wrapper'>
|
||||
<AsyncSelect
|
||||
id='asyncTeamSelect'
|
||||
inputId='asyncTeamSelectInput'
|
||||
classNamePrefix='DropDown'
|
||||
className={classNames('Input Input__focus', props.className)}
|
||||
styles={styles}
|
||||
isMulti={false}
|
||||
isClearable={false}
|
||||
hideSelectedOptions={false}
|
||||
cacheOptions={false}
|
||||
placeholder={''} // Since we have a default value, we don't need placeholder
|
||||
loadingMessage={() => formatMessage({id: 'admin.channels.filterBy.team.loading', defaultMessage: 'Loading teams'})}
|
||||
noOptionsMessage={() => formatMessage({id: 'admin.channels.filterBy.team.noTeams', defaultMessage: 'No teams found'})}
|
||||
loadOptions={searchInList}
|
||||
defaultOptions={list}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
onMenuScrollToBottom={handleMenuScrolledToBottom}
|
||||
components={{
|
||||
IndicatorsContainer,
|
||||
LoadingIndicator,
|
||||
Option,
|
||||
Control,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<InputError
|
||||
message={props.error || error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Partial<StylesConfig> = {
|
||||
input: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
color: 'var(--center-channel-color)',
|
||||
}),
|
||||
control: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
padding: '0 2px',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
indicatorSeparator: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
display: 'none',
|
||||
}),
|
||||
menu: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
zIndex: 100,
|
||||
}),
|
||||
menuPortal: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
zIndex: 100,
|
||||
}),
|
||||
};
|
||||
|
||||
const IndicatorsContainer = (props: IndicatorContainerProps<OptionType>) => {
|
||||
return (
|
||||
<div className='asyncTeamSelectInput__indicatorsContainer'>
|
||||
<components.IndicatorsContainer {...props}>
|
||||
<i className='icon icon-chevron-down'/>
|
||||
</components.IndicatorsContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Control = (props: ControlProps<OptionType>) => {
|
||||
return (
|
||||
<div className='asyncTeamSelectInput__controlContainer'>
|
||||
<components.Control {...props}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Option = (props: OptionProps<OptionType>) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('asyncTeamSelectInput__option', {
|
||||
selected: props.isSelected,
|
||||
focused: props.isFocused,
|
||||
})}
|
||||
>
|
||||
<components.Option {...props}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingIndicator = () => {
|
||||
return (
|
||||
<LoadingSpinner/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/admin_console/system_users/list should match default snapshot 1`] = `
|
||||
<div>
|
||||
<injectIntl(SearchableUserList)
|
||||
actionProps={
|
||||
Object {
|
||||
"doEmailReset": [Function],
|
||||
"doManageRoles": [Function],
|
||||
"doManageTeams": [Function],
|
||||
"doManageTokens": [Function],
|
||||
"doPasswordReset": [Function],
|
||||
"enableUserAccessTokens": false,
|
||||
"experimentalEnableAuthenticationTransfer": false,
|
||||
"mfaEnabled": false,
|
||||
}
|
||||
}
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
enableUserAccessTokens={false}
|
||||
experimentalEnableAuthenticationTransfer={false}
|
||||
extraInfo={Object {}}
|
||||
filter=""
|
||||
focusOnMount={false}
|
||||
isDisabled={false}
|
||||
mfaEnabled={false}
|
||||
nextPage={[Function]}
|
||||
noBuiltInFilters={true}
|
||||
onTermChange={[MockFunction]}
|
||||
page={0}
|
||||
previousPage={[Function]}
|
||||
renderCount={[Function]}
|
||||
renderFilterRow={[MockFunction]}
|
||||
rowComponentType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
search={[MockFunction]}
|
||||
teamId=""
|
||||
term=""
|
||||
total={0}
|
||||
users={Array []}
|
||||
usersPerPage={0}
|
||||
/>
|
||||
<Connect(ManageTeamsModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ManageRolesModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ManageTokensModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ResetPasswordModal)
|
||||
onModalDismissed={[Function]}
|
||||
onModalSubmit={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ResetEmailModal)
|
||||
onModalDismissed={[Function]}
|
||||
onModalSubmit={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/system_users/list should match default snapshot, with users and mfa disabled 1`] = `
|
||||
<div>
|
||||
<injectIntl(SearchableUserList)
|
||||
actionProps={
|
||||
Object {
|
||||
"doEmailReset": [Function],
|
||||
"doManageRoles": [Function],
|
||||
"doManageTeams": [Function],
|
||||
"doManageTokens": [Function],
|
||||
"doPasswordReset": [Function],
|
||||
"enableUserAccessTokens": false,
|
||||
"experimentalEnableAuthenticationTransfer": false,
|
||||
"mfaEnabled": false,
|
||||
}
|
||||
}
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
enableUserAccessTokens={false}
|
||||
experimentalEnableAuthenticationTransfer={false}
|
||||
extraInfo={
|
||||
Object {
|
||||
"id1": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** Email"
|
||||
id="admin.user_item.authServiceEmail"
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id1",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
],
|
||||
"id2": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** Email"
|
||||
id="admin.user_item.authServiceEmail"
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id2",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
],
|
||||
"id3": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** {service}"
|
||||
id="admin.user_item.authServiceNotEmail"
|
||||
values={
|
||||
Object {
|
||||
"service": "LDAP",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id3",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
],
|
||||
"id4": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** {service}"
|
||||
id="admin.user_item.authServiceNotEmail"
|
||||
values={
|
||||
Object {
|
||||
"service": "SAML",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id4",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
],
|
||||
"id5": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** {service}"
|
||||
id="admin.user_item.authServiceNotEmail"
|
||||
values={
|
||||
Object {
|
||||
"service": "Other Service",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id5",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
],
|
||||
}
|
||||
}
|
||||
filter=""
|
||||
focusOnMount={false}
|
||||
isDisabled={false}
|
||||
mfaEnabled={false}
|
||||
nextPage={[Function]}
|
||||
noBuiltInFilters={true}
|
||||
onTermChange={[MockFunction]}
|
||||
page={0}
|
||||
previousPage={[Function]}
|
||||
renderCount={[Function]}
|
||||
renderFilterRow={[MockFunction]}
|
||||
rowComponentType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
search={[MockFunction]}
|
||||
teamId=""
|
||||
term=""
|
||||
total={0}
|
||||
users={
|
||||
Array [
|
||||
Object {
|
||||
"id": "id1",
|
||||
},
|
||||
Object {
|
||||
"id": "id2",
|
||||
},
|
||||
Object {
|
||||
"auth_service": "ldap",
|
||||
"id": "id3",
|
||||
},
|
||||
Object {
|
||||
"auth_service": "saml",
|
||||
"id": "id4",
|
||||
},
|
||||
Object {
|
||||
"auth_service": "other service",
|
||||
"id": "id5",
|
||||
},
|
||||
]
|
||||
}
|
||||
usersPerPage={0}
|
||||
/>
|
||||
<Connect(ManageTeamsModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ManageRolesModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ManageTokensModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ResetPasswordModal)
|
||||
onModalDismissed={[Function]}
|
||||
onModalSubmit={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ResetEmailModal)
|
||||
onModalDismissed={[Function]}
|
||||
onModalSubmit={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/admin_console/system_users/list should match default snapshot, with users and mfa enabled 1`] = `
|
||||
<div>
|
||||
<injectIntl(SearchableUserList)
|
||||
actionProps={
|
||||
Object {
|
||||
"doEmailReset": [Function],
|
||||
"doManageRoles": [Function],
|
||||
"doManageTeams": [Function],
|
||||
"doManageTokens": [Function],
|
||||
"doPasswordReset": [Function],
|
||||
"enableUserAccessTokens": false,
|
||||
"experimentalEnableAuthenticationTransfer": false,
|
||||
"mfaEnabled": true,
|
||||
}
|
||||
}
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
enableUserAccessTokens={false}
|
||||
experimentalEnableAuthenticationTransfer={false}
|
||||
extraInfo={
|
||||
Object {
|
||||
"id1": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** Email"
|
||||
id="admin.user_item.authServiceEmail"
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id1",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**MFA**: No"
|
||||
id="admin.user_item.mfaNo"
|
||||
/>,
|
||||
],
|
||||
"id2": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** Email"
|
||||
id="admin.user_item.authServiceEmail"
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id2",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**MFA**: No"
|
||||
id="admin.user_item.mfaNo"
|
||||
/>,
|
||||
],
|
||||
"id3": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** {service}"
|
||||
id="admin.user_item.authServiceNotEmail"
|
||||
values={
|
||||
Object {
|
||||
"service": "LDAP",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id3",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**MFA**: No"
|
||||
id="admin.user_item.mfaNo"
|
||||
/>,
|
||||
],
|
||||
"id4": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** {service}"
|
||||
id="admin.user_item.authServiceNotEmail"
|
||||
values={
|
||||
Object {
|
||||
"service": "SAML",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id4",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**MFA**: No"
|
||||
id="admin.user_item.mfaNo"
|
||||
/>,
|
||||
],
|
||||
"id5": Array [
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**Sign-in Method:** {service}"
|
||||
id="admin.user_item.authServiceNotEmail"
|
||||
values={
|
||||
Object {
|
||||
"service": "Other Service",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**User ID:** {userID}"
|
||||
id="admin.user_item.user_id"
|
||||
values={
|
||||
Object {
|
||||
"userID": "id5",
|
||||
}
|
||||
}
|
||||
/>,
|
||||
", ",
|
||||
<FormattedMarkdownMessage
|
||||
defaultMessage="**MFA**: No"
|
||||
id="admin.user_item.mfaNo"
|
||||
/>,
|
||||
],
|
||||
}
|
||||
}
|
||||
filter=""
|
||||
focusOnMount={false}
|
||||
isDisabled={false}
|
||||
mfaEnabled={true}
|
||||
nextPage={[Function]}
|
||||
noBuiltInFilters={true}
|
||||
onTermChange={[MockFunction]}
|
||||
page={0}
|
||||
previousPage={[Function]}
|
||||
renderCount={[Function]}
|
||||
renderFilterRow={[MockFunction]}
|
||||
rowComponentType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
search={[MockFunction]}
|
||||
teamId=""
|
||||
term=""
|
||||
total={0}
|
||||
users={
|
||||
Array [
|
||||
Object {
|
||||
"id": "id1",
|
||||
},
|
||||
Object {
|
||||
"id": "id2",
|
||||
},
|
||||
Object {
|
||||
"auth_service": "ldap",
|
||||
"id": "id3",
|
||||
},
|
||||
Object {
|
||||
"auth_service": "saml",
|
||||
"id": "id4",
|
||||
},
|
||||
Object {
|
||||
"auth_service": "other service",
|
||||
"id": "id5",
|
||||
},
|
||||
]
|
||||
}
|
||||
usersPerPage={0}
|
||||
/>
|
||||
<Connect(ManageTeamsModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ManageRolesModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ManageTokensModal)
|
||||
onModalDismissed={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ResetPasswordModal)
|
||||
onModalDismissed={[Function]}
|
||||
onModalSubmit={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
<Connect(ResetEmailModal)
|
||||
onModalDismissed={[Function]}
|
||||
onModalSubmit={[Function]}
|
||||
show={false}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getUser} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {getNonBotUsers} from './selectors';
|
||||
import SystemUsersList from './system_users_list';
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
teamId: string;
|
||||
term: string;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: Props) {
|
||||
const users = getNonBotUsers(state, ownProps.loading, ownProps.teamId, ownProps.term, ownProps.filter);
|
||||
return {
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getUser,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SystemUsersList);
|
||||
@@ -1,176 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import * as users from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getUsers} from 'components/admin_console/system_users/system_users_list/selectors';
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/users');
|
||||
|
||||
describe('components/admin_console/system_users/list/selectors', () => {
|
||||
const state = {} as GlobalState;
|
||||
|
||||
test('should return no users when loading', () => {
|
||||
const loading = true;
|
||||
const teamId = 'teamId';
|
||||
const term = 'term';
|
||||
const filter = '';
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual([]);
|
||||
});
|
||||
|
||||
describe('should search by term', () => {
|
||||
const loading = false;
|
||||
|
||||
describe('over all profiles', () => {
|
||||
const teamId = '';
|
||||
const filter = '';
|
||||
|
||||
it('returning users users', () => {
|
||||
const term = 'term';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}, {id: 'id2'}];
|
||||
(users.makeSearchProfilesStartingWithTerm as jest.Mock).mockImplementation(() => jest.fn().mockReturnValue(expectedUsers));
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
});
|
||||
|
||||
describe('falling back to fetching user by id', () => {
|
||||
const term = 'x'.repeat(26);
|
||||
|
||||
it('and the user is found', () => {
|
||||
const expectedUsers = [{id: 'id1'}];
|
||||
(users.makeSearchProfilesStartingWithTerm as jest.Mock).mockImplementation(() => jest.fn().mockReturnValue([]));
|
||||
|
||||
(users.getUser as jest.Mock).mockReturnValue(expectedUsers[0]);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.getUser).toBeCalledWith(state, term);
|
||||
});
|
||||
|
||||
it('and the user is not found', () => {
|
||||
const expectedUsers = [] as UserProfile[];
|
||||
(users.makeSearchProfilesStartingWithTerm as jest.Mock).mockImplementation(() => jest.fn().mockReturnValue([]));
|
||||
(users.getUser as jest.Mock).mockReturnValue(null);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.getUser).toBeCalledWith(state, term);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and team id', () => {
|
||||
const teamId = 'teamId';
|
||||
const filter = '';
|
||||
|
||||
it('returning users users found in team', () => {
|
||||
const term = 'term';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}, {id: 'id2'}];
|
||||
(users.searchProfilesInTeam as jest.Mock).mockReturnValue(expectedUsers);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.searchProfilesInTeam).toBeCalledWith(state, teamId, term, false, {});
|
||||
});
|
||||
|
||||
describe('falling back to fetching user by id', () => {
|
||||
const term = 'x'.repeat(26);
|
||||
|
||||
it('and the user is found', () => {
|
||||
const expectedUsers = [{id: 'id1'}];
|
||||
(users.searchProfilesInTeam as jest.Mock).mockReturnValue([]);
|
||||
(users.getUser as jest.Mock).mockReturnValue(expectedUsers[0]);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.searchProfilesInTeam).toBeCalledWith(state, teamId, term, false, {});
|
||||
expect(users.getUser).toBeCalledWith(state, term);
|
||||
});
|
||||
|
||||
it('and the user is not found', () => {
|
||||
const expectedUsers = [] as UserProfile[];
|
||||
(users.searchProfilesInTeam as jest.Mock).mockReturnValue([]);
|
||||
(users.getUser as jest.Mock).mockReturnValue(null);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.searchProfilesInTeam).toBeCalledWith(state, teamId, term, false, {});
|
||||
expect(users.getUser).toBeCalledWith(state, term);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return', () => {
|
||||
const loading = false;
|
||||
const term = '';
|
||||
const filter = '';
|
||||
|
||||
it('all profiles', () => {
|
||||
const teamId = '';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}, {id: 'id2'}];
|
||||
(users.getProfiles as jest.Mock).mockReturnValue(expectedUsers);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.getProfiles).toBeCalledWith(state, {});
|
||||
});
|
||||
|
||||
it('profiles without a team', () => {
|
||||
const teamId = 'no_team';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}, {id: 'id2'}];
|
||||
(users.getProfilesWithoutTeam as jest.Mock).mockReturnValue(expectedUsers);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.getProfilesWithoutTeam).toBeCalledWith(state, {});
|
||||
});
|
||||
|
||||
it('profiles for the given team', () => {
|
||||
const teamId = 'team_id1';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}, {id: 'id2'}];
|
||||
(users.getProfilesInTeam as jest.Mock).mockReturnValue(expectedUsers);
|
||||
expect(getUsers(state, loading, teamId, term, filter)).toEqual(expectedUsers);
|
||||
expect(users.getProfilesInTeam).toBeCalledWith(state, teamId, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters', () => {
|
||||
const loading = false;
|
||||
const term = '';
|
||||
const systemAdmin = 'system_admin';
|
||||
const roleFilter = {role: 'system_admin'};
|
||||
const inactiveFilter = {inactive: true};
|
||||
const inactive = 'inactive';
|
||||
|
||||
it('all profiles with system admin', () => {
|
||||
const teamId = '';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}];
|
||||
(users.getProfiles as jest.Mock).mockReturnValue(expectedUsers);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, systemAdmin)).toEqual(expectedUsers);
|
||||
expect(users.getProfiles).toBeCalledWith(state, roleFilter);
|
||||
});
|
||||
|
||||
it('inactive profiles without a team', () => {
|
||||
const teamId = 'no_team';
|
||||
|
||||
const expectedUsers = [{id: 'id1'}, {id: 'id2'}];
|
||||
(users.getProfilesWithoutTeam as jest.Mock).mockReturnValue(expectedUsers);
|
||||
|
||||
expect(getUsers(state, loading, teamId, term, inactive)).toEqual(expectedUsers);
|
||||
expect(users.getProfilesWithoutTeam).toBeCalledWith(state, inactiveFilter);
|
||||
});
|
||||
|
||||
it('system admin profiles for the given team', () => {
|
||||
const teamId = 'team_id1';
|
||||
|
||||
const expectedUsers = [{id: 'id2'}];
|
||||
(users.getProfilesInTeam as jest.Mock).mockReturnValue(expectedUsers);
|
||||
expect(getUsers(state, loading, teamId, term, systemAdmin)).toEqual(expectedUsers);
|
||||
expect(users.getProfilesInTeam).toBeCalledWith(state, teamId, roleFilter);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {getUser, getProfiles, getProfilesInTeam, getProfilesWithoutTeam, makeSearchProfilesStartingWithTerm, searchProfilesInTeam} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {userSelectorOptionsFromFilter} from 'utils/filter_users';
|
||||
|
||||
const ALL_USERS = '';
|
||||
const NO_TEAM = 'no_team';
|
||||
const USER_ID_LENGTH = 26;
|
||||
|
||||
export function getUsers(state: GlobalState, loading: boolean, teamId: string, term: string, filter: string): UserProfile[] {
|
||||
if (loading) {
|
||||
// Show no users while loading.
|
||||
return [];
|
||||
}
|
||||
const searchProfilesStartingWithTerm = makeSearchProfilesStartingWithTerm();
|
||||
|
||||
const filters = userSelectorOptionsFromFilter(filter);
|
||||
if (term) {
|
||||
let users = [];
|
||||
if (teamId) {
|
||||
users = searchProfilesInTeam(state, teamId, term, false, filters);
|
||||
} else {
|
||||
users = searchProfilesStartingWithTerm(state, term, false, filters);
|
||||
}
|
||||
|
||||
if (users.length === 0 && term.length === USER_ID_LENGTH) {
|
||||
const user = getUser(state, term);
|
||||
if (user) {
|
||||
users = [user];
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
if (teamId === ALL_USERS) {
|
||||
return getProfiles(state, filters);
|
||||
} else if (teamId === NO_TEAM) {
|
||||
return getProfilesWithoutTeam(state, filters);
|
||||
}
|
||||
|
||||
return getProfilesInTeam(state, teamId, filters);
|
||||
}
|
||||
|
||||
export function getNonBotUsers(state: GlobalState, loading: boolean, teamId: string, term: string, filter: string): UserProfile[] {
|
||||
return getUsers(state, loading, teamId, term, filter).filter((user) => {
|
||||
return !user.is_bot;
|
||||
});
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import SystemUsersList from 'components/admin_console/system_users/system_users_list/system_users_list';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
describe('components/admin_console/system_users/list', () => {
|
||||
const defaultProps = {
|
||||
users: [] as UserProfile[],
|
||||
usersPerPage: 0,
|
||||
total: 0,
|
||||
nextPage: jest.fn(),
|
||||
search: jest.fn(),
|
||||
focusOnMount: false,
|
||||
renderFilterRow: jest.fn(),
|
||||
teamId: '',
|
||||
filter: '',
|
||||
term: '',
|
||||
onTermChange: jest.fn(),
|
||||
mfaEnabled: false,
|
||||
enableUserAccessTokens: false,
|
||||
experimentalEnableAuthenticationTransfer: false,
|
||||
actions: {
|
||||
getUser: jest.fn(),
|
||||
updateTeamMemberSchemeRoles: jest.fn(),
|
||||
getTeamMembersForUser: jest.fn(),
|
||||
getTeamsForUser: jest.fn(),
|
||||
removeUserFromTeam: jest.fn(),
|
||||
},
|
||||
isDisabled: false,
|
||||
};
|
||||
|
||||
test('should match default snapshot', () => {
|
||||
const props = defaultProps;
|
||||
const wrapper = shallow(<SystemUsersList {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('should match default snapshot, with users', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
users: [
|
||||
{id: 'id1'},
|
||||
{id: 'id2'},
|
||||
{id: 'id3', auth_service: Constants.LDAP_SERVICE},
|
||||
{id: 'id4', auth_service: Constants.SAML_SERVICE},
|
||||
{id: 'id5', auth_service: 'other service'},
|
||||
] as UserProfile[],
|
||||
};
|
||||
|
||||
it('and mfa enabled', () => {
|
||||
const wrapper = shallow(
|
||||
<SystemUsersList
|
||||
{...props}
|
||||
mfaEnabled={true}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('and mfa disabled', () => {
|
||||
const wrapper = shallow(
|
||||
<SystemUsersList
|
||||
{...props}
|
||||
mfaEnabled={false}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should reset page', () => {
|
||||
it('when team changes', () => {
|
||||
const wrapper = shallow(
|
||||
<SystemUsersList {...defaultProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state('page')).toBe(0);
|
||||
(wrapper.instance() as SystemUsersList).nextPage();
|
||||
expect(wrapper.state('page')).toBe(1);
|
||||
wrapper.setProps({...defaultProps, teamId: 'new'});
|
||||
expect(wrapper.state('page')).toBe(0);
|
||||
});
|
||||
|
||||
it('when filter changes', () => {
|
||||
const wrapper = shallow(
|
||||
<SystemUsersList {...defaultProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state('page')).toBe(0);
|
||||
(wrapper.instance() as SystemUsersList).nextPage();
|
||||
expect(wrapper.state('page')).toBe(1);
|
||||
wrapper.setProps({...defaultProps, filter: 'new'});
|
||||
expect(wrapper.state('page')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not reset page', () => {
|
||||
it('when term changes', () => {
|
||||
const wrapper = shallow(
|
||||
<SystemUsersList {...defaultProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state('page')).toBe(0);
|
||||
(wrapper.instance() as SystemUsersList).nextPage();
|
||||
expect(wrapper.state('page')).toBe(1);
|
||||
wrapper.setProps({...defaultProps, term: 'new term'});
|
||||
expect(wrapper.state('page')).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,381 +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 type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import ManageRolesModal from 'components/admin_console/manage_roles_modal';
|
||||
import ManageTeamsModal from 'components/admin_console/manage_teams_modal';
|
||||
import ManageTokensModal from 'components/admin_console/manage_tokens_modal';
|
||||
import ResetEmailModal from 'components/admin_console/reset_email_modal';
|
||||
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import SearchableUserList from 'components/searchable_user_list/searchable_user_list';
|
||||
import UserListRowWithError from 'components/user_list_row_with_error';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import SystemUsersDropdown from '../system_users_dropdown';
|
||||
|
||||
type Props = {
|
||||
users: UserProfile[];
|
||||
teams?: Team[];
|
||||
usersPerPage: number;
|
||||
total: number;
|
||||
nextPage: (page: number) => void;
|
||||
search: (term: string) => void;
|
||||
focusOnMount?: boolean;
|
||||
|
||||
teamId: string;
|
||||
filter: string;
|
||||
term: string;
|
||||
onTermChange: (term: string) => void;
|
||||
|
||||
/**
|
||||
* Whether MFA is licensed and enabled.
|
||||
*/
|
||||
mfaEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not user access tokens are enabled.
|
||||
*/
|
||||
enableUserAccessTokens: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the experimental authentication transfer is enabled.
|
||||
*/
|
||||
experimentalEnableAuthenticationTransfer: boolean;
|
||||
|
||||
actions: {
|
||||
getUser: (id: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type State = {
|
||||
page: number;
|
||||
filter: string;
|
||||
teamId: string;
|
||||
showManageTeamsModal: boolean;
|
||||
showManageRolesModal: boolean;
|
||||
showManageTokensModal: boolean;
|
||||
showPasswordModal: boolean;
|
||||
showEmailModal: boolean;
|
||||
user?: UserProfile;
|
||||
};
|
||||
|
||||
export default class SystemUsersList extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
|
||||
filter: props.filter,
|
||||
teamId: props.teamId,
|
||||
showManageTeamsModal: false,
|
||||
showManageRolesModal: false,
|
||||
showManageTokensModal: false,
|
||||
showPasswordModal: false,
|
||||
showEmailModal: false,
|
||||
user: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State): { page: number; teamId: string; filter: string } | null {
|
||||
if (prevState.teamId !== nextProps.teamId || prevState.filter !== nextProps.filter) {
|
||||
return {
|
||||
page: 0,
|
||||
teamId: nextProps.teamId,
|
||||
filter: nextProps.filter,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
nextPage = () => {
|
||||
this.setState({page: this.state.page + 1});
|
||||
|
||||
this.props.nextPage(this.state.page + 1);
|
||||
};
|
||||
|
||||
previousPage = () => {
|
||||
this.setState({page: this.state.page - 1});
|
||||
};
|
||||
|
||||
search = (term: string) => {
|
||||
this.props.search(term);
|
||||
|
||||
if (term !== '') {
|
||||
this.setState({page: 0});
|
||||
}
|
||||
};
|
||||
|
||||
doManageTeams = (user: UserProfile) => {
|
||||
this.setState({
|
||||
showManageTeamsModal: true,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
doManageRoles = (user: UserProfile) => {
|
||||
this.setState({
|
||||
showManageRolesModal: true,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
doManageTokens = (user: UserProfile) => {
|
||||
this.setState({
|
||||
showManageTokensModal: true,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
doManageTeamsDismiss = () => {
|
||||
this.setState({
|
||||
showManageTeamsModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
doManageRolesDismiss = () => {
|
||||
this.setState({
|
||||
showManageRolesModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
doManageTokensDismiss = () => {
|
||||
this.setState({
|
||||
showManageTokensModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
doPasswordReset = (user: UserProfile) => {
|
||||
this.setState({
|
||||
showPasswordModal: true,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
doPasswordResetDismiss = () => {
|
||||
this.setState({
|
||||
showPasswordModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
doPasswordResetSubmit = (user?: UserProfile) => {
|
||||
if (user) {
|
||||
this.props.actions.getUser(user.id);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showPasswordModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
doEmailReset = (user: UserProfile) => {
|
||||
this.setState({
|
||||
showEmailModal: true,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
doEmailResetDismiss = () => {
|
||||
this.setState({
|
||||
showEmailModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
doEmailResetSubmit = (user?: UserProfile) => {
|
||||
if (user) {
|
||||
this.props.actions.getUser(user.id);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showEmailModal: false,
|
||||
user: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
getInfoForUser(user: UserProfile) {
|
||||
const info = [];
|
||||
|
||||
if (user.auth_service) {
|
||||
let service;
|
||||
if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
|
||||
service = user.auth_service.toUpperCase();
|
||||
} else {
|
||||
service = Utils.toTitleCase(user.auth_service);
|
||||
}
|
||||
|
||||
info.push(
|
||||
<FormattedMarkdownMessage
|
||||
key='admin.user_item.authServiceNotEmail'
|
||||
id='admin.user_item.authServiceNotEmail'
|
||||
defaultMessage='**Sign-in Method:** {service}'
|
||||
values={{
|
||||
service,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
info.push(
|
||||
<FormattedMarkdownMessage
|
||||
key='admin.user_item.authServiceEmail'
|
||||
id='admin.user_item.authServiceEmail'
|
||||
defaultMessage='**Sign-in Method:** Email'
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
info.push(', ');
|
||||
const userID = user.id;
|
||||
info.push(
|
||||
<FormattedMarkdownMessage
|
||||
key='admin.user_item.user_id'
|
||||
id='admin.user_item.user_id'
|
||||
defaultMessage='**User ID:** {userID}'
|
||||
values={{
|
||||
userID,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
if (this.props.mfaEnabled) {
|
||||
info.push(', ');
|
||||
|
||||
if (user.mfa_active) {
|
||||
info.push(
|
||||
<FormattedMarkdownMessage
|
||||
key='admin.user_item.mfaYes'
|
||||
id='admin.user_item.mfaYes'
|
||||
defaultMessage='**MFA**: Yes'
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
info.push(
|
||||
<FormattedMarkdownMessage
|
||||
key='admin.user_item.mfaNo'
|
||||
id='admin.user_item.mfaNo'
|
||||
defaultMessage='**MFA**: No'
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
renderCount(count: number, total: number, startCount: number, endCount: number, isSearch: boolean) {
|
||||
if (total) {
|
||||
if (isSearch) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='system_users_list.countSearch'
|
||||
defaultMessage='{count, number} {count, plural, one {user} other {users}} of {total, number} total'
|
||||
values={{
|
||||
count,
|
||||
total,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (startCount !== 0 || endCount !== total) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='system_users_list.countPage'
|
||||
defaultMessage='{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total, number} total'
|
||||
values={{
|
||||
count,
|
||||
startCount: startCount + 1,
|
||||
endCount,
|
||||
total,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='system_users_list.count'
|
||||
defaultMessage='{count, number} {count, plural, one {user} other {users}}'
|
||||
values={{
|
||||
count,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const extraInfo: {[key: string]: Array<string | JSX.Element>} = {};
|
||||
if (this.props.users) {
|
||||
for (const user of this.props.users) {
|
||||
extraInfo[user.id] = this.getInfoForUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SearchableUserList
|
||||
{...this.props}
|
||||
renderCount={this.renderCount}
|
||||
extraInfo={extraInfo}
|
||||
actions={[SystemUsersDropdown]}
|
||||
actionProps={{
|
||||
mfaEnabled: this.props.mfaEnabled,
|
||||
enableUserAccessTokens: this.props.enableUserAccessTokens,
|
||||
experimentalEnableAuthenticationTransfer: this.props.experimentalEnableAuthenticationTransfer,
|
||||
doPasswordReset: this.doPasswordReset,
|
||||
doEmailReset: this.doEmailReset,
|
||||
doManageTeams: this.doManageTeams,
|
||||
doManageRoles: this.doManageRoles,
|
||||
doManageTokens: this.doManageTokens,
|
||||
}}
|
||||
nextPage={this.nextPage}
|
||||
previousPage={this.previousPage}
|
||||
page={this.state.page}
|
||||
rowComponentType={UserListRowWithError}
|
||||
noBuiltInFilters={true}
|
||||
/>
|
||||
<ManageTeamsModal
|
||||
user={this.state.user}
|
||||
show={this.state.showManageTeamsModal}
|
||||
onModalDismissed={this.doManageTeamsDismiss}
|
||||
/>
|
||||
<ManageRolesModal
|
||||
user={this.state.user}
|
||||
show={this.state.showManageRolesModal}
|
||||
onModalDismissed={this.doManageRolesDismiss}
|
||||
/>
|
||||
<ManageTokensModal
|
||||
user={this.state.user}
|
||||
show={this.state.showManageTokensModal}
|
||||
onModalDismissed={this.doManageTokensDismiss}
|
||||
/>
|
||||
<ResetPasswordModal
|
||||
user={this.state.user}
|
||||
show={this.state.showPasswordModal}
|
||||
onModalSubmit={this.doPasswordResetSubmit}
|
||||
onModalDismissed={this.doPasswordResetDismiss}
|
||||
/>
|
||||
<ResetEmailModal
|
||||
user={this.state.user}
|
||||
show={this.state.showEmailModal}
|
||||
onModalSubmit={this.doEmailResetSubmit}
|
||||
onModalDismissed={this.doEmailResetDismiss}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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 {useDispatch} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {createGroupTeamsAndChannels} from 'mattermost-redux/actions/groups';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onExited: () => void;
|
||||
onError: (error: ServerError) => void;
|
||||
}
|
||||
|
||||
export default function CreateGroupSyncablesMembershipsModal({user, onExited, onError}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function confirm() {
|
||||
const {error} = await dispatch(createGroupTeamsAndChannels(user.id));
|
||||
if (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.title'
|
||||
defaultMessage='Re-add {username} to teams and channels'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.desc'
|
||||
defaultMessage="You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time."
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const createGroupMembershipsButton = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.create'
|
||||
defaultMessage='Yes'
|
||||
/>
|
||||
);
|
||||
|
||||
const cancelGroupMembershipsButton = (
|
||||
<FormattedMessage
|
||||
id='create_group_memberships_modal.cancel'
|
||||
defaultMessage='No'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
cancelButtonText={cancelGroupMembershipsButton}
|
||||
confirmButtonText={createGroupMembershipsButton}
|
||||
onConfirm={confirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// 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 {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {updateUserActive} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/admin';
|
||||
import {getExternalBotAccounts} from 'mattermost-redux/selectors/entities/bots';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onExited: () => void;
|
||||
onSuccess: () => void;
|
||||
onError: (error: ServerError) => void;
|
||||
}
|
||||
|
||||
export default function DeactivateMemberModal({user, onExited, onSuccess, onError}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const config = useSelector(getConfig);
|
||||
const bots = useSelector(getExternalBotAccounts);
|
||||
const siteURL = config.ServiceSettings?.SiteURL;
|
||||
|
||||
async function deactivateMember() {
|
||||
const {error} = await dispatch(updateUserActive(user.id, false));
|
||||
if (error) {
|
||||
onError(error);
|
||||
} else {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.title'
|
||||
defaultMessage='Deactivate {username}'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const defaultMessage = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc'
|
||||
defaultMessage='This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\n'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>);
|
||||
|
||||
let warning;
|
||||
if (user.auth_service !== '' && user.auth_service !== Constants.EMAIL_SERVICE) {
|
||||
warning = (
|
||||
<strong>
|
||||
<br/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.sso_warning'
|
||||
defaultMessage='You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.'
|
||||
/>
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
const confirmationMessage = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.confirm'
|
||||
defaultMessage='Are you sure you want to deactivate {username}?'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>);
|
||||
let messageForUsersWithBotAccounts;
|
||||
if (config.ServiceSettings?.DisableBotsWhenOwnerIsDeactivated) {
|
||||
for (const bot of Object.values(bots)) {
|
||||
if ((bot.owner_id === user.id) && (bot.delete_at === 0)) {
|
||||
messageForUsersWithBotAccounts = (
|
||||
<>
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.for_users_with_bot_accounts1'
|
||||
defaultMessage='This action deactivates {username}'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.for_users_with_bot_accounts2'
|
||||
defaultMessage='They will be logged out and not have access to any teams or channels on this system.'
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.desc.for_users_with_bot_accounts3'
|
||||
defaultMessage='Bot accounts they manage will be disabled along with their integrations. To enable them again, go to <linkBots>Integrations > Bot Accounts</linkBots>. <linkDocumentation>Learn more about bot accounts</linkDocumentation>.'
|
||||
values={{
|
||||
siteURL,
|
||||
linkBots: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href={`${siteURL}/_redirect/integrations/bots`}
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
linkDocumentation: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href='https://mattermost.com/pl/default-bot-accounts'
|
||||
location='system_users_dropdown'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p/>
|
||||
<p/>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const message = (
|
||||
<div>
|
||||
{messageForUsersWithBotAccounts || defaultMessage}
|
||||
{confirmationMessage}
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
|
||||
const confirmButtonClass = 'btn btn-danger';
|
||||
const deactivateMemberButton = (
|
||||
<FormattedMessage
|
||||
id='deactivate_member_modal.deactivate'
|
||||
defaultMessage='Deactivate'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass={confirmButtonClass}
|
||||
confirmButtonText={deactivateMemberButton}
|
||||
onConfirm={deactivateMember}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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 {useDispatch} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {demoteUserToGuest} from 'mattermost-redux/actions/users';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onExited: () => void;
|
||||
onSuccess: () => void;
|
||||
onError: (error: ServerError) => void;
|
||||
}
|
||||
|
||||
export default function DemoteToGuestModal({user, onExited, onSuccess, onError}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function confirm() {
|
||||
const {error} = await dispatch(demoteUserToGuest(user.id));
|
||||
if (error) {
|
||||
onError(error);
|
||||
} else {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='demote_to_user_modal.title'
|
||||
defaultMessage='Demote User {username} to Guest'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='demote_to_user_modal.desc'
|
||||
defaultMessage={'This action demotes the user {username} to a guest. It will restrict the user\'s ability to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to demote user {username} to guest?'}
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const demoteGuestButton = (
|
||||
<FormattedMessage
|
||||
id='demote_to_user_modal.demote'
|
||||
defaultMessage='Demote'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={demoteGuestButton}
|
||||
onConfirm={confirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {updateUserActive} from 'mattermost-redux/actions/users';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import General from 'mattermost-redux/constants/general';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isSystemAdmin, isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {adminResetMfa} from 'actions/admin_actions';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import ManageRolesModal from 'components/admin_console/manage_roles_modal';
|
||||
import ManageTeamsModal from 'components/admin_console/manage_teams_modal';
|
||||
import ManageTokensModal from 'components/admin_console/manage_tokens_modal';
|
||||
import ResetEmailModal from 'components/admin_console/reset_email_modal';
|
||||
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import CreateGroupSyncablesMembershipsModal from './create_group_syncables_membership_modal';
|
||||
import DeactivateMemberModal from './deactivate_member_modal';
|
||||
import DemoteToGuestModal from './demote_to_guest_modal';
|
||||
import PromoteToMemberModal from './promote_to_member_modal';
|
||||
import RevokeSessionsModal from './revoke_sessions_modal';
|
||||
|
||||
interface Props {
|
||||
user: UserProfile;
|
||||
currentUser: UserProfile;
|
||||
tableId?: string;
|
||||
rowIndex: number;
|
||||
onError: (error: ServerError) => void;
|
||||
updateUser: (user: Partial<UserProfile>) => void;
|
||||
}
|
||||
|
||||
export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onError, updateUser}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const config = useSelector(getConfig);
|
||||
const isLicensed = useSelector(getLicense)?.IsLicensed === 'true';
|
||||
|
||||
function getTranslatedUserRole(userRoles: UserProfile['roles']) {
|
||||
if (user.delete_at > 0) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.deactivated'
|
||||
defaultMessage='Deactivated'
|
||||
/>
|
||||
);
|
||||
} else if (user.roles.length > 0 && isSystemAdmin(userRoles)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.userAdmin'
|
||||
defaultMessage='System Admin'
|
||||
/>
|
||||
);
|
||||
} else if (isGuest(userRoles)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.userGuest'
|
||||
defaultMessage='Guest'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.userMember'
|
||||
defaultMessage='Member'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const menuButtonId = `actionMenuButton-${tableId}-${rowIndex}`;
|
||||
const menuId = `actionMenu-${tableId}-${rowIndex}`;
|
||||
const menuItemIdPrefix = `actionMenuItem-${tableId}-${rowIndex}`;
|
||||
|
||||
// Disable if SystemAdmin being edited by non SystemAdmin eg. userManager with EditOtherUsers permissions
|
||||
const isDisabled = !isSystemAdmin(currentUser.roles) && isSystemAdmin(user.roles);
|
||||
|
||||
const onDeactivateMember = () => updateUser({delete_at: new Date().getMilliseconds()});
|
||||
const onUpdateRoles = (roles: string) => updateUser({roles});
|
||||
const onSwitchToEmailPassword = () => updateUser({auth_service: undefined});
|
||||
const onUpdateEmail = (email: string) => updateUser({email});
|
||||
const onPromoteToMember = () => updateUser({roles: user.roles.replace(General.SYSTEM_GUEST_ROLE, '')});
|
||||
const onDemoteToGuest = () => updateUser({roles: `${user.roles} ${General.SYSTEM_GUEST_ROLE}`});
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: menuButtonId,
|
||||
class: classNames('btn btn-quaternary btn-sm', {
|
||||
disabled: isDisabled,
|
||||
}),
|
||||
disabled: isDisabled,
|
||||
children: (
|
||||
<>
|
||||
{getTranslatedUserRole(user.roles)}
|
||||
{!isDisabled && (
|
||||
<i
|
||||
aria-hidden='true'
|
||||
className='icon icon-chevron-down'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
menu={{
|
||||
id: menuId,
|
||||
'aria-label': formatMessage({
|
||||
id: 'admin.system_users.list.actions.menu.dropdownAriaLabel',
|
||||
defaultMessage: 'User actions menu',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{user.delete_at > 0 &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-active`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.activate'
|
||||
defaultMessage='Activate'
|
||||
/>
|
||||
}
|
||||
disabled={user.auth_service === Constants.LDAP_SERVICE}
|
||||
onClick={async () => {
|
||||
if (user.auth_service === Constants.LDAP_SERVICE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {error} = await dispatch(updateUserActive(user.id, true));
|
||||
if (error) {
|
||||
onError(error);
|
||||
} else {
|
||||
updateUser({delete_at: 0});
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
|
||||
{user.delete_at === 0 &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-deactivate`}
|
||||
isDestructive={true}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.deactivate'
|
||||
defaultMessage='Deactivate'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.DEACTIVATE_MEMBER_MODAL,
|
||||
dialogType: DeactivateMemberModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onError,
|
||||
onSuccess: onDeactivateMember,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
|
||||
{isSystemAdmin(currentUser.roles) &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-manageRoles`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.manageRoles'
|
||||
defaultMessage='Manage roles'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.MANAGE_ROLES_MODAL,
|
||||
dialogType: ManageRolesModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onSuccess: onUpdateRoles,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-manageTeams`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.manageTeams'
|
||||
defaultMessage='Manage teams'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.MANAGE_TEAMS_MODAL,
|
||||
dialogType: ManageTeamsModal,
|
||||
dialogProps: {user},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
||||
{config.EnableUserAccessTokens === 'true' &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-manageTokens`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.manageTokens'
|
||||
defaultMessage='Manage tokens'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.MANAGE_TOKENS_MODAL,
|
||||
dialogType: ManageTokensModal,
|
||||
dialogProps: {user},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
|
||||
{!user.auth_service &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-resetPassword`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.resetPassword'
|
||||
defaultMessage='Reset password'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.RESET_PASSWORD_MODAL,
|
||||
dialogType: ResetPasswordModal,
|
||||
dialogProps: {user},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
|
||||
{user.mfa_active && config.EnableMultifactorAuthentication &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-removeMFA`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.removeMFA'
|
||||
defaultMessage='Remove MFA'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
adminResetMfa(user.id, null, onError).then(() => {
|
||||
updateUser({mfa_active: false});
|
||||
});
|
||||
}}
|
||||
/>}
|
||||
|
||||
{Boolean(user.auth_service) && config.ExperimentalEnableAuthenticationTransfer === 'true' &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-switchToEmailPassword`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.switchToEmailPassword'
|
||||
defaultMessage='Switch to Email/Password'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.RESET_PASSWORD_MODAL,
|
||||
dialogType: ResetPasswordModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onSuccess: onSwitchToEmailPassword,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
|
||||
{!user.auth_service &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-updateEmail`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.updateEmail'
|
||||
defaultMessage='Update email'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.RESET_EMAIL_MODAL,
|
||||
dialogType: ResetEmailModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onSuccess: onUpdateEmail,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
|
||||
{isGuest(user.roles) &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-promoteToMember`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.promoteToMember'
|
||||
defaultMessage='Promote to member'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.PROMOTE_TO_MEMBER_MODAL,
|
||||
dialogType: PromoteToMemberModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onError,
|
||||
onSuccess: onPromoteToMember,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
{!isGuest(user.roles) && user.id !== currentUser.id && isLicensed && config.EnableGuestAccounts === 'true' &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-demoteToGuest`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.demoteToGuest'
|
||||
defaultMessage='Demote to guest'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.DEMOTE_TO_GUEST_MODAL,
|
||||
dialogType: DemoteToGuestModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onError,
|
||||
onSuccess: onDemoteToGuest,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
<SystemPermissionGate permissions={[Permissions.REVOKE_USER_ACCESS_TOKEN]}>
|
||||
{!user.delete_at &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-removeSessions`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.removeSessions'
|
||||
defaultMessage='Remove sessions'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.REVOKE_SESSIONS_MODAL,
|
||||
dialogType: RevokeSessionsModal,
|
||||
dialogProps: {user, currentUser, onError},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
</SystemPermissionGate>
|
||||
<SystemPermissionGate permissions={[Permissions.SYSCONSOLE_WRITE_USERMANAGEMENT_GROUPS]}>
|
||||
{user.auth_service === Constants.LDAP_SERVICE &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-resyncUserViaLdapGroups`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.resyncUserViaLdapGroups'
|
||||
defaultMessage='Re-sync user via LDAP groups'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.CREATE_GROUP_SYNCABLES_MEMBERSHIP_MODAL,
|
||||
dialogType: CreateGroupSyncablesMembershipsModal,
|
||||
dialogProps: {user, onError},
|
||||
}));
|
||||
}}
|
||||
/>}
|
||||
</SystemPermissionGate>
|
||||
</Menu.Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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 {useDispatch} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {promoteGuestToUser} from 'mattermost-redux/actions/users';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onSuccess: () => void;
|
||||
onExited: () => void;
|
||||
onError: (error: ServerError) => void;
|
||||
}
|
||||
|
||||
export default function PromoteToMemberModal({user, onExited, onSuccess, onError}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function confirm() {
|
||||
const {error} = await dispatch(promoteGuestToUser(user.id));
|
||||
if (error) {
|
||||
onError(error);
|
||||
} else {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='promote_to_user_modal.title'
|
||||
defaultMessage='Promote guest {username} to member'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='promote_to_user_modal.desc'
|
||||
defaultMessage='This action promotes the guest {username} to a member. It will allow the user to join public channels and interact with users outside of the channels they are currently members of. Are you sure you want to promote guest {username} to member?'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const promoteUserButton = (
|
||||
<FormattedMessage
|
||||
id='promote_to_user_modal.promote'
|
||||
defaultMessage='Promote'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={promoteUserButton}
|
||||
onConfirm={confirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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 {useDispatch} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {revokeAllSessionsForUser} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {emitUserLoggedOutEvent} from 'actions/global_actions';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
currentUser: UserProfile;
|
||||
onExited: () => void;
|
||||
onError: (error: ServerError) => void;
|
||||
}
|
||||
|
||||
export default function RevokeSessionsModal({user, currentUser, onExited, onError}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function confirm() {
|
||||
const {data, error} = await dispatch(revokeAllSessionsForUser(user.id));
|
||||
if (data && user.id === currentUser.id) {
|
||||
emitUserLoggedOutEvent();
|
||||
} else if (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='revoke_user_sessions_modal.title'
|
||||
defaultMessage='Revoke Sessions for {username}'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='revoke_user_sessions_modal.desc'
|
||||
defaultMessage='This action revokes all sessions for {username}. They will be logged out from all devices. Are you sure you want to revoke all sessions for {username}?'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const revokeUserButtonButton = (
|
||||
<FormattedMessage
|
||||
id='revoke_user_sessions_modal.revoke'
|
||||
defaultMessage='Revoke'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
confirmButtonText={revokeUserButtonButton}
|
||||
onConfirm={confirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import React, {useCallback, useEffect} from 'react';
|
||||
import React, {useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {setAdminConsoleUsersManagementTableProperties} from 'actions/views/admin';
|
||||
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
import './system_users_search.scss';
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onChange: ({searchTerm, teamId, filter}: {searchTerm?: string; teamId?: string; filter?: string}) => void;
|
||||
onSearch: (value: string) => void;
|
||||
};
|
||||
searchTerm: AdminConsoleUserManagementTableProperties['searchTerm'];
|
||||
}
|
||||
|
||||
function SystemUsersSearch(props: Props) {
|
||||
export function SystemUsersSearch(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const debouncedSearch = useCallback(debounce((value: string) => {
|
||||
props.onSearch(value);
|
||||
}, Constants.SEARCH_TIMEOUT_MILLISECONDS), []);
|
||||
const [inputValue, setInputValue] = useState(props.searchTerm);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, []);
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const {target: {value}} = event;
|
||||
setInputValue(value);
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
const searchTerm = e?.target?.value?.trim() ?? '';
|
||||
props.onChange({searchTerm});
|
||||
clearTimeout(timeout.current);
|
||||
timeout.current = setTimeout(() => {
|
||||
dispatch(setAdminConsoleUsersManagementTableProperties({searchTerm: value}));
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (searchTerm.length > 0) {
|
||||
debouncedSearch(searchTerm);
|
||||
}
|
||||
function handleClear() {
|
||||
setInputValue('');
|
||||
dispatch(setAdminConsoleUsersManagementTableProperties({searchTerm: ''}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='system-users__filter'>
|
||||
<input
|
||||
id='searchUsers'
|
||||
className='form-control filter-textbox'
|
||||
placeholder={formatMessage({
|
||||
id: 'filtered_user_list.search',
|
||||
defaultMessage: 'Search users',
|
||||
})}
|
||||
value={props.value}
|
||||
<Input
|
||||
type='text'
|
||||
clearable={true}
|
||||
name='searchTerm'
|
||||
containerClassName='systemUsersSearch'
|
||||
placeholder={formatMessage({id: 'admin.system_users.search.placeholder', defaultMessage: 'Search users'})}
|
||||
inputPrefix={<i className={'icon icon-magnify'}/>}
|
||||
onChange={handleChange}
|
||||
onClear={handleClear}
|
||||
value={inputValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemUsersSearch;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.systemUsersSearch {
|
||||
width: 360px;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {UserReport} from '@mattermost/types/reports';
|
||||
|
||||
import {ColumnNames, StatusFilter} from '../constants';
|
||||
|
||||
import {getSortColumnForOptions, getSortDirectionForOptions, getSortableColumnValueBySortColumn, getStatusFilterOption} from './index';
|
||||
|
||||
describe('getSortColumnForOptions', () => {
|
||||
it('should return correct sort column for email', () => {
|
||||
const result = getSortColumnForOptions(ColumnNames.email);
|
||||
expect(result.sort_column).toBe('Email');
|
||||
});
|
||||
|
||||
it('should return correct sort column for createAt', () => {
|
||||
const result = getSortColumnForOptions(ColumnNames.createAt);
|
||||
expect(result.sort_column).toBe('CreateAt');
|
||||
});
|
||||
|
||||
it('should default to username if no id is provided', () => {
|
||||
const result = getSortColumnForOptions();
|
||||
expect(result.sort_column).toBe('Username');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortDirectionForOptions', () => {
|
||||
it('should return ascending sort direction if desc is false', () => {
|
||||
const result = getSortDirectionForOptions(false);
|
||||
expect(result.sort_direction).toBe('asc');
|
||||
});
|
||||
|
||||
it('should return descending sort direction if desc is true', () => {
|
||||
const result = getSortDirectionForOptions(true);
|
||||
expect(result.sort_direction).toBe('desc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortableColumnValueBySortColumn', () => {
|
||||
it('should return email value when sortColumn is email', () => {
|
||||
const row = {email: 'test@example.com', id: 'testId'} as UserReport;
|
||||
const result = getSortableColumnValueBySortColumn(row, ColumnNames.email);
|
||||
expect(result).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should return create_at value as string when sortColumn is createAt', () => {
|
||||
const row = {create_at: 1234} as UserReport;
|
||||
const result = getSortableColumnValueBySortColumn(row, ColumnNames.createAt);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toBe('1234');
|
||||
});
|
||||
|
||||
it('should return username value by default', () => {
|
||||
const row = {username: 'testuser', id: 'testId', create_at: 22122} as UserReport;
|
||||
const result = getSortableColumnValueBySortColumn(row, 'someOtherColumn');
|
||||
expect(result).toBe('testuser');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatusFilterOption', () => {
|
||||
it('should return hide_inactive true for Active status', () => {
|
||||
const result = getStatusFilterOption(StatusFilter.Active);
|
||||
expect(result).toEqual({hide_inactive: true});
|
||||
});
|
||||
|
||||
it('should return hide_active true for Deactivated status', () => {
|
||||
const result = getStatusFilterOption(StatusFilter.Deactivated);
|
||||
expect(result).toEqual({hide_active: true});
|
||||
});
|
||||
|
||||
it('should return an empty object for other status', () => {
|
||||
const result = getStatusFilterOption('SomeOtherStatus' as any);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {SortingState} from '@tanstack/react-table';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {UserReportSortColumns, ReportSortDirection} from '@mattermost/types/reports';
|
||||
import type {UserReportOptions, UserReport} from '@mattermost/types/reports';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {PAGE_SIZES} from 'components/admin_console/list_table';
|
||||
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
import {ColumnNames, RoleFilters, StatusFilter, TeamFilters} from '../constants';
|
||||
import type {TableOptions} from '../system_users';
|
||||
import type {OptionType as TeamFilterOptionType} from '../system_users_filters_popover/system_users_filter_team';
|
||||
|
||||
export function convertTableOptionsToUserReportOptions(tableOptions?: TableOptions): UserReportOptions {
|
||||
return {
|
||||
page_size: tableOptions?.pageSize || PAGE_SIZES[0],
|
||||
from_column_value: tableOptions?.fromColumnValue,
|
||||
from_id: tableOptions?.fromId,
|
||||
direction: tableOptions?.direction,
|
||||
...getSortColumnForOptions(tableOptions?.sortColumn),
|
||||
...getSortDirectionForOptions(tableOptions?.sortIsDescending),
|
||||
...getSearchFilterOption(tableOptions?.searchTerm),
|
||||
...getTeamFilterOption(tableOptions?.filterTeam),
|
||||
...getStatusFilterOption(tableOptions?.filterStatus),
|
||||
...getRoleFilterOption(tableOptions?.filterRole),
|
||||
date_range: tableOptions?.dateRange,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the sorting column name to API compatible sorting column name. Default sorting column name is by username.
|
||||
*/
|
||||
export function getSortColumnForOptions(id?: SortingState[0]['id']): Pick<UserReportOptions, 'sort_column'> {
|
||||
let sortColumn: UserReportOptions['sort_column'];
|
||||
|
||||
if (id === ColumnNames.email) {
|
||||
sortColumn = UserReportSortColumns.email;
|
||||
} else if (id === ColumnNames.createAt) {
|
||||
sortColumn = UserReportSortColumns.createAt;
|
||||
} else {
|
||||
// Default sorting to first User details column
|
||||
sortColumn = UserReportSortColumns.username;
|
||||
}
|
||||
|
||||
return {
|
||||
sort_column: sortColumn,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the sorting direction to API compatible sorting direction. Default sorting direction is ascending.
|
||||
*/
|
||||
export function getSortDirectionForOptions(desc?: SortingState[0]['desc']): Pick<UserReportOptions, 'sort_direction'> {
|
||||
let sortDirection: UserReportOptions['sort_direction'];
|
||||
|
||||
if (desc) {
|
||||
sortDirection = ReportSortDirection.descending;
|
||||
} else {
|
||||
sortDirection = ReportSortDirection.ascending;
|
||||
}
|
||||
|
||||
return {
|
||||
sort_direction: sortDirection,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns the value of that column on which sorting is applied.
|
||||
*/
|
||||
export function getSortableColumnValueBySortColumn(row: UserReport, sortColumn: AdminConsoleUserManagementTableProperties['sortColumn']): string {
|
||||
switch (sortColumn) {
|
||||
case ColumnNames.email:
|
||||
return row.email;
|
||||
case ColumnNames.createAt:
|
||||
return String(row.create_at);
|
||||
default:
|
||||
return row.username;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTeamFilterOption(teamId?: string): Partial<Pick<UserReportOptions, 'team_filter' | 'has_no_team'>> {
|
||||
if (!teamId || teamId === TeamFilters.AllTeams) {
|
||||
return {
|
||||
team_filter: undefined,
|
||||
has_no_team: undefined,
|
||||
};
|
||||
} else if (teamId === TeamFilters.NoTeams) {
|
||||
return {
|
||||
team_filter: undefined,
|
||||
has_no_team: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
team_filter: teamId,
|
||||
has_no_team: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function getStatusFilterOption(status?: string): Partial<Pick<UserReportOptions, 'hide_active' | 'hide_inactive'>> {
|
||||
if (status === StatusFilter.Active) {
|
||||
return {
|
||||
hide_inactive: true,
|
||||
};
|
||||
} else if (status === StatusFilter.Deactivated) {
|
||||
return {
|
||||
hide_active: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hide_active: undefined,
|
||||
hide_inactive: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPaginationInfo(pageIndex: number, pageSize: number, currentLength: number, total?: number) {
|
||||
if (!currentLength) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users_list.pagination.no_users'
|
||||
defaultMessage='0 users'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const firstPage = (pageIndex * pageSize) + 1;
|
||||
const lastPage = (pageIndex * pageSize) + currentLength;
|
||||
const totalItems = total || 0;
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.system_users_list.pagination'
|
||||
defaultMessage='Showing {firstPage} - {lastPage} of {totalItems} users'
|
||||
values={{
|
||||
firstPage,
|
||||
lastPage,
|
||||
totalItems,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultSelectedValueFromList<T extends {value: string}>(value: string, options: T[]) {
|
||||
const option = options.find((option) => option.value === value);
|
||||
|
||||
if (option) {
|
||||
return option;
|
||||
}
|
||||
|
||||
return options[0];
|
||||
}
|
||||
|
||||
export function getDefaultSelectedTeam(teamId: Team['id'] | string, label?: string): TeamFilterOptionType {
|
||||
if (!teamId || teamId === TeamFilters.AllTeams) {
|
||||
return {
|
||||
value: TeamFilters.AllTeams,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.filters.team.allTeams'
|
||||
defaultMessage='All Teams'
|
||||
/>
|
||||
),
|
||||
};
|
||||
} else if (teamId === TeamFilters.NoTeams) {
|
||||
return {
|
||||
value: TeamFilters.NoTeams,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='admin.system_users.filters.team.noTeams'
|
||||
defaultMessage='No Teams'
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: teamId,
|
||||
label: label || '',
|
||||
};
|
||||
}
|
||||
|
||||
export function getRoleFilterOption(role?: string): Pick<UserReportOptions, 'role_filter'> {
|
||||
if (!role || role === RoleFilters.Any) {
|
||||
return {role_filter: undefined};
|
||||
}
|
||||
return {role_filter: role};
|
||||
}
|
||||
|
||||
export function getSearchFilterOption(search?: string): Pick<UserReportOptions, 'search_term'> {
|
||||
if (!search || search.trim().length === 0) {
|
||||
return {search_term: undefined};
|
||||
}
|
||||
|
||||
return {search_term: search};
|
||||
}
|
||||
3
webapp/channels/src/components/confirm_modal.scss
Обычный файл
3
webapp/channels/src/components/confirm_modal.scss
Обычный файл
@@ -0,0 +1,3 @@
|
||||
.modal-confirm .modal-footer .checkbox {
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import './confirm_modal.scss';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
@@ -52,6 +54,11 @@ type Props = {
|
||||
*/
|
||||
checkboxText?: React.ReactNode;
|
||||
|
||||
/*
|
||||
* If true, show the checkbox in the footer instead of the modal body
|
||||
*/
|
||||
checkboxInFooter?: boolean;
|
||||
|
||||
/*
|
||||
* Function called when the confirm button or ENTER is pressed. Passes `true` if the checkbox is checked
|
||||
*/
|
||||
@@ -179,9 +186,10 @@ export default class ConfirmModal extends React.Component<Props, State> {
|
||||
</Modal.Header>
|
||||
<Modal.Body id='confirmModalBody'>
|
||||
{this.props.message}
|
||||
{checkbox}
|
||||
{!this.props.checkboxInFooter && checkbox}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{this.props.checkboxInFooter && checkbox}
|
||||
{cancelButton}
|
||||
<button
|
||||
autoFocus={true}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
$dropdown_input_index: 999999;
|
||||
|
||||
.DropdownInput {
|
||||
z-index: $dropdown_input_index;
|
||||
|
||||
&.Input_container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@ const baseStyles = {
|
||||
...provided,
|
||||
display: 'none',
|
||||
}),
|
||||
menu: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
zIndex: 100,
|
||||
}),
|
||||
menuPortal: (provided: CSSProperties) => ({
|
||||
...provided,
|
||||
zIndex: 100,
|
||||
}),
|
||||
};
|
||||
|
||||
const IndicatorsContainer = (props: any) => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
ReactNode,
|
||||
MouseEvent,
|
||||
KeyboardEvent,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
@@ -42,6 +41,7 @@ type MenuButtonProps = {
|
||||
'aria-label'?: string;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export function Menu(props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleMenuButtonClick(event: SyntheticEvent<HTMLButtonElement>) {
|
||||
function handleMenuButtonClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -157,7 +157,7 @@ export function Menu(props: Props) {
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setAnchorElement(event.currentTarget);
|
||||
setAnchorElement(event.currentTarget as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +168,10 @@ export function Menu(props: Props) {
|
||||
|
||||
// We construct the menu button so we can set onClick correctly here to support both web and mobile view
|
||||
function renderMenuButton() {
|
||||
const MenuButtonComponent = props.menuButton?.as ?? 'button';
|
||||
|
||||
const triggerElement = (
|
||||
<button
|
||||
<MenuButtonComponent
|
||||
id={props.menuButton.id}
|
||||
data-testid={props.menuButton.dateTestId}
|
||||
aria-controls={props.menu.id}
|
||||
@@ -182,7 +184,7 @@ export function Menu(props: Props) {
|
||||
onMouseDown={handleMenuButtonMouseDown}
|
||||
>
|
||||
{props.menuButton.children}
|
||||
</button>
|
||||
</MenuButtonComponent>
|
||||
);
|
||||
|
||||
if (props.menuButtonTooltip && props.menuButtonTooltip.text && !isMobileView) {
|
||||
|
||||
@@ -13,7 +13,9 @@ import type {
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
KeyboardEvent,
|
||||
MouseEvent} from 'react';
|
||||
MouseEvent,
|
||||
AriaRole,
|
||||
} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
@@ -79,6 +81,8 @@ export interface Props extends MuiMenuItemProps {
|
||||
|
||||
onClick?: (event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
|
||||
|
||||
role?: AriaRole;
|
||||
|
||||
/**
|
||||
* ONLY to support submenus. Avoid passing children to this component. Support for children is only added to support submenus.
|
||||
*/
|
||||
@@ -112,6 +116,7 @@ export function MenuItem(props: Props) {
|
||||
isLabelsRowLayout,
|
||||
children,
|
||||
onClick,
|
||||
role = 'menuitem',
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -122,19 +127,26 @@ export function MenuItem(props: Props) {
|
||||
|
||||
function handleClick(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
||||
if (isCorrectKeyPressedOnMenuItem(event)) {
|
||||
// close submenu first if it is open
|
||||
if (subMenuContext.close) {
|
||||
subMenuContext.close();
|
||||
}
|
||||
// If the menu item is a checkbox or radio button, we don't want to close the menu when it is clicked.
|
||||
// see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
|
||||
if (isRoleCheckboxOrRadio(role)) {
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
// close submenu first if it is open
|
||||
if (subMenuContext.close) {
|
||||
subMenuContext.close();
|
||||
}
|
||||
|
||||
// And then close the menu
|
||||
if (menuContext.close) {
|
||||
menuContext.close();
|
||||
// And then close the menu
|
||||
if (menuContext.close) {
|
||||
menuContext.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
if (isMobileView) {
|
||||
// If the menu is in mobile view, we execute the click event immediately.
|
||||
// If the menu is in mobile view, we execute the click event immediately.
|
||||
// If the menu item is a checkbox or radio button, we execute the click event immediately.
|
||||
if (isMobileView || isRoleCheckboxOrRadio(role)) {
|
||||
onClick(event);
|
||||
} else {
|
||||
// Clone the event since we delay the click handler until after the menu has closed.
|
||||
@@ -160,6 +172,7 @@ export function MenuItem(props: Props) {
|
||||
isLabelsRowLayout={isLabelsRowLayout}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleClick}
|
||||
role={role}
|
||||
{...otherProps}
|
||||
>
|
||||
{leadingElement && <div className='leading-element'>{leadingElement}</div>}
|
||||
@@ -176,7 +189,7 @@ interface MenuItemStyledProps extends MuiMenuItemProps {
|
||||
isLabelsRowLayout?: boolean;
|
||||
}
|
||||
|
||||
const MenuItemStyled = styled(MuiMenuItem, {
|
||||
export const MenuItemStyled = styled(MuiMenuItem, {
|
||||
shouldForwardProp: (prop) => prop !== 'isDestructive' &&
|
||||
prop !== 'hasSecondaryLabel' && prop !== 'isLabelsRowLayout',
|
||||
})<MenuItemStyledProps>(
|
||||
@@ -307,3 +320,8 @@ function isCorrectKeyPressedOnMenuItem(event: MouseEvent<HTMLLIElement> | Keyboa
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isRoleCheckboxOrRadio(role: AriaRole) {
|
||||
return role === 'menuitemcheckbox' || role === 'menuitemradio';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
/**
|
||||
* This is a styled div that should be used as a container for popovers.
|
||||
* It has basic styling common to all popovers containers, however, it can be extended to add more styling.
|
||||
*/
|
||||
export const StyledPopoverContainer = styled.div`
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background-color: var(--center-channel-bg);
|
||||
box-shadow: var(--elevation-4);
|
||||
z-index: 9999;
|
||||
`;
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.Input {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
@@ -121,7 +120,6 @@
|
||||
|
||||
.Input_fieldset {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@@ -170,7 +168,7 @@
|
||||
|
||||
.Input_legend {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
z-index: 1;
|
||||
top: -8px;
|
||||
display: flex;
|
||||
width: auto;
|
||||
|
||||
@@ -2427,19 +2427,84 @@
|
||||
"admin.support.termsTitle": "Terms of Use Link:",
|
||||
"admin.system_roles_feature_discovery.copy": "Use System Roles to give designated users read and/or write access to select sections of System Console.",
|
||||
"admin.system_roles_feature_discovery.title": "Provide controlled access to the System Console with Mattermost Enterprise",
|
||||
"admin.system_users.active": "Activated",
|
||||
"admin.system_users.allUsers": "All Users",
|
||||
"admin.system_users.guest": "Guest",
|
||||
"admin.system_users.inactive": "Deactivated",
|
||||
"admin.system_users_list.pagination": "Showing {firstPage} - {lastPage} of {totalItems} users",
|
||||
"admin.system_users_list.pagination.no_users": "0 users",
|
||||
"admin.system_users.column_toggler.dropdownAriaLabel": "Columns visibility menu",
|
||||
"admin.system_users.column_toggler.menuButtonAriaLabel": "Open menu to select columns to display",
|
||||
"admin.system_users.column_toggler.menuButtonText": "{selectedCount} selected",
|
||||
"admin.system_users.column_toggler.mysql_unavailable.desc": "Please use the export functionality to view these values",
|
||||
"admin.system_users.column_toggler.mysql_unavailable.label": "Not available",
|
||||
"admin.system_users.column_toggler.mysql_unavailable.title": "Not available for servers using MySQL",
|
||||
"admin.system_users.column_toggler.placeholder": "Columns",
|
||||
"admin.system_users.date_range_selector.date_range.all_time": "All time",
|
||||
"admin.system_users.date_range_selector.date_range.last_30_days": "Last 30 days",
|
||||
"admin.system_users.date_range_selector.date_range.last_6_months": "Last 6 months",
|
||||
"admin.system_users.date_range_selector.date_range.previous_month": "Previous month",
|
||||
"admin.system_users.date_range_selector.date_range.sublabel": "{startDate} - {endDate}",
|
||||
"admin.system_users.date_range_selector.dropdownAriaLabel": "Date range menu",
|
||||
"admin.system_users.date_range_selector.label": "Duration",
|
||||
"admin.system_users.date_range_selector.menuButtonAriaLabel": "Open menu to select columns to display",
|
||||
"admin.system_users.date_range_selector.trailing_message": "Note: This filter will only affect values in the <strong>Last Post, Days Active, and Messages Posted</strong> columns.",
|
||||
"admin.system_users.exportButton": "Export",
|
||||
"admin.system_users.exportButton.notLicensed.hint": "This feature is available on the professional plan",
|
||||
"admin.system_users.exportButton.notLicensed.title": "Professional feature",
|
||||
"admin.system_users.filters.role.any": "Any",
|
||||
"admin.system_users.filters.role.system_admin": "System Admin",
|
||||
"admin.system_users.filters.role.system_guest": "Guest",
|
||||
"admin.system_users.filters.role.system_user": "Member",
|
||||
"admin.system_users.filters.role.title": "Role",
|
||||
"admin.system_users.filters.status.active": "Active users",
|
||||
"admin.system_users.filters.status.any": "Any",
|
||||
"admin.system_users.filters.status.deactive": "Deactivated users",
|
||||
"admin.system_users.filters.status.title": "Status",
|
||||
"admin.system_users.filters.team.allTeams": "All Teams",
|
||||
"admin.system_users.filters.team.errorLoading": "Error while loading teams",
|
||||
"admin.system_users.filters.team.errorSearching": "Error while searching teams",
|
||||
"admin.system_users.filters.team.noTeams": "No Teams",
|
||||
"admin.system_users.filters.team.title": "Team",
|
||||
"admin.system_users.filtersMenu": "Filters ({count})",
|
||||
"admin.system_users.filtersPopover.apply": "Apply",
|
||||
"admin.system_users.filtersPopover.title": "Filter by",
|
||||
"admin.system_users.list.actions": "Actions",
|
||||
"admin.system_users.list.actions.deactivated": "Deactivated",
|
||||
"admin.system_users.list.actions.menu.activate": "Activate",
|
||||
"admin.system_users.list.actions.menu.deactivate": "Deactivate",
|
||||
"admin.system_users.list.actions.menu.demoteToGuest": "Demote to guest",
|
||||
"admin.system_users.list.actions.menu.dropdownAriaLabel": "User actions menu",
|
||||
"admin.system_users.list.actions.menu.manageRoles": "Manage roles",
|
||||
"admin.system_users.list.actions.menu.manageTeams": "Manage teams",
|
||||
"admin.system_users.list.actions.menu.manageTokens": "Manage tokens",
|
||||
"admin.system_users.list.actions.menu.promoteToMember": "Promote to member",
|
||||
"admin.system_users.list.actions.menu.removeMFA": "Remove MFA",
|
||||
"admin.system_users.list.actions.menu.removeSessions": "Remove sessions",
|
||||
"admin.system_users.list.actions.menu.resetPassword": "Reset password",
|
||||
"admin.system_users.list.actions.menu.resyncUserViaLdapGroups": "Re-sync user via LDAP groups",
|
||||
"admin.system_users.list.actions.menu.switchToEmailPassword": "Switch to Email/Password",
|
||||
"admin.system_users.list.actions.menu.updateEmail": "Update email",
|
||||
"admin.system_users.list.actions.userAdmin": "System Admin",
|
||||
"admin.system_users.list.actions.userGuest": "Guest",
|
||||
"admin.system_users.list.actions.userMember": "Member",
|
||||
"admin.system_users.list.caption": "System Users",
|
||||
"admin.system_users.list.daysActive": "Days active",
|
||||
"admin.system_users.list.email": "Email",
|
||||
"admin.system_users.list.lastActivity": "Last activity",
|
||||
"admin.system_users.list.lastLoginAt": "Last login",
|
||||
"admin.system_users.list.lastPost": "Last post",
|
||||
"admin.system_users.list.memberSince": "Member since",
|
||||
"admin.system_users.list.memberSince.days": "{days} days",
|
||||
"admin.system_users.list.memberSince.today": "Today",
|
||||
"admin.system_users.list.memberSince.yesterday": "Yesterday",
|
||||
"admin.system_users.noTeams": "No Teams",
|
||||
"admin.system_users.list.totalPosts": "Messages posted",
|
||||
"admin.system_users.list.userDetails": "User details",
|
||||
"admin.system_users.mysql_stats.desc": "Use of MySQL may limit the availability of some statistics features. We recommend transitioning from MySQL to PostgreSQL to fully leverage improved performance and comprehensive analytics. While you’re still using MySQL, please use the export functionality to view all user statistics.",
|
||||
"admin.system_users.mysql_stats.dismiss": "Dismiss",
|
||||
"admin.system_users.mysql_stats.learn_more": "Learn more",
|
||||
"admin.system_users.mysql_stats.title": "Some statistics are unavailable for servers using MySQL",
|
||||
"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, 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",
|
||||
"admin.system_users.search.placeholder": "Search users",
|
||||
"admin.system_users.title": "{siteName} Users",
|
||||
"admin.systemUserDetail.teamList.header.name": "Name",
|
||||
"admin.systemUserDetail.teamList.header.role": "Role",
|
||||
@@ -2606,46 +2671,35 @@
|
||||
"admin.user_grid.shared_member": "Shared Member",
|
||||
"admin.user_grid.system_admin": "System Admin",
|
||||
"admin.user_grid.team_admin": "Team Admin",
|
||||
"admin.user_item.authServiceEmail": "**Sign-in Method:** Email",
|
||||
"admin.user_item.authServiceNotEmail": "**Sign-in Method:** {service}",
|
||||
"admin.user_item.createGroupTeamChannelMemberships": "Re-sync user via LDAP groups",
|
||||
"admin.user_item.demoteToGuest": "Demote to Guest",
|
||||
"admin.user_item.deactivate": "Deactivate",
|
||||
"admin.user_item.emailTitle": "**Email:** {email}",
|
||||
"admin.user_item.guest": "Guest",
|
||||
"admin.user_item.inactive": "Deactivated",
|
||||
"admin.user_item.invalidEmail": "Invalid email address",
|
||||
"admin.user_item.makeActive": "Activate",
|
||||
"admin.user_item.makeInactive": "Deactivate",
|
||||
"admin.user_item.makeMember": "Make Team Member",
|
||||
"admin.user_item.makeTeamAdmin": "Make Team Admin",
|
||||
"admin.user_item.managedByLdap": "Managed by LDAP",
|
||||
"admin.user_item.manageRoles": "Manage Roles",
|
||||
"admin.user_item.manageTeams": "Manage Teams",
|
||||
"admin.user_item.manageTokens": "Manage Tokens",
|
||||
"admin.user_item.member": "Member",
|
||||
"admin.user_item.menuAriaLabel": "User Actions Menu",
|
||||
"admin.user_item.mfaNo": "**MFA**: No",
|
||||
"admin.user_item.mfaYes": "**MFA**: Yes",
|
||||
"admin.user_item.promoteToMember": "Promote to Member",
|
||||
"admin.user_item.remoteUser": "Remote user",
|
||||
"admin.user_item.resetEmail": "Update Email",
|
||||
"admin.user_item.resetMfa": "Remove MFA",
|
||||
"admin.user_item.resetPwd": "Reset Password",
|
||||
"admin.user_item.revokeSessions": "Revoke Sessions",
|
||||
"admin.user_item.switchToEmail": "Switch to Email/Password",
|
||||
"admin.user_item.sysAdmin": "System Admin",
|
||||
"admin.user_item.teamAdmin": "Team Admin",
|
||||
"admin.user_item.teamMember": "Team Member",
|
||||
"admin.user_item.user_id": "**User ID:** {userID}",
|
||||
"admin.user_item.userAccessTokenPostAll": "(can create post:all personal access tokens)",
|
||||
"admin.user_item.userAccessTokenPostAllPublic": "(can create post:channels personal access tokens)",
|
||||
"admin.user_item.userAccessTokenYes": "(can create personal access tokens)",
|
||||
"admin.user_item.userActivateFailed": "Failed to activate user",
|
||||
"admin.user_item.userDeactivateFailed": "Failed to deactivate user",
|
||||
"admin.user_item.userMFARemoveFailed": "Failed to remove user's MFA",
|
||||
"admin.user_item.userNotFound": "Cannot load User",
|
||||
"admin.user_item.userUpdateFailed": "Failed to update user",
|
||||
"admin.userManagement.userDetail.addTeam": "Add Team",
|
||||
"admin.userManagement.userDetail.authenticationMethod": "Authentication Method",
|
||||
"admin.userManagement.userDetail.email": "Email",
|
||||
"admin.userManagement.userDetail.mfa": "MFA",
|
||||
"admin.userManagement.userDetail.notFound": "User not found",
|
||||
"admin.userManagement.userDetail.teamsSubtitle": "Teams to which this user belongs",
|
||||
"admin.userManagement.userDetail.teamsTitle": "Team Membership",
|
||||
"admin.userManagement.userDetail.userId": "User ID:",
|
||||
"admin.userManagement.userDetail.userId": "User ID: {userId}",
|
||||
"admin.userManagement.userDetail.username": "Username",
|
||||
"admin.viewArchivedChannelsHelpText": "When true, allows users to view, share and search for content of channels that have been archived. Users can only view the content in channels of which they were a member before the channel was archived.",
|
||||
"admin.viewArchivedChannelsTitle": "Allow users to view archived channels:",
|
||||
@@ -2659,9 +2713,11 @@
|
||||
"admin.webserverModeUncompressed": "Uncompressed",
|
||||
"admin.webserverModeUncompressedDescription": "The Mattermost server will serve static files uncompressed.",
|
||||
"adminConsole.list.table.exactTime.at": "at",
|
||||
"adminConsole.list.table.genericError": "There was an error loading the data, please try again",
|
||||
"adminConsole.list.table.genericLoading": "Loading",
|
||||
"adminConsole.list.table.genericNoData": "No data",
|
||||
"adminConsole.list.table.pagination.next": "Go to next page",
|
||||
"adminConsole.list.table.pagination.previous": "Go to previous page",
|
||||
"adminConsole.list.table.pinnedColumn": "This column is pinned",
|
||||
"adminConsole.list.table.rowCount.label": "Show {count} rows per page",
|
||||
"adminConsole.list.table.rowsCount.(show)rowsPerPage": "Show",
|
||||
"adminConsole.list.table.rowsCount.10": "10",
|
||||
@@ -3260,7 +3316,7 @@
|
||||
"create_comment.commentTitle": "Comment",
|
||||
"create_group_memberships_modal.cancel": "No",
|
||||
"create_group_memberships_modal.create": "Yes",
|
||||
"create_group_memberships_modal.desc": "This will add (or re-add) {username} to all of the available teams and channels based on their group memberships. You can revert this change at any time.",
|
||||
"create_group_memberships_modal.desc": "You're about to add or re-add {username} to teams and channels based on their LDAP group membership. You can revert this change at any time.",
|
||||
"create_group_memberships_modal.title": "Re-add {username} to teams and channels",
|
||||
"create_post.comment": "Comment",
|
||||
"create_post.deactivated": "You are viewing an archived channel with a **deactivated user**. New messages cannot be posted.",
|
||||
@@ -3339,8 +3395,8 @@
|
||||
"datetime.yesterday": "yesterday",
|
||||
"deactivate_member_modal.deactivate": "Deactivate",
|
||||
"deactivate_member_modal.desc": "This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system.\n",
|
||||
"deactivate_member_modal.desc.confirm": " Are you sure you want to deactivate {username}?",
|
||||
"deactivate_member_modal.desc.for_users_with_bot_accounts1": "This action deactivates {username}.",
|
||||
"deactivate_member_modal.desc.confirm": "Are you sure you want to deactivate {username}?",
|
||||
"deactivate_member_modal.desc.for_users_with_bot_accounts1": "This action deactivates {username}",
|
||||
"deactivate_member_modal.desc.for_users_with_bot_accounts2": "They will be logged out and not have access to any teams or channels on this system.",
|
||||
"deactivate_member_modal.desc.for_users_with_bot_accounts3": "Bot accounts they manage will be disabled along with their integrations. To enable them again, go to <linkBots>Integrations > Bot Accounts</linkBots>. <linkDocumentation>Learn more about bot accounts</linkDocumentation>.",
|
||||
"deactivate_member_modal.sso_warning": "You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.",
|
||||
@@ -3528,6 +3584,17 @@
|
||||
"error.plugin_not_found.title": "Plugin Not Found",
|
||||
"error.team_not_found.message": "The team you're requesting is private or does not exist. Please contact your Administrator for an invitation.",
|
||||
"error.team_not_found.title": "Team Not Found",
|
||||
"export_error_modal.desc": "We’re not able to initiate an export of this data at the moment. Please wait a few minutes and try again.",
|
||||
"export_error_modal.inProgress.desc": "You've already started an export of this data. Please wait a few more minutes to access the CSV file or to generate the report again.",
|
||||
"export_error_modal.inProgress.title": "Export is in progress",
|
||||
"export_error_modal.title": "Export could not be initiated",
|
||||
"export_user_data_modal.dange_range.all_time": "You're about to export user data for all time. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.",
|
||||
"export_user_data_modal.dange_range.last_30_days": "You're about to export user data for the last 30 days. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.",
|
||||
"export_user_data_modal.dange_range.last_6_months": "You're about to export user data for the last 6 months. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.",
|
||||
"export_user_data_modal.dange_range.previous_month": "You're about to export user data for the previous month. When the export is ready, a CSV file will be sent to you in a Mattermost direct message. This export will take a few minutes.",
|
||||
"export_user_data_modal.do_not_show": "Do not show this again",
|
||||
"export_user_data_modal.export_data": "Export data",
|
||||
"export_user_data_modal.title": "Export user data",
|
||||
"feature_restricted_modal.agreement": "By selecting <highlight>Try free for {trialLength} days</highlight>, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>Privacy Policy</linkPrivacy>, and receiving product emails.",
|
||||
"feature_restricted_modal.button.notify": "Notify admin",
|
||||
"feature_restricted_modal.button.plans": "View plans",
|
||||
@@ -3575,8 +3642,6 @@
|
||||
"filtered_user_list.next": "Next",
|
||||
"filtered_user_list.prev": "Previous",
|
||||
"filtered_user_list.search": "Search users",
|
||||
"filtered_user_list.team": "Team:",
|
||||
"filtered_user_list.userStatus": "User Status:",
|
||||
"FIVE_HUNDRED_TO_1000": "501-1000",
|
||||
"flag_post.flag": "Save Message",
|
||||
"flag_post.unflag": "Remove from Saved",
|
||||
@@ -5105,9 +5170,6 @@
|
||||
"system_notice.title": "Notice from Mattermost",
|
||||
"system_notice.title.gm_as_dm": "Updates to Group Messages",
|
||||
"system_noticy.body.gm_as_dm": "You will now be notified for all activity in your group messages along with a notification badge for every new message.{br}{br}You can configure this in notification preferences for each group message.",
|
||||
"system_users_list.count": "{count, number} {count, plural, one {user} other {users}}",
|
||||
"system_users_list.countPage": "{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total, number} total",
|
||||
"system_users_list.countSearch": "{count, number} {count, plural, one {user} other {users}} of {total, number} total",
|
||||
"tag.default.beta": "BETA",
|
||||
"tag.default.bot": "BOT",
|
||||
"tag.default.guest": "GUEST",
|
||||
@@ -5248,6 +5310,9 @@
|
||||
"update_outgoing_webhook.confirm": "Edit Outgoing Webhook",
|
||||
"update_outgoing_webhook.question": "Your changes may break the existing outgoing webhook. Are you sure you would like to update it?",
|
||||
"update_outgoing_webhook.update": "Update",
|
||||
"upgrade_export_data_modal.desc": "Export detailed data reports with ease and analyse user statistics conveniently. Upgrade to the Professional plan to gain access to data export.",
|
||||
"upgrade_export_data_modal.title": "Upgrade to export data reports",
|
||||
"upgrade_export_data_modal.view_plans": "View Plans",
|
||||
"upgradeLink.warn.upgrade_now": "Upgrade now",
|
||||
"upload_overlay.info": "Drop a file to upload it.",
|
||||
"upsell_advantages.more": "And more...",
|
||||
|
||||
@@ -77,6 +77,11 @@ const Preferences = {
|
||||
CATEGORY_DRAFTS: 'drafts',
|
||||
DRAFTS_TOUR_TIP_SHOWED: 'drafts_tour_tip_showed',
|
||||
|
||||
CATEGORY_REPORTING: 'reporting',
|
||||
|
||||
HIDE_BATCH_EXPORT_CONFIRM_MODAL: 'hide_batch_export_confirm_modal',
|
||||
HIDE_MYSQL_STATS_NOTIFICATION: 'hide_mysql_stats_notifcation',
|
||||
|
||||
CATEGORY_THEME: 'theme',
|
||||
THEMES: {
|
||||
denim: {
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
import type {AnyAction} from 'redux';
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import {CursorPaginationDirection} from '@mattermost/types/reports';
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
|
||||
const initialState = {
|
||||
import type {AdminConsoleUserManagementTableProperties} from 'types/store/views';
|
||||
|
||||
const navigationBlockInitialState = {
|
||||
blocked: false,
|
||||
onNavigationConfirmed: null,
|
||||
showNavigationPrompt: false,
|
||||
};
|
||||
|
||||
function navigationBlock(state = initialState, action: AnyAction) {
|
||||
function navigationBlock(state = navigationBlockInitialState, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_NAVIGATION_BLOCKED:
|
||||
return {...state, blocked: action.blocked};
|
||||
@@ -39,7 +43,7 @@ function navigationBlock(state = initialState, action: AnyAction) {
|
||||
};
|
||||
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return initialState;
|
||||
return navigationBlockInitialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -54,7 +58,36 @@ export function needsLoggedInLimitReachedCheck(state = false, action: AnyAction)
|
||||
}
|
||||
}
|
||||
|
||||
export const adminConsoleUserManagementTablePropertiesInitialState: AdminConsoleUserManagementTableProperties = {
|
||||
sortColumn: '',
|
||||
sortIsDescending: false,
|
||||
pageSize: 0,
|
||||
pageIndex: 0,
|
||||
cursorDirection: CursorPaginationDirection.next,
|
||||
cursorUserId: '',
|
||||
cursorColumnValue: '',
|
||||
columnVisibility: {},
|
||||
searchTerm: '',
|
||||
filterTeam: '',
|
||||
filterTeamLabel: '',
|
||||
filterStatus: '',
|
||||
filterRole: '',
|
||||
};
|
||||
|
||||
export function adminConsoleUserManagementTableProperties(state = adminConsoleUserManagementTablePropertiesInitialState, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_ADMIN_CONSOLE_USER_MANAGEMENT_TABLE_PROPERTIES: {
|
||||
return {...state, ...action.data};
|
||||
}
|
||||
case ActionTypes.CLEAR_ADMIN_CONSOLE_USER_MANAGEMENT_TABLE_PROPERTIES:
|
||||
return adminConsoleUserManagementTablePropertiesInitialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
navigationBlock,
|
||||
needsLoggedInLimitReachedCheck,
|
||||
adminConsoleUserManagementTableProperties,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ describe('Reducers.Search', () => {
|
||||
popoverSearch: '',
|
||||
channelMembersRhsSearch: '',
|
||||
modalFilters: {},
|
||||
systemUsersSearch: {},
|
||||
userGridSearch: {},
|
||||
teamListSearch: '',
|
||||
channelListSearch: {},
|
||||
@@ -21,7 +20,6 @@ describe('Reducers.Search', () => {
|
||||
const nextState = searchReducer(
|
||||
{
|
||||
modalSearch: '',
|
||||
systemUsersSearch: {},
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -63,19 +63,6 @@ function modalFilters(state: ViewsState['search']['modalFilters'] = {}, action:
|
||||
}
|
||||
}
|
||||
|
||||
function systemUsersSearch(state: Partial<ViewsState['search']['systemUsersSearch']> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case SearchTypes.SET_SYSTEM_USERS_SEARCH: {
|
||||
return action.data;
|
||||
}
|
||||
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function userGridSearch(state: Partial<ViewsState['search']['userGridSearch']> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case SearchTypes.SET_USER_GRID_SEARCH: {
|
||||
@@ -142,7 +129,6 @@ export default combineReducers({
|
||||
popoverSearch,
|
||||
channelMembersRhsSearch,
|
||||
modalFilters,
|
||||
systemUsersSearch,
|
||||
userGridSearch,
|
||||
teamListSearch,
|
||||
channelListSearch,
|
||||
|
||||
@@ -111,3 +111,17 @@ input::-webkit-file-upload-button {
|
||||
.input-group-addon {
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
|
||||
// This class is used to make the input field look like a dropdown to be used in menus
|
||||
.inputWithMenu {
|
||||
cursor: pointer;
|
||||
|
||||
input.form-control {
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
|
||||
&:read-only {
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,18 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > .admin-console__container {
|
||||
overflow: auto;
|
||||
.admin-console__filters-rows {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding-block-end: 12px;
|
||||
padding-block-start: 24px;
|
||||
padding-inline-end: 24px;
|
||||
padding-inline-start: 24px;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
.admin-console__container {
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
box-shadow: 0 2px 3px 0 alpha-color($black, 0.08);
|
||||
}
|
||||
@@ -143,7 +153,9 @@
|
||||
|
||||
&:focus:not(.Input) {
|
||||
border-color: #66afe9;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||
box-shadow:
|
||||
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||
0 0 8px rgba(102, 175, 233, 0.6);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@@ -692,7 +704,7 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
>svg {
|
||||
> svg {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
@@ -875,9 +887,13 @@
|
||||
}
|
||||
|
||||
.system-users__filter-row {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
display: flex;
|
||||
margin-right: 15px;
|
||||
margin-left: 15px;
|
||||
padding-block-end: 12px;
|
||||
padding-block-start: 24px;
|
||||
padding-inline-end: 24px;
|
||||
padding-inline-start: 24px;
|
||||
|
||||
.system-users__filter {
|
||||
flex: 1 1 140px;
|
||||
@@ -1023,7 +1039,7 @@
|
||||
border-width: 5px 5px 2.5px;
|
||||
border-style: solid;
|
||||
border-color: #999 transparent transparent;
|
||||
content: '';
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,3 +18,7 @@ export function getOnNavigationConfirmed(state: GlobalState) {
|
||||
export function getNeedsLoggedInLimitReachedCheck(state: GlobalState): boolean {
|
||||
return state.views.admin.needsLoggedInLimitReachedCheck;
|
||||
}
|
||||
|
||||
export function getAdminConsoleUserManagementTableProperties(state: GlobalState) {
|
||||
return state.views.admin.adminConsoleUserManagementTableProperties;
|
||||
}
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user