From c7f24dfa061af561b9a5c4646a3216b2d5fbadc5 Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Fri, 8 Dec 2023 17:29:58 +0000 Subject: [PATCH] [MM-56028] Decouple system_users components for drop in replacement of list (#25613) --- .../__snapshots__/system_users.test.tsx.snap | 21 ++- .../system_users/system_users.test.tsx | 16 +- .../system_users/system_users.tsx | 158 ++++++++++-------- .../system_users_filter_role/index.tsx | 59 +++++++ .../system_users_filter_team/index.tsx | 66 ++++++++ .../system_users_list.test.tsx.snap | 9 +- .../{list => system_users_list}/index.ts | 0 .../selectors.test.ts} | 2 +- .../selectors.ts} | 0 .../system_users_list.test.tsx | 2 +- .../system_users_list.tsx | 5 +- .../system_users_search/index.tsx | 55 ++++++ .../searchable_user_list.tsx | 51 +++--- 13 files changed, 330 insertions(+), 114 deletions(-) create mode 100644 webapp/channels/src/components/admin_console/system_users/system_users_filter_role/index.tsx create mode 100644 webapp/channels/src/components/admin_console/system_users/system_users_filter_team/index.tsx rename webapp/channels/src/components/admin_console/system_users/{list => system_users_list}/__snapshots__/system_users_list.test.tsx.snap (98%) rename webapp/channels/src/components/admin_console/system_users/{list => system_users_list}/index.ts (100%) rename webapp/channels/src/components/admin_console/system_users/{list/selectors.test.tsx => system_users_list/selectors.test.ts} (98%) rename webapp/channels/src/components/admin_console/system_users/{list/selectors.tsx => system_users_list/selectors.ts} (100%) rename webapp/channels/src/components/admin_console/system_users/{list => system_users_list}/system_users_list.test.tsx (98%) rename webapp/channels/src/components/admin_console/system_users/{list => system_users_list}/system_users_list.tsx (97%) create mode 100644 webapp/channels/src/components/admin_console/system_users/system_users_search/index.tsx diff --git a/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap b/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap index d559c8bd42..0143fca439 100644 --- a/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/system_users/__snapshots__/system_users.test.tsx.snap @@ -25,6 +25,26 @@ exports[`components/admin_console/system_users should match default snapshot 1`]
+
+ + + +
{ test('should match default snapshot', () => { const props = defaultProps; - const wrapper = shallowWithIntl(); + const wrapper = shallow(); 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 = shallowWithIntl(); + const wrapper = shallow(); wrapper.setState({loading: true}); @@ -66,7 +66,7 @@ describe('components/admin_console/system_users', () => { test('loadDataForTeam() should have called loadProfilesWithoutTeam', async () => { const loadProfilesWithoutTeam = jest.fn().mockResolvedValue(undefined); const props = {...defaultProps, actions: {...defaultProps.actions, loadProfilesWithoutTeam}}; - const wrapper = shallowWithIntl(); + const wrapper = shallow(); wrapper.setState({loading: true}); @@ -91,7 +91,7 @@ describe('components/admin_console/system_users', () => { teamId: SearchUserTeamFilter.ALL_USERS, actions: {...defaultProps.actions, getProfiles}, }; - const wrapper = shallowWithIntl(); + const wrapper = shallow(); wrapper.setState({loading: true}); @@ -111,7 +111,7 @@ describe('components/admin_console/system_users', () => { teamId: SearchUserTeamFilter.NO_TEAM, actions: {...defaultProps.actions, loadProfilesWithoutTeam}, }; - const wrapper = shallowWithIntl(); + const wrapper = shallow(); wrapper.setState({loading: true}); @@ -131,7 +131,7 @@ describe('components/admin_console/system_users', () => { teamId: SearchUserTeamFilter.NO_TEAM, actions: {...defaultProps.actions, searchProfiles}, }; - const wrapper = shallowWithIntl(); + const wrapper = shallow(); const instance = wrapper.instance() as SystemUserClass; @@ -149,7 +149,7 @@ describe('components/admin_console/system_users', () => { teamId: SearchUserTeamFilter.NO_TEAM, actions: {...defaultProps.actions, searchProfiles}, }; - const wrapper = shallowWithIntl(); + const wrapper = shallow(); const instance = wrapper.instance() as SystemUserClass; diff --git a/webapp/channels/src/components/admin_console/system_users/system_users.tsx b/webapp/channels/src/components/admin_console/system_users/system_users.tsx index 25cb956123..8331254a12 100644 --- a/webapp/channels/src/components/admin_console/system_users/system_users.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users.tsx @@ -1,8 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {type ChangeEvent} from 'react'; -import {FormattedMessage, type IntlShape, injectIntl} from 'react-intl'; +import React from 'react'; +import type {ChangeEvent} from 'react'; +import {FormattedMessage} from 'react-intl'; import type {ServerError} from '@mattermost/types/errors'; import type {Team} from '@mattermost/types/teams'; @@ -13,19 +14,20 @@ import type {ActionFunc} from 'mattermost-redux/types/actions'; import AdminHeader from 'components/widgets/admin_console/admin_header'; -import {Constants, UserSearchOptions, SearchUserTeamFilter, UserFilters} from 'utils/constants'; +import {Constants, UserSearchOptions, SearchUserTeamFilter} from 'utils/constants'; import {getUserOptionsFromFilter, searchUserOptionsFromFilter} from 'utils/filter_users'; -import SystemUsersList from './list'; 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'; const USER_ID_LENGTH = 26; const USERS_PER_PAGE = 50; type Props = { - intl: IntlShape; - /** * Array of team objects */ @@ -168,6 +170,14 @@ export class SystemUsers extends React.PureComponent { 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; @@ -190,6 +200,56 @@ export class SystemUsers extends React.PureComponent { 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, + }; + + 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}); + }; + + 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; @@ -238,67 +298,6 @@ export class SystemUsers extends React.PureComponent { this.getUserById(id); }; - renderFilterRow = (doSearch: ((event: React.FormEvent) => void) | undefined) => { - const teams = this.props.teams.map((team) => ( - - )); - - return ( -
-
- -
- - -
- ); - }; - render() { return (
@@ -306,18 +305,33 @@ export class SystemUsers extends React.PureComponent {
+
+ + + +
{ } } -export default injectIntl(SystemUsers); +export default SystemUsers; diff --git a/webapp/channels/src/components/admin_console/system_users/system_users_filter_role/index.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_filter_role/index.tsx new file mode 100644 index 0000000000..c48dd6b6b1 --- /dev/null +++ b/webapp/channels/src/components/admin_console/system_users/system_users_filter_role/index.tsx @@ -0,0 +1,59 @@ +// 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; +}; + +function SystemUsersFilterRole(props: Props) { + const {formatMessage} = useIntl(); + + function handleChange(e: ChangeEvent) { + const filter = e?.target?.value ?? ''; + props.onChange({filter}); + props.onFilter({filter}); + } + + return ( + + ); +} + +export default SystemUsersFilterRole; diff --git a/webapp/channels/src/components/admin_console/system_users/system_users_filter_team/index.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_filter_team/index.tsx new file mode 100644 index 0000000000..870b0695b7 --- /dev/null +++ b/webapp/channels/src/components/admin_console/system_users/system_users_filter_team/index.tsx @@ -0,0 +1,66 @@ +// 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; +}; + +function SystemUsersFilterTeam(props: Props) { + const {formatMessage} = useIntl(); + + function handleChange(e: ChangeEvent) { + const teamId = e?.target?.value ?? ''; + props.onChange({teamId}); + props.onFilter({teamId}); + } + + return ( + + ); +} + +export default SystemUsersFilterTeam; diff --git a/webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap b/webapp/channels/src/components/admin_console/system_users/system_users_list/__snapshots__/system_users_list.test.tsx.snap similarity index 98% rename from webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap rename to webapp/channels/src/components/admin_console/system_users/system_users_list/__snapshots__/system_users_list.test.tsx.snap index d73a8006f0..8905b2f523 100644 --- a/webapp/channels/src/components/admin_console/system_users/list/__snapshots__/system_users_list.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/system_users/system_users_list/__snapshots__/system_users_list.test.tsx.snap @@ -33,6 +33,7 @@ exports[`components/admin_console/system_users/list should match default snapsho isDisabled={false} mfaEnabled={false} nextPage={[Function]} + noBuiltInFilters={true} onTermChange={[MockFunction]} page={0} previousPage={[Function]} @@ -46,7 +47,7 @@ exports[`components/admin_console/system_users/list should match default snapsho "type": [Function], } } - search={[Function]} + search={[MockFunction]} teamId="" term="" total={0} @@ -214,6 +215,7 @@ exports[`components/admin_console/system_users/list should match default snapsho isDisabled={false} mfaEnabled={false} nextPage={[Function]} + noBuiltInFilters={true} onTermChange={[MockFunction]} page={0} previousPage={[Function]} @@ -227,7 +229,7 @@ exports[`components/admin_console/system_users/list should match default snapsho "type": [Function], } } - search={[Function]} + search={[MockFunction]} teamId="" term="" total={0} @@ -441,6 +443,7 @@ exports[`components/admin_console/system_users/list should match default snapsho isDisabled={false} mfaEnabled={true} nextPage={[Function]} + noBuiltInFilters={true} onTermChange={[MockFunction]} page={0} previousPage={[Function]} @@ -454,7 +457,7 @@ exports[`components/admin_console/system_users/list should match default snapsho "type": [Function], } } - search={[Function]} + search={[MockFunction]} teamId="" term="" total={0} diff --git a/webapp/channels/src/components/admin_console/system_users/list/index.ts b/webapp/channels/src/components/admin_console/system_users/system_users_list/index.ts similarity index 100% rename from webapp/channels/src/components/admin_console/system_users/list/index.ts rename to webapp/channels/src/components/admin_console/system_users/system_users_list/index.ts diff --git a/webapp/channels/src/components/admin_console/system_users/list/selectors.test.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_list/selectors.test.ts similarity index 98% rename from webapp/channels/src/components/admin_console/system_users/list/selectors.test.tsx rename to webapp/channels/src/components/admin_console/system_users/system_users_list/selectors.test.ts index c97d596867..92c696f609 100644 --- a/webapp/channels/src/components/admin_console/system_users/list/selectors.test.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users_list/selectors.test.ts @@ -6,7 +6,7 @@ import type {UserProfile} from '@mattermost/types/users'; import * as users from 'mattermost-redux/selectors/entities/users'; -import {getUsers} from 'components/admin_console/system_users/list/selectors'; +import {getUsers} from 'components/admin_console/system_users/system_users_list/selectors'; jest.mock('mattermost-redux/selectors/entities/users'); diff --git a/webapp/channels/src/components/admin_console/system_users/list/selectors.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_list/selectors.ts similarity index 100% rename from webapp/channels/src/components/admin_console/system_users/list/selectors.tsx rename to webapp/channels/src/components/admin_console/system_users/system_users_list/selectors.ts diff --git a/webapp/channels/src/components/admin_console/system_users/list/system_users_list.test.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_list/system_users_list.test.tsx similarity index 98% rename from webapp/channels/src/components/admin_console/system_users/list/system_users_list.test.tsx rename to webapp/channels/src/components/admin_console/system_users/system_users_list/system_users_list.test.tsx index f14bb50dc2..84c1aff931 100644 --- a/webapp/channels/src/components/admin_console/system_users/list/system_users_list.test.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users_list/system_users_list.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import type {UserProfile} from '@mattermost/types/users'; -import SystemUsersList from 'components/admin_console/system_users/list/system_users_list'; +import SystemUsersList from 'components/admin_console/system_users/system_users_list/system_users_list'; import {Constants} from 'utils/constants'; diff --git a/webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx b/webapp/channels/src/components/admin_console/system_users/system_users_list/system_users_list.tsx similarity index 97% rename from webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx rename to webapp/channels/src/components/admin_console/system_users/system_users_list/system_users_list.tsx index 38d20105fb..62c07bd193 100644 --- a/webapp/channels/src/components/admin_console/system_users/list/system_users_list.tsx +++ b/webapp/channels/src/components/admin_console/system_users/system_users_list/system_users_list.tsx @@ -31,7 +31,6 @@ type Props = { nextPage: (page: number) => void; search: (term: string) => void; focusOnMount?: boolean; - renderFilterRow: (doSearch: ((event: React.FormEvent) => void) | undefined) => JSX.Element; teamId: string; filter: string; @@ -347,11 +346,9 @@ export default class SystemUsersList extends React.PureComponent { }} nextPage={this.nextPage} previousPage={this.previousPage} - search={this.search} page={this.state.page} - term={this.props.term} - onTermChange={this.props.onTermChange} rowComponentType={UserListRowWithError} + noBuiltInFilters={true} /> void; + onSearch: (value: string) => void; +}; + +function SystemUsersSearch(props: Props) { + const {formatMessage} = useIntl(); + + const debouncedSearch = useCallback(debounce((value: string) => { + props.onSearch(value); + }, Constants.SEARCH_TIMEOUT_MILLISECONDS), []); + + useEffect(() => { + return () => { + debouncedSearch.cancel(); + }; + }, []); + + function handleChange(e: ChangeEvent) { + const searchTerm = e?.target?.value?.trim() ?? ''; + props.onChange({searchTerm}); + + if (searchTerm.length > 0) { + debouncedSearch(searchTerm); + } + } + + return ( +
+ +
+ ); +} + +export default SystemUsersSearch; diff --git a/webapp/channels/src/components/searchable_user_list/searchable_user_list.tsx b/webapp/channels/src/components/searchable_user_list/searchable_user_list.tsx index ec20dc6169..dcee4cbe92 100644 --- a/webapp/channels/src/components/searchable_user_list/searchable_user_list.tsx +++ b/webapp/channels/src/components/searchable_user_list/searchable_user_list.tsx @@ -24,6 +24,7 @@ type Props = { previousPage: () => void; search: (term: string) => void; actions?: React.ReactNode[]; + noBuiltInFilters?: boolean; actionProps?: { mfaEnabled: boolean; enableUserAccessTokens: boolean; @@ -274,31 +275,33 @@ class SearchableUserList extends React.PureComponent { } let filterRow; - if (this.props.renderFilterRow) { - filterRow = this.props.renderFilterRow(this.handleInput); - } else { - filterRow = ( -
- - -
- ); +
+ ); + } } return (