Admin: Use backend sort (#75228)

* Admin: Use InteractiveTable

* Admin: Fix pagination

* Admin: Use CellWrapper

* Admin: Split components

* Admin: Separate OrgUsersTable

* Admin: Remove UsersTable

* Admin: Use the new table for TeamList

* Admin: Cleanup TeamList page

* Admin: Add edit team action

* Admin: Use explicit edit action instead of a link wrapper

* Admin: Fix responsive styles

* Cleanup

* Remove redundant sort

* Add item key

* Fix icon styles

* Set loading by default

* Use separate pagination component

* Use default sorting functionality

* Fix merge conflicts

* Update betterer

* Add controlled sort

* Revert pagination changes

* Move pagination inside OrgUsersTable.tsx

* Add missing prop

* Sort users table

* Fix tests

* Remove loader

* Add sort to Teams

* Cleanup

* Update loadTeams action

* Remove sort condition
This commit is contained in:
Alex Khomenko 2023-09-29 11:48:36 +02:00 committed by GitHub
parent 5796836662
commit 4a692fc82e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 151 additions and 68 deletions

View File

@ -8,11 +8,10 @@ import { LinkButton, RadioButtonGroup, useStyles2, FilterInput } from '@grafana/
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import PageLoader from '../../core/components/PageLoader/PageLoader';
import { AccessControlAction, StoreState, UserFilter } from '../../types'; import { AccessControlAction, StoreState, UserFilter } from '../../types';
import { UsersTable } from './Users/UsersTable'; import { UsersTable } from './Users/UsersTable';
import { changeFilter, changePage, changeQuery, fetchUsers } from './state/actions'; import { changeFilter, changePage, changeQuery, changeSort, fetchUsers } from './state/actions';
export interface FilterProps { export interface FilterProps {
filters: UserFilter[]; filters: UserFilter[];
@ -31,6 +30,7 @@ const mapDispatchToProps = {
changeQuery, changeQuery,
changePage, changePage,
changeFilter, changeFilter,
changeSort,
}; };
const mapStateToProps = (state: StoreState) => ({ const mapStateToProps = (state: StoreState) => ({
@ -40,7 +40,6 @@ const mapStateToProps = (state: StoreState) => ({
totalPages: state.userListAdmin.totalPages, totalPages: state.userListAdmin.totalPages,
page: state.userListAdmin.page, page: state.userListAdmin.page,
filters: state.userListAdmin.filters, filters: state.userListAdmin.filters,
isLoading: state.userListAdmin.isLoading,
}); });
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
@ -57,10 +56,10 @@ const UserListAdminPageUnConnected = ({
showPaging, showPaging,
changeFilter, changeFilter,
filters, filters,
isLoading,
totalPages, totalPages,
page, page,
changePage, changePage,
changeSort,
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -97,17 +96,14 @@ const UserListAdminPageUnConnected = ({
)} )}
</div> </div>
</div> </div>
{isLoading ? (
<PageLoader />
) : (
<UsersTable <UsersTable
users={users} users={users}
showPaging={showPaging} showPaging={showPaging}
totalPages={totalPages} totalPages={totalPages}
onChangePage={changePage} onChangePage={changePage}
currentPage={page} currentPage={page}
fetchData={changeSort}
/> />
)}
</Page.Contents> </Page.Contents>
); );
}; };

View File

