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 (