mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5796836662
commit
4a692fc82e
@ -1,5 +1,5 @@
|
|||||||
// BETTERER RESULTS V2.
|
// BETTERER RESULTS V2.
|
||||||
//
|
//
|
||||||
// If this file contains merge conflicts, use `betterer merge` to automatically resolve them:
|
// If this file contains merge conflicts, use `betterer merge` to automatically resolve them:
|
||||||
// https://phenomnomnominal.github.io/betterer/docs/results-file/#merge
|
// https://phenomnomnominal.github.io/betterer/docs/results-file/#merge
|
||||||
//
|
//
|
||||||
|
@ -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 ? (
|
<UsersTable
|
||||||
<PageLoader />
|
users={users}
|
||||||
) : (
|
showPaging={showPaging}
|
||||||
<UsersTable
|
totalPages={totalPages}
|
||||||
users={users}
|
onChangePage={changePage}
|
||||||
showPaging={showPaging}
|
currentPage={page}
|
||||||
totalPages={totalPages}
|
fetchData={changeSort}
|
||||||
onChangePage={changePage}
|
/>
|
||||||
currentPage={page}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}?`}
|
||||||
|
@ -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} />
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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());
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user