@ -13,6 +13,7 @@ import {
Tag, Tag,
InteractiveTable, InteractiveTable,
Column, Column,
FetchDataFunc,
Pagination, Pagination,
HorizontalGroup, HorizontalGroup,
VerticalGroup, VerticalGroup,
@ -53,16 +54,24 @@ export interface Props {
orgId?: number; orgId?: number;
onRoleChange: (role: OrgRole, user: OrgUser) => void; onRoleChange: (role: OrgRole, user: OrgUser) => void;
onRemoveUser: (user: OrgUser) => void; onRemoveUser: (user: OrgUser) => void;
fetchData?: FetchDataFunc<OrgUser>;
changePage: (page: number) => void; changePage: (page: number) => void;
page: number; page: number;
totalPages: number; totalPages: number;
} }
export const OrgUsersTable = ({ users, orgId, onRoleChange, onRemoveUser, changePage, page, totalPages }: Props) => { export const OrgUsersTable = ({
users,
orgId,
onRoleChange,
onRemoveUser,
fetchData,
changePage,
page,
totalPages,
}: Props) => {
const [userToRemove, setUserToRemove] = useState<OrgUser | null>(null); const [userToRemove, setUserToRemove] = useState<OrgUser | null>(null);
const [roleOptions, setRoleOptions] = useState<Role[]>([]); const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const enableSort = totalPages === 1;
const styles = useStyles2(getStyles);
useEffect(() => { useEffect(() => {
async function fetchOptions() { async function fetchOptions() {
@ -91,27 +100,25 @@ export const OrgUsersTable = ({ users, orgId, onRoleChange, onRemoveUser, change
id: 'login', id: 'login',
header: 'Login', header: 'Login',
cell: ({ cell: { value } }: Cell<'login'>) => <div>{value}</div>, cell: ({ cell: { value } }: Cell<'login'>) => <div>{value}</div>,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'email', id: 'email',
header: 'Email', header: 'Email',
cell: ({ cell: { value } }: Cell<'email'>) => value, cell: ({ cell: { value } }: Cell<'email'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'name', id: 'name',
header: 'Name', header: 'Name',
cell: ({ cell: { value } }: Cell<'name'>) => value, cell: ({ cell: { value } }: Cell<'name'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'lastSeenAtAge', id: 'lastSeenAtAge',
header: 'Last active', header: 'Last active',
cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => value, cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => value,
sortType: enableSort sortType: (a, b) => new Date(a.original.lastSeenAt).getTime() - new Date(b.original.lastSeenAt).getTime(),
? (a, b) => new Date(a.original.lastSeenAt).getTime() - new Date(b.original.lastSeenAt).getTime()
: undefined,
}, },
{ {
id: 'role', id: 'role',
@ -175,17 +182,15 @@ export const OrgUsersTable = ({ users, orgId, onRoleChange, onRemoveUser, change
}, },
}, },
], ],
[orgId, roleOptions, onRoleChange, enableSort] [orgId, roleOptions, onRoleChange]
); );
return ( return (
<VerticalGroup spacing="md" data-testid={selectors.container}> <VerticalGroup spacing="md" data-testid={selectors.container}>
<div className={styles.wrapper}> <InteractiveTable columns={columns} data={users} getRowId={(user) => String(user.userId)} fetchData={fetchData} />
<InteractiveTable columns={columns} data={users} getRowId={(user) => String(user.userId)} /> <HorizontalGroup justify="flex-end">
<HorizontalGroup justify="flex-end" height={'auto'}>
<Pagination onNavigate={changePage} currentPage={page} numberOfPages={totalPages} hideWhenSinglePage={true} /> <Pagination onNavigate={changePage} currentPage={page} numberOfPages={totalPages} hideWhenSinglePage={true} />
</HorizontalGroup> </HorizontalGroup>
</div>
{Boolean(userToRemove) && ( {Boolean(userToRemove) && (
<ConfirmModal <ConfirmModal
body={`Are you sure you want to delete user ${userToRemove?.login}?`} body={`Are you sure you want to delete user ${userToRemove?.login}?`}

View File

@ -13,6 +13,7 @@ import {
Column, Column,
VerticalGroup, VerticalGroup,
HorizontalGroup, HorizontalGroup,
FetchDataFunc,
} from '@grafana/ui'; } from '@grafana/ui';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { UserDTO } from 'app/types'; import { UserDTO } from 'app/types';
@ -28,11 +29,18 @@ interface UsersTableProps {
totalPages: number; totalPages: number;
onChangePage: (page: number) => void; onChangePage: (page: number) => void;
currentPage: number; currentPage: number;
fetchData?: FetchDataFunc<UserDTO>;
} }
export const UsersTable = ({ users, showPaging, totalPages, onChangePage, currentPage }: UsersTableProps) => { export const UsersTable = ({
users,
showPaging,
totalPages,
onChangePage,
currentPage,
fetchData,
}: UsersTableProps) => {
const showLicensedRole = useMemo(() => users.some((user) => user.licensedRole), [users]); const showLicensedRole = useMemo(() => users.some((user) => user.licensedRole), [users]);
const enableSort = totalPages === 1;
const columns: Array<Column<UserDTO>> = useMemo( const columns: Array<Column<UserDTO>> = useMemo(
() => [ () => [
{ {
@ -44,25 +52,25 @@ export const UsersTable = ({ users, showPaging, totalPages, onChangePage, curren
id: 'login', id: 'login',
header: 'Login', header: 'Login',
cell: ({ cell: { value } }: Cell<'login'>) => value, cell: ({ cell: { value } }: Cell<'login'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'email', id: 'email',
header: 'Email', header: 'Email',
cell: ({ cell: { value } }: Cell<'email'>) => value, cell: ({ cell: { value } }: Cell<'email'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'name', id: 'name',
header: 'Name', header: 'Name',
cell: ({ cell: { value } }: Cell<'name'>) => value, cell: ({ cell: { value } }: Cell<'name'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'orgs', id: 'orgs',
header: 'Belongs to', header: 'Belongs to',
cell: OrgUnitsCell, cell: OrgUnitsCell,
sortType: enableSort ? (a, b) => (a.original.orgs?.length || 0) - (b.original.orgs?.length || 0) : undefined, sortType: (a, b) => (a.original.orgs?.length || 0) - (b.original.orgs?.length || 0),
}, },
...(showLicensedRole ...(showLicensedRole
? [ ? [
@ -71,7 +79,7 @@ export const UsersTable = ({ users, showPaging, totalPages, onChangePage, curren
header: 'Licensed role', header: 'Licensed role',
cell: LicensedRoleCell, cell: LicensedRoleCell,
// Needs the assertion here, the types are not inferred correctly due to the conditional assignment // Needs the assertion here, the types are not inferred correctly due to the conditional assignment
sortType: enableSort ? ('string' as const) : undefined, sortType: 'string' as const,
}, },
] ]
: []), : []),
@ -83,9 +91,7 @@ export const UsersTable = ({ users, showPaging, totalPages, onChangePage, curren
iconName: 'question-circle', iconName: 'question-circle',
}, },
cell: LastSeenAtCell, cell: LastSeenAtCell,
sortType: enableSort sortType: (a, b) => new Date(a.original.lastSeenAt!).getTime() - new Date(b.original.lastSeenAt!).getTime(),
? (a, b) => new Date(a.original.lastSeenAt!).getTime() - new Date(b.original.lastSeenAt!).getTime()
: undefined,
}, },
{ {
id: 'authLabels', id: 'authLabels',
@ -113,11 +119,11 @@ export const UsersTable = ({ users, showPaging, totalPages, onChangePage, curren
}, },
}, },
], ],
[showLicensedRole, enableSort] [showLicensedRole]
); );
return ( return (
<VerticalGroup spacing={'md'}> <VerticalGroup spacing={'md'}>
<InteractiveTable columns={columns} data={users} getRowId={(user) => String(user.id)} /> <InteractiveTable columns={columns} data={users} getRowId={(user) => String(user.id)} fetchData={fetchData} />
{showPaging && ( {showPaging && (
<HorizontalGroup justify={'flex-end'}> <HorizontalGroup justify={'flex-end'}>
<Pagination numberOfPages={totalPages} currentPage={currentPage} onNavigate={onChangePage} /> <Pagination numberOfPages={totalPages} currentPage={currentPage} onNavigate={onChangePage} />

View File

@ -2,6 +2,7 @@ import { debounce } from 'lodash';
import { dateTimeFormatTimeAgo } from '@grafana/data'; import { dateTimeFormatTimeAgo } from '@grafana/data';
import { featureEnabled, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { featureEnabled, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { FetchDataArgs } from '@grafana/ui';
import config from 'app/core/config'; import config from 'app/core/config';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { accessControlQueryParam } from 'app/core/utils/accessControl';
@ -26,6 +27,7 @@ import {
filterChanged, filterChanged,
usersFetchBegin, usersFetchBegin,
usersFetchEnd, usersFetchEnd,
sortChanged,
} from './reducers'; } from './reducers';
// UserAdminPage // UserAdminPage
@ -281,10 +283,12 @@ const getFilters = (filters: UserFilter[]) => {
export function fetchUsers(): ThunkResult<void> { export function fetchUsers(): ThunkResult<void> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
try { try {
const { perPage, page, query, filters } = getState().userListAdmin; const { perPage, page, query, filters, sort } = getState().userListAdmin;
const result = await getBackendSrv().get( let url = `/api/users/search?perpage=${perPage}&page=${page}&query=${query}&${getFilters(filters)}`;
`/api/users/search?perpage=${perPage}&page=${page}&query=${query}&${getFilters(filters)}` if (sort) {
); url += `&sort=${sort}`;
}
const result = await getBackendSrv().get(url);
dispatch(usersFetched(result)); dispatch(usersFetched(result));
} catch (error) { } catch (error) {
usersFetchEnd(); usersFetchEnd();
@ -318,3 +322,15 @@ export function changePage(page: number): ThunkResult<void> {
dispatch(fetchUsers()); dispatch(fetchUsers());
}; };
} }
export function changeSort({ sortBy }: FetchDataArgs<UserDTO>): ThunkResult<void> {
const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined;
return async (dispatch, getState) => {
const currentSort = getState().userListAdmin.sort;
if (currentSort !== sort) {
dispatch(usersFetchBegin());
dispatch(sortChanged(sort));
dispatch(fetchUsers());
}
};
}

View File

@ -173,6 +173,11 @@ export const userListAdminSlice = createSlice({
...state, ...state,
page: action.payload, page: action.payload,
}), }),
sortChanged: (state, action: PayloadAction<UserListAdminState['sort']>) => ({
...state,
page: 0,
sort: action.payload,
}),
filterChanged: (state, action: PayloadAction<UserFilter>) => { filterChanged: (state, action: PayloadAction<UserFilter>) => {
const { name, value } = action.payload; const { name, value } = action.payload;
@ -192,7 +197,7 @@ export const userListAdminSlice = createSlice({
}, },
}); });
export const { usersFetched, usersFetchBegin, usersFetchEnd, queryChanged, pageChanged, filterChanged } = export const { usersFetched, usersFetchBegin, usersFetchEnd, queryChanged, pageChanged, filterChanged, sortChanged } =
userListAdminSlice.actions; userListAdminSlice.actions;
export const userListAdminReducer = userListAdminSlice.reducer; export const userListAdminReducer = userListAdminSlice.reducer;

View File

@ -25,6 +25,7 @@ const setup = (propOverrides?: object) => {
deleteTeam: jest.fn(), deleteTeam: jest.fn(),
changePage: jest.fn(), changePage: jest.fn(),
changeQuery: jest.fn(), changeQuery: jest.fn(),
changeSort: jest.fn(),
query: '', query: '',
totalPages: 0, totalPages: 0,
page: 0, page: 0,

View File

@ -25,7 +25,7 @@ import { AccessControlAction, Role, StoreState, Team } from 'app/types';
import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker';
import { Avatar } from '../admin/Users/Avatar'; import { Avatar } from '../admin/Users/Avatar';
import { deleteTeam, loadTeams, changePage, changeQuery } from './state/actions'; import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions';
import { isPermissionTeamAdmin } from './state/selectors'; import { isPermissionTeamAdmin } from './state/selectors';
type Cell<T extends keyof Team = keyof Team> = CellProps<Team, Team[T]>; type Cell<T extends keyof Team = keyof Team> = CellProps<Team, Team[T]>;
@ -48,9 +48,9 @@ export const TeamList = ({
editorsCanAdmin, editorsCanAdmin,
page, page,
changePage, changePage,
changeSort,
}: Props) => { }: Props) => {
const [roleOptions, setRoleOptions] = useState<Role[]>([]); const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const enableSort = totalPages === 1;
useEffect(() => { useEffect(() => {
loadTeams(true); loadTeams(true);
@ -76,19 +76,19 @@ export const TeamList = ({
id: 'name', id: 'name',
header: 'Name', header: 'Name',
cell: ({ cell: { value } }: Cell<'name'>) => value, cell: ({ cell: { value } }: Cell<'name'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'email', id: 'email',
header: 'Email', header: 'Email',
cell: ({ cell: { value } }: Cell<'email'>) => value, cell: ({ cell: { value } }: Cell<'email'>) => value,
sortType: enableSort ? 'string' : undefined, sortType: 'string',
}, },
{ {
id: 'memberCount', id: 'memberCount',
header: 'Members', header: 'Members',
cell: ({ cell: { value } }: Cell<'memberCount'>) => value, cell: ({ cell: { value } }: Cell<'memberCount'>) => value,
sortType: enableSort ? 'number' : undefined, sortType: 'number',
}, },
...(displayRolePicker ...(displayRolePicker
? [ ? [
@ -155,7 +155,7 @@ export const TeamList = ({
}, },
}, },
], ],
[displayRolePicker, editorsCanAdmin, roleOptions, signedInUser, deleteTeam, enableSort] [displayRolePicker, editorsCanAdmin, roleOptions, signedInUser, deleteTeam]
); );
return ( return (
@ -185,7 +185,12 @@ export const TeamList = ({
</LinkButton> </LinkButton>
</div> </div>
<VerticalGroup spacing={'md'}> <VerticalGroup spacing={'md'}>
<InteractiveTable columns={columns} data={teams} getRowId={(team) => String(team.id)} /> <InteractiveTable
columns={columns}
data={teams}
getRowId={(team) => String(team.id)}
fetchData={changeSort}
/>
<HorizontalGroup justify="flex-end"> <HorizontalGroup justify="flex-end">
<Pagination hideWhenSinglePage currentPage={page} numberOfPages={totalPages} onNavigate={changePage} /> <Pagination hideWhenSinglePage currentPage={page} numberOfPages={totalPages} onNavigate={changePage} />
</HorizontalGroup> </HorizontalGroup>
@ -224,6 +229,7 @@ const mapDispatchToProps = {
deleteTeam, deleteTeam,
changePage, changePage,
changeQuery, changeQuery,
changeSort,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@ -1,17 +1,26 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { FetchDataArgs } from '@grafana/ui';
import { updateNavIndex } from 'app/core/actions'; import { updateNavIndex } from 'app/core/actions';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { AccessControlAction, TeamMember, ThunkResult } from 'app/types'; import { AccessControlAction, Team, TeamMember, ThunkResult } from 'app/types';
import { buildNavModel } from './navModel'; import { buildNavModel } from './navModel';
import { teamGroupsLoaded, queryChanged, pageChanged, teamLoaded, teamMembersLoaded, teamsLoaded } from './reducers'; import {
teamGroupsLoaded,
queryChanged,
pageChanged,
teamLoaded,
teamMembersLoaded,
teamsLoaded,
sortChanged,
} from './reducers';
export function loadTeams(initial = false): ThunkResult<void> { export function loadTeams(initial = false): ThunkResult<void> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const { query, page, perPage } = getState().teams; const { query, page, perPage, sort } = getState().teams;
// Early return if the user cannot list teams // Early return if the user cannot list teams
if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) { if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) {
dispatch(teamsLoaded({ teams: [], totalCount: 0, page: 1, perPage, noTeams: true })); dispatch(teamsLoaded({ teams: [], totalCount: 0, page: 1, perPage, noTeams: true }));
@ -20,7 +29,7 @@ export function loadTeams(initial = false): ThunkResult<void> {
const response = await getBackendSrv().get( const response = await getBackendSrv().get(
'/api/teams/search', '/api/teams/search',
accessControlQueryParam({ query, page, perpage: perPage }) accessControlQueryParam({ query, page, perpage: perPage, sort })
); );
// We only want to check if there is no teams on the initial request. // We only want to check if there is no teams on the initial request.
@ -67,6 +76,14 @@ export function changePage(page: number): ThunkResult<void> {
}; };
} }
export function changeSort({ sortBy }: FetchDataArgs<Team>): ThunkResult<void> {
const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined;
return async (dispatch) => {
dispatch(sortChanged(sort));
dispatch(loadTeams());
};
}
export function loadTeamMembers(): ThunkResult<void> { export function loadTeamMembers(): ThunkResult<void> {
return async (dispatch, getStore) => { return async (dispatch, getStore) => {
const team = getStore().team.team; const team = getStore().team.team;

View File

@ -35,10 +35,13 @@ const teamsSlice = createSlice({
pageChanged: (state, action: PayloadAction<number>): TeamsState => { pageChanged: (state, action: PayloadAction<number>): TeamsState => {
return { ...state, page: action.payload }; return { ...state, page: action.payload };
}, },
sortChanged: (state, action: PayloadAction<TeamsState['sort']>): TeamsState => {
return { ...state, sort: action.payload, page: 1 };
},
}, },
}); });
export const { teamsLoaded, queryChanged, pageChanged } = teamsSlice.actions; export const { teamsLoaded, queryChanged, pageChanged, sortChanged } = teamsSlice.actions;
export const teamsReducer = teamsSlice.reducer; export const teamsReducer = teamsSlice.reducer;

View File

@ -7,7 +7,7 @@ import { configureStore } from 'app/store/configureStore';
import { Invitee, OrgUser } from 'app/types'; import { Invitee, OrgUser } from 'app/types';
import { Props, UsersListPageUnconnected } from './UsersListPage'; import { Props, UsersListPageUnconnected } from './UsersListPage';
import { pageChanged } from './state/reducers'; import { pageChanged, sortChanged } from './state/reducers';
jest.mock('../../core/app_events', () => ({ jest.mock('../../core/app_events', () => ({
emit: jest.fn(), emit: jest.fn(),
@ -36,6 +36,7 @@ const setup = (propOverrides?: object) => {
updateUser: jest.fn(), updateUser: jest.fn(),
removeUser: jest.fn(), removeUser: jest.fn(),
changePage: mockToolkitActionCreator(pageChanged), changePage: mockToolkitActionCreator(pageChanged),
changeSort: mockToolkitActionCreator(sortChanged),
isLoading: false, isLoading: false,
}; };

View File

@ -12,7 +12,7 @@ import { fetchInvitees } from '../invites/state/actions';
import { selectInvitesMatchingQuery } from '../invites/state/selectors'; import { selectInvitesMatchingQuery } from '../invites/state/selectors';
import { UsersActionBar } from './UsersActionBar'; import { UsersActionBar } from './UsersActionBar';
import { loadUsers, removeUser, updateUser, changePage } from './state/actions'; import { loadUsers, removeUser, updateUser, changePage, changeSort } from './state/actions';
import { getUsers, getUsersSearchQuery } from './state/selectors'; import { getUsers, getUsersSearchQuery } from './state/selectors';
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
@ -33,6 +33,7 @@ const mapDispatchToProps = {
loadUsers, loadUsers,
fetchInvitees, fetchInvitees,
changePage, changePage,
changeSort,
updateUser, updateUser,
removeUser, removeUser,
}; };
@ -57,6 +58,7 @@ export const UsersListPageUnconnected = ({
changePage, changePage,
updateUser, updateUser,
removeUser, removeUser,
changeSort,
}: Props) => { }: Props) => {
const [showInvites, setShowInvites] = useState(false); const [showInvites, setShowInvites] = useState(false);
const externalUserMngInfoHtml = externalUserMngInfo ? renderMarkdown(externalUserMngInfo) : ''; const externalUserMngInfoHtml = externalUserMngInfo ? renderMarkdown(externalUserMngInfo) : '';
@ -86,6 +88,7 @@ export const UsersListPageUnconnected = ({
orgId={contextSrv.user.orgId} orgId={contextSrv.user.orgId}
onRoleChange={onRoleChange} onRoleChange={onRoleChange}
onRemoveUser={onRemoveUser} onRemoveUser={onRemoveUser}
fetchData={changeSort}
changePage={changePage} changePage={changePage}
page={page} page={page}
totalPages={totalPages} totalPages={totalPages}

View File

@ -1,20 +1,21 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { FetchDataArgs } from '@grafana/ui';
import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { OrgUser } from 'app/types'; import { OrgUser } from 'app/types';
import { ThunkResult } from '../../../types'; import { ThunkResult } from '../../../types';
import { usersLoaded, pageChanged, usersFetchBegin, usersFetchEnd, searchQueryChanged } from './reducers'; import { usersLoaded, pageChanged, usersFetchBegin, usersFetchEnd, searchQueryChanged, sortChanged } from './reducers';
export function loadUsers(): ThunkResult<void> { export function loadUsers(): ThunkResult<void> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
try { try {
const { perPage, page, searchQuery } = getState().users; const { perPage, page, searchQuery, sort } = getState().users;
const users = await getBackendSrv().get( const users = await getBackendSrv().get(
`/api/org/users/search`, `/api/org/users/search`,
accessControlQueryParam({ perpage: perPage, page, query: searchQuery }) accessControlQueryParam({ perpage: perPage, page, query: searchQuery, sort })
); );
dispatch(usersLoaded(users)); dispatch(usersLoaded(users));
} catch (error) { } catch (error) {
@ -47,6 +48,15 @@ export function changePage(page: number): ThunkResult<void> {
}; };
} }
export function changeSort({ sortBy }: FetchDataArgs<OrgUser>): ThunkResult<void> {
const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined;
return async (dispatch) => {
dispatch(usersFetchBegin());
dispatch(sortChanged(sort));
dispatch(loadUsers());
};
}
export function changeSearchQuery(query: string): ThunkResult<void> { export function changeSearchQuery(query: string): ThunkResult<void> {
return async (dispatch) => { return async (dispatch) => {
dispatch(usersFetchBegin()); dispatch(usersFetchBegin());

View File

@ -51,6 +51,10 @@ const usersSlice = createSlice({
...state, ...state,
page: action.payload, page: action.payload,
}), }),
sortChanged: (state, action: PayloadAction<UsersState['sort']>) => ({
...state,
sort: action.payload,
}),
usersFetchBegin: (state) => { usersFetchBegin: (state) => {
return { ...state, isLoading: true }; return { ...state, isLoading: true };
}, },
@ -60,8 +64,15 @@ const usersSlice = createSlice({
}, },
}); });
export const { searchQueryChanged, setUsersSearchPage, usersLoaded, usersFetchBegin, usersFetchEnd, pageChanged } = export const {
usersSlice.actions; searchQueryChanged,
setUsersSearchPage,
usersLoaded,
usersFetchBegin,
usersFetchEnd,
pageChanged,
sortChanged,
} = usersSlice.actions;
export const usersReducer = usersSlice.reducer; export const usersReducer = usersSlice.reducer;

View File

@ -63,6 +63,7 @@ export interface TeamsState {
noTeams: boolean; noTeams: boolean;
totalPages: number; totalPages: number;
hasFetched: boolean; hasFetched: boolean;
sort?: string;
} }
export interface TeamState { export interface TeamState {

View File

@ -80,6 +80,7 @@ export interface UsersState {
page: number; page: number;
perPage: number; perPage: number;
totalPages: number; totalPages: number;
sort?: string;
} }
export interface UserSession { export interface UserSession {
@ -124,4 +125,5 @@ export interface UserListAdminState {
showPaging: boolean; showPaging: boolean;
filters: UserFilter[]; filters: UserFilter[];
isLoading: boolean; isLoading: boolean;
sort?: string;
} }