mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4af817de2e
commit
9caf2f8b3a
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -61,6 +61,7 @@ export interface UsersState {
|
||||
users: OrgUser[];
|
||||
invitees: Invitee[];
|
||||
searchQuery: string;
|
||||
searchPage: number;
|
||||
canInvite: boolean;
|
||||
externalUserMngLinkUrl: string;
|
||||
externalUserMngLinkName: string;
|
||||
|
Loading…
Reference in New Issue
Block a user