Configuration: Prevent browser hanging / crashing with large number of org users (#32546)

* fix(userslist): introduce pagination to prevent browser crash with large number of users

* test(userlist): fix failing tests

* refactor(userlist): use layout components for spacing

* test(userslist): update snapshots
This commit is contained in:
Jack Westbrook 2021-04-01 14:46:55 +02:00 committed by GitHub
parent 4af817de2e
commit 9caf2f8b3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 16 deletions

View File

@ -5,7 +5,7 @@ import { Invitee, OrgUser } from 'app/types';
// import { getMockUser } from './__mocks__/userMocks';
import { NavModel } from '@grafana/data';
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { setUsersSearchQuery } from './state/reducers';
import { setUsersSearchPage, setUsersSearchQuery } from './state/reducers';
jest.mock('../../core/app_events', () => ({
emit: jest.fn(),
@ -24,12 +24,14 @@ const setup = (propOverrides?: object) => {
users: [] as OrgUser[],
invitees: [] as Invitee[],
searchQuery: '',
searchPage: 1,
externalUserMngInfo: '',
loadInvitees: jest.fn(),
loadUsers: jest.fn(),
updateUser: jest.fn(),
removeUser: jest.fn(),
setUsersSearchQuery: mockToolkitActionCreator(setUsersSearchQuery),
setUsersSearchPage: mockToolkitActionCreator(setUsersSearchPage),
hasFetched: false,
};

View File

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { NavModel, renderMarkdown } from '@grafana/data';
import { HorizontalGroup, Pagination, VerticalGroup } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import UsersActionBar from './UsersActionBar';
@ -10,19 +11,21 @@ import InviteesTable from './InviteesTable';
import { Invitee, OrgUser, OrgRole } from 'app/types';
import { loadInvitees, loadUsers, removeUser, updateUser } from './state/actions';
import { getNavModel } from 'app/core/selectors/navModel';
import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors';
import { setUsersSearchQuery } from './state/reducers';
import { getInvitees, getUsers, getUsersSearchQuery, getUsersSearchPage } from './state/selectors';
import { setUsersSearchQuery, setUsersSearchPage } from './state/reducers';
export interface Props {
navModel: NavModel;
invitees: Invitee[];
users: OrgUser[];
searchQuery: string;
searchPage: number;
externalUserMngInfo: string;
hasFetched: boolean;
loadUsers: typeof loadUsers;
loadInvitees: typeof loadInvitees;
setUsersSearchQuery: typeof setUsersSearchQuery;
setUsersSearchPage: typeof setUsersSearchPage;
updateUser: typeof updateUser;
removeUser: typeof removeUser;
}
@ -31,6 +34,8 @@ export interface State {
showInvites: boolean;
}
const pageLimit = 30;
export class UsersListPage extends PureComponent<Props, State> {
externalUserMngInfoHtml: string;
@ -71,18 +76,35 @@ export class UsersListPage extends PureComponent<Props, State> {
}));
};
getPaginatedUsers = (users: OrgUser[]) => {
const offset = (this.props.searchPage - 1) * pageLimit;
return users.slice(offset, offset + pageLimit);
};
renderTable() {
const { invitees, users } = this.props;
const { invitees, users, setUsersSearchPage } = this.props;
const paginatedUsers = this.getPaginatedUsers(users);
const totalPages = Math.ceil(users.length / pageLimit);
if (this.state.showInvites) {
return <InviteesTable invitees={invitees} />;
} else {
return (
<UsersTable
users={users}
onRoleChange={(role, user) => this.onRoleChange(role, user)}
onRemoveUser={(user) => this.props.removeUser(user.userId)}
/>
<VerticalGroup spacing="md">
<UsersTable
users={paginatedUsers}
onRoleChange={(role, user) => this.onRoleChange(role, user)}
onRemoveUser={(user) => this.props.removeUser(user.userId)}
/>
<HorizontalGroup justify="flex-end">
<Pagination
onNavigate={setUsersSearchPage}
currentPage={this.props.searchPage}
numberOfPages={totalPages}
hideWhenSinglePage={true}
/>
</HorizontalGroup>
</VerticalGroup>
);
}
}
@ -112,6 +134,7 @@ function mapStateToProps(state: any) {
navModel: getNavModel(state.navIndex, 'users'),
users: getUsers(state.users),
searchQuery: getUsersSearchQuery(state.users),
searchPage: getUsersSearchPage(state.users),
invitees: getInvitees(state.users),
externalUserMngInfo: state.users.externalUserMngInfo,
hasFetched: state.users.hasFetched,
@ -122,6 +145,7 @@ const mapDispatchToProps = {
loadUsers,
loadInvitees,
setUsersSearchQuery,
setUsersSearchPage,
updateUser,
removeUser,
};

View File

@ -20,11 +20,25 @@ exports[`Render should render List page 1`] = `
onShowInvites={[Function]}
showInvites={false}
/>
<UsersTable
onRemoveUser={[Function]}
onRoleChange={[Function]}
users={Array []}
/>
<VerticalGroup
spacing="md"
>
<UsersTable
onRemoveUser={[Function]}
onRoleChange={[Function]}
users={Array []}
/>
<HorizontalGroup
justify="flex-end"
>
<Pagination
currentPage={1}
hideWhenSinglePage={true}
numberOfPages={0}
onNavigate={[MockFunction]}
/>
</HorizontalGroup>
</VerticalGroup>
</PageContents>
</Page>
`;

View File

@ -7,6 +7,7 @@ export const initialState: UsersState = {
invitees: [] as Invitee[],
users: [] as OrgUser[],
searchQuery: '',
searchPage: 1,
canInvite: !config.externalUserMngLinkName,
externalUserMngInfo: config.externalUserMngInfo,
externalUserMngLinkName: config.externalUserMngLinkName,
@ -25,12 +26,16 @@ const usersSlice = createSlice({
return { ...state, hasFetched: true, invitees: action.payload };
},
setUsersSearchQuery: (state, action: PayloadAction<string>): UsersState => {
return { ...state, searchQuery: action.payload };
// reset searchPage otherwise search results won't appear
return { ...state, searchQuery: action.payload, searchPage: initialState.searchPage };
},
setUsersSearchPage: (state, action: PayloadAction<number>): UsersState => {
return { ...state, searchPage: action.payload };
},
},
});
export const { inviteesLoaded, setUsersSearchQuery, usersLoaded } = usersSlice.actions;
export const { inviteesLoaded, setUsersSearchQuery, setUsersSearchPage, usersLoaded } = usersSlice.actions;
export const usersReducer = usersSlice.reducer;

View File

@ -18,3 +18,4 @@ export const getInvitees = (state: UsersState) => {
export const getInviteesCount = (state: UsersState) => state.invitees.length;
export const getUsersSearchQuery = (state: UsersState) => state.searchQuery;
export const getUsersSearchPage = (state: UsersState) => state.searchPage;

View File

@ -61,6 +61,7 @@ export interface UsersState {
users: OrgUser[];
invitees: Invitee[];
searchQuery: string;
searchPage: number;
canInvite: boolean;
externalUserMngLinkUrl: string;
externalUserMngLinkName: string;