Teams: Support paginating and filtering more then 1000 teams (#58761)

* TeamList: break out rows to its own component

* TeamsState: Add total count

* TeamList: Remove teamsCount prop

* TeamList: Restructure code and use count from backend response

* TeamList: calculate total pages using totalCount

* TeamList: Rename to state to currentPage and the reducer to
setCurrentPage

* TeamList: remove wrapper functions

* TeamList: rewrite as a functional component

* TeamList: export components for test

* TeamList: pass limit, page and query to backend

* TeamList: Rename properties in state and create actions for page and
query change

* TeamList: Add flag to control if EmptyList banner should render
This commit is contained in:
Karl Persson 2022-11-16 15:55:10 +01:00 committed by GitHub
parent a8c48b6801
commit 9283773c12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 296 additions and 273 deletions

View File

@ -28,12 +28,15 @@ describe('rootReducer', () => {
reducerTester<StoreState>()
.givenReducer(rootReducer, state)
.whenActionIsDispatched(teamsLoaded(teams))
.whenActionIsDispatched(teamsLoaded({ teams: teams, page: 1, noTeams: false, perPage: 30, totalCount: 1 }))
.thenStatePredicateShouldEqual((resultingState) => {
expect(resultingState.teams).toEqual({
hasFetched: true,
searchQuery: '',
searchPage: 1,
noTeams: false,
perPage: 30,
totalPages: 1,
query: '',
page: 1,
teams,
});
return true;
@ -47,8 +50,11 @@ describe('rootReducer', () => {
const state: StoreState = {
teams: {
hasFetched: true,
searchQuery: '',
searchPage: 1,
query: '',
page: 1,
noTeams: false,
totalPages: 1,
perPage: 30,
teams,
},
} as StoreState;

View File

@ -1,7 +1,6 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { contextSrv, User } from 'app/core/services/context_srv';
@ -9,7 +8,6 @@ import { OrgRole, Team } from '../../types';
import { Props, TeamList } from './TeamList';
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
import { setSearchQuery, setTeamsSearchPage } from './state/reducers';
jest.mock('app/core/config', () => ({
...jest.requireActual('app/core/config'),
@ -19,13 +17,14 @@ jest.mock('app/core/config', () => ({
const setup = (propOverrides?: object) => {
const props: Props = {
teams: [] as Team[],
noTeams: false,
loadTeams: jest.fn(),
deleteTeam: jest.fn(),
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
setTeamsSearchPage: mockToolkitActionCreator(setTeamsSearchPage),
searchQuery: '',
searchPage: 1,
teamsCount: 0,
changePage: jest.fn(),
changeQuery: jest.fn(),
query: '',
page: 1,
totalPages: 0,
hasFetched: false,
editorsCanAdmin: false,
signedInUser: {
@ -52,7 +51,7 @@ describe('TeamList', () => {
it('should enable the new team button', () => {
setup({
teams: getMultipleMockTeams(1),
teamsCount: 1,
totalCount: 1,
hasFetched: true,
editorsCanAdmin: true,
signedInUser: {
@ -69,7 +68,7 @@ describe('TeamList', () => {
it('should disable the new team button', () => {
setup({
teams: getMultipleMockTeams(1),
teamsCount: 1,
totalCount: 1,
hasFetched: true,
editorsCanAdmin: true,
signedInUser: {
@ -87,7 +86,7 @@ describe('TeamList', () => {
it('should call delete team', async () => {
const mockDelete = jest.fn();
const mockTeam = getMockTeam();
setup({ deleteTeam: mockDelete, teams: [mockTeam], teamsCount: 1, hasFetched: true });
setup({ deleteTeam: mockDelete, teams: [mockTeam], totalCount: 1, hasFetched: true });
await userEvent.click(screen.getByRole('button', { name: `Delete team ${mockTeam.name}` }));
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
await waitFor(() => {

View File

@ -1,9 +1,8 @@
import React, { PureComponent } from 'react';
import React, { useEffect, useState } from 'react';
import { DeleteButton, LinkButton, FilterInput, VerticalGroup, HorizontalGroup, Pagination } from '@grafana/ui';
import { LinkButton, FilterInput, VerticalGroup, HorizontalGroup, Pagination } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
import { config } from 'app/core/config';
import { contextSrv, User } from 'app/core/services/context_srv';
@ -11,22 +10,22 @@ import { AccessControlAction, Role, StoreState, Team } from 'app/types';
import { connectWithCleanUp } from '../../core/components/connectWithCleanUp';
import { deleteTeam, loadTeams } from './state/actions';
import { initialTeamsState, setSearchQuery, setTeamsSearchPage } from './state/reducers';
import { getSearchQuery, getTeams, getTeamsCount, getTeamsSearchPage, isPermissionTeamAdmin } from './state/selectors';
const pageLimit = 30;
import { TeamListRow } from './TeamListRow';
import { deleteTeam, loadTeams, changePage, changeQuery } from './state/actions';
import { initialTeamsState } from './state/reducers';
import { isPermissionTeamAdmin } from './state/selectors';
export interface Props {
teams: Team[];
searchQuery: string;
searchPage: number;
teamsCount: number;
page: number;
query: string;
noTeams: boolean;
totalPages: number;
hasFetched: boolean;
loadTeams: typeof loadTeams;
deleteTeam: typeof deleteTeam;
setSearchQuery: typeof setSearchQuery;
setTeamsSearchPage: typeof setTeamsSearchPage;
changePage: typeof changePage;
changeQuery: typeof changeQuery;
editorsCanAdmin: boolean;
signedInUser: User;
}
@ -35,199 +34,130 @@ export interface State {
roleOptions: Role[];
}
export class TeamList extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { roleOptions: [] };
}
export const TeamList = ({
teams,
page,
query,
noTeams,
totalPages,
hasFetched,
loadTeams,
deleteTeam,
changeQuery,
changePage,
signedInUser,
editorsCanAdmin,
}: Props) => {
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
componentDidMount() {
this.fetchTeams();
useEffect(() => {
loadTeams(true);
}, [loadTeams]);
useEffect(() => {
if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
this.fetchRoleOptions();
fetchRoleOptions().then((roles) => setRoleOptions(roles));
}
}
}, []);
async fetchTeams() {
await this.props.loadTeams();
}
const canCreate = canCreateTeam(editorsCanAdmin);
const displayRolePicker = shouldDisplayRolePicker();
async fetchRoleOptions() {
const roleOptions = await fetchRoleOptions();
this.setState({ roleOptions });
}
deleteTeam = (team: Team) => {
this.props.deleteTeam(team.id);
};
onSearchQueryChange = (value: string) => {
this.props.setSearchQuery(value);
};
renderTeam(team: Team) {
const { editorsCanAdmin, signedInUser } = this.props;
const permission = team.permission;
const teamUrl = `org/teams/edit/${team.id}`;
const isTeamAdmin = isPermissionTeamAdmin({ permission, editorsCanAdmin, signedInUser });
const canDelete = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsDelete, team, isTeamAdmin);
const canReadTeam = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRead, team, isTeamAdmin);
const canSeeTeamRoles = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRolesList, team, false);
const displayRolePicker =
contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList);
return (
<tr key={team.id}>
<td className="width-4 text-center link-td">
{canReadTeam ? (
<a href={teamUrl}>
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
</a>
) : (
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
)}
</td>
<td className="link-td">
{canReadTeam ? <a href={teamUrl}>{team.name}</a> : <div style={{ padding: '0px 8px' }}>{team.name}</div>}
</td>
<td className="link-td">
{canReadTeam ? (
<a href={teamUrl} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
{team.email}
</a>
) : (
<div style={{ padding: '0px 8px' }} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
{team.email}
</div>
)}
</td>
<td className="link-td">
{canReadTeam ? (
<a href={teamUrl}>{team.memberCount}</a>
) : (
<div style={{ padding: '0px 8px' }}>{team.memberCount}</div>
)}
</td>
{displayRolePicker && (
<td>{canSeeTeamRoles && <TeamRolePicker teamId={team.id} roleOptions={this.state.roleOptions} />}</td>
)}
<td className="text-right">
<DeleteButton
aria-label={`Delete team ${team.name}`}
size="sm"
disabled={!canDelete}
onConfirm={() => this.deleteTeam(team)}
return (
<Page navId="teams">
<Page.Contents isLoading={!hasFetched}>
{noTeams ? (
<EmptyListCTA
title="You haven't created any teams yet."
buttonIcon="users-alt"
buttonLink="org/teams/new"
buttonTitle=" New team"
buttonDisabled={!contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate)}
proTip="Assign folder and dashboard permissions to teams instead of users to ease administration."
proTipLink=""
proTipLinkTitle=""
proTipTarget="_blank"
/>
</td>
</tr>
);
}
) : (
<>
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<FilterInput placeholder="Search teams" value={query} onChange={changeQuery} />
</div>
renderEmptyList() {
return (
<EmptyListCTA
title="You haven't created any teams yet."
buttonIcon="users-alt"
buttonLink="org/teams/new"
buttonTitle=" New team"
buttonDisabled={!contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate)}
proTip="Assign folder and dashboard permissions to teams instead of users to ease administration."
proTipLink=""
proTipLinkTitle=""
proTipTarget="_blank"
/>
);
}
<LinkButton href={canCreate ? 'org/teams/new' : '#'} disabled={!canCreate}>
New Team
</LinkButton>
</div>
getPaginatedTeams = (teams: Team[]) => {
const offset = (this.props.searchPage - 1) * pageLimit;
return teams.slice(offset, offset + pageLimit);
};
<div className="admin-list-table">
<VerticalGroup spacing="md">
<table className="filter-table filter-table--hover form-inline">
<thead>
<tr>
<th />
<th>Name</th>
<th>Email</th>
<th>Members</th>
{displayRolePicker && <th>Roles</th>}
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>
{teams.map((team) => (
<TeamListRow
key={team.id}
team={team}
roleOptions={roleOptions}
displayRolePicker={displayRolePicker}
isTeamAdmin={isPermissionTeamAdmin({
permission: team.permission,
editorsCanAdmin,
signedInUser,
})}
onDelete={deleteTeam}
/>
))}
</tbody>
</table>
<HorizontalGroup justify="flex-end">
<Pagination
hideWhenSinglePage
currentPage={page}
numberOfPages={totalPages}
onNavigate={changePage}
/>
</HorizontalGroup>
</VerticalGroup>
</div>
</>
)}
</Page.Contents>
</Page>
);
};
renderTeamList() {
const { teams, searchQuery, editorsCanAdmin, searchPage, setTeamsSearchPage } = this.props;
const teamAdmin = contextSrv.hasRole('Admin') || (editorsCanAdmin && contextSrv.hasRole('Editor'));
const canCreate = contextSrv.hasAccess(AccessControlAction.ActionTeamsCreate, teamAdmin);
const displayRolePicker =
contextSrv.licensedAccessControlEnabled() &&
contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) &&
contextSrv.hasPermission(AccessControlAction.ActionRolesList);
const newTeamHref = canCreate ? 'org/teams/new' : '#';
const paginatedTeams = this.getPaginatedTeams(teams);
const totalPages = Math.ceil(teams.length / pageLimit);
function canCreateTeam(editorsCanAdmin: boolean): boolean {
const teamAdmin = contextSrv.hasRole('Admin') || (editorsCanAdmin && contextSrv.hasRole('Editor'));
return contextSrv.hasAccess(AccessControlAction.ActionTeamsCreate, teamAdmin);
}
return (
<>
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<FilterInput placeholder="Search teams" value={searchQuery} onChange={this.onSearchQueryChange} />
</div>
<LinkButton href={newTeamHref} disabled={!canCreate}>
New Team
</LinkButton>
</div>
<div className="admin-list-table">
<VerticalGroup spacing="md">
<table className="filter-table filter-table--hover form-inline">
<thead>
<tr>
<th />
<th>Name</th>
<th>Email</th>
<th>Members</th>
{displayRolePicker && <th>Roles</th>}
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>{paginatedTeams.map((team) => this.renderTeam(team))}</tbody>
</table>
<HorizontalGroup justify="flex-end">
<Pagination
onNavigate={setTeamsSearchPage}
currentPage={searchPage}
numberOfPages={totalPages}
hideWhenSinglePage={true}
/>
</HorizontalGroup>
</VerticalGroup>
</div>
</>
);
}
renderList() {
const { teamsCount, hasFetched } = this.props;
if (!hasFetched) {
return null;
}
if (teamsCount > 0) {
return this.renderTeamList();
} else {
return this.renderEmptyList();
}
}
render() {
const { hasFetched } = this.props;
return (
<Page navId="teams">
<Page.Contents isLoading={!hasFetched}>{this.renderList()}</Page.Contents>
</Page>
);
}
function shouldDisplayRolePicker(): boolean {
return (
contextSrv.licensedAccessControlEnabled() &&
contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) &&
contextSrv.hasPermission(AccessControlAction.ActionRolesList)
);
}
function mapStateToProps(state: StoreState) {
return {
teams: getTeams(state.teams),
searchQuery: getSearchQuery(state.teams),
searchPage: getTeamsSearchPage(state.teams),
teamsCount: getTeamsCount(state.teams),
teams: state.teams.teams,
page: state.teams.page,
query: state.teams.query,
perPage: state.teams.perPage,
noTeams: state.teams.noTeams,
totalPages: state.teams.totalPages,
hasFetched: state.teams.hasFetched,
editorsCanAdmin: config.editorsCanAdmin, // this makes the feature toggle mockable/controllable from tests,
signedInUser: contextSrv.user, // this makes the feature toggle mockable/controllable from tests,
@ -237,8 +167,8 @@ function mapStateToProps(state: StoreState) {
const mapDispatchToProps = {
loadTeams,
deleteTeam,
setSearchQuery,
setTeamsSearchPage,
changePage,
changeQuery,
};
export default connectWithCleanUp(

View File

@ -0,0 +1,65 @@
import React from 'react';
import { DeleteButton } from '@grafana/ui';
import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker';
import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction, Role, Team } from 'app/types';
type Props = {
team: Team;
roleOptions: Role[];
isTeamAdmin: boolean;
displayRolePicker: boolean;
onDelete: (id: number) => void;
};
export const TeamListRow = ({ team, roleOptions, isTeamAdmin, displayRolePicker, onDelete }: Props) => {
const teamUrl = `org/teams/edit/${team.id}`;
const canDelete = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsDelete, team, isTeamAdmin);
const canReadTeam = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRead, team, isTeamAdmin);
const canSeeTeamRoles = contextSrv.hasAccessInMetadata(AccessControlAction.ActionTeamsRolesList, team, false);
return (
<tr key={team.id}>
<td className="width-4 text-center link-td">
{canReadTeam ? (
<a href={teamUrl}>
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
</a>
) : (
<img className="filter-table__avatar" src={team.avatarUrl} alt="Team avatar" />
)}
</td>
<td className="link-td">
{canReadTeam ? <a href={teamUrl}>{team.name}</a> : <div style={{ padding: '0px 8px' }}>{team.name}</div>}
</td>
<td className="link-td">
{canReadTeam ? (
<a href={teamUrl} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
{team.email}
</a>
) : (
<div style={{ padding: '0px 8px' }} aria-label={team.email?.length > 0 ? undefined : 'Empty email cell'}>
{team.email}
</div>
)}
</td>
<td className="link-td">
{canReadTeam ? (
<a href={teamUrl}>{team.memberCount}</a>
) : (
<div style={{ padding: '0px 8px' }}>{team.memberCount}</div>
)}
</td>
{displayRolePicker && <td>{canSeeTeamRoles && <TeamRolePicker teamId={team.id} roleOptions={roleOptions} />}</td>}
<td className="text-right">
<DeleteButton
aria-label={`Delete team ${team.name}`}
size="sm"
disabled={!canDelete}
onConfirm={() => onDelete(team.id)}
/>
</td>
</tr>
);
};

View File

@ -1,3 +1,5 @@
import { debounce } from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { updateNavIndex } from 'app/core/actions';
import { contextSrv } from 'app/core/core';
@ -5,24 +7,35 @@ import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { AccessControlAction, TeamMember, ThunkResult } from 'app/types';
import { buildNavModel } from './navModel';
import { teamGroupsLoaded, teamLoaded, teamMembersLoaded, teamsLoaded } from './reducers';
import { teamGroupsLoaded, queryChanged, pageChanged, teamLoaded, teamMembersLoaded, teamsLoaded } from './reducers';
export function loadTeams(): ThunkResult<void> {
return async (dispatch) => {
export function loadTeams(initial = false): ThunkResult<void> {
return async (dispatch, getState) => {
const { query, page, perPage } = getState().teams;
// Early return if the user cannot list teams
if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) {
dispatch(teamsLoaded([]));
dispatch(teamsLoaded({ teams: [], totalCount: 0, page: 1, perPage, noTeams: true }));
return;
}
const response = await getBackendSrv().get(
'/api/teams/search',
accessControlQueryParam({ perpage: 1000, page: 1 })
accessControlQueryParam({ query, page, perpage: perPage })
);
dispatch(teamsLoaded(response.teams));
// We only want to check if there is no teams on the initial request.
// A query that returns no teams should not render the empty list banner.
let noTeams = false;
if (initial) {
noTeams = response.teams.length === 0;
}
dispatch(teamsLoaded({ noTeams, ...response }));
};
}
const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500);
export function loadTeam(id: number): ThunkResult<void> {
return async (dispatch) => {
const response = await getBackendSrv().get(`/api/teams/${id}`, accessControlQueryParam());
@ -31,6 +44,29 @@ export function loadTeam(id: number): ThunkResult<void> {
};
}
export function deleteTeam(id: number): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().delete(`/api/teams/${id}`);
// Update users permissions in case they lost teams.read with the deletion
await contextSrv.fetchUserPermissions();
dispatch(loadTeams());
};
}
export function changeQuery(query: string): ThunkResult<void> {
return async (dispatch) => {
dispatch(queryChanged(query));
loadTeamsWithDebounce(dispatch);
};
}
export function changePage(page: number): ThunkResult<void> {
return async (dispatch) => {
dispatch(pageChanged(page));
dispatch(loadTeams());
};
}
export function loadTeamMembers(): ThunkResult<void> {
return async (dispatch, getStore) => {
const team = getStore().team.team;
@ -87,15 +123,6 @@ export function removeTeamGroup(groupId: string): ThunkResult<void> {
};
}
export function deleteTeam(id: number): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().delete(`/api/teams/${id}`);
// Update users permissions in case they lost teams.read with the deletion
await contextSrv.fetchUserPermissions();
dispatch(loadTeams());
};
}
export function updateTeamMember(member: TeamMember): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().put(`/api/teams/${member.teamId}/members/${member.userId}`, {

View File

@ -6,9 +6,9 @@ import {
initialTeamsState,
initialTeamState,
setSearchMemberQuery,
setSearchQuery,
teamGroupsLoaded,
teamLoaded,
queryChanged,
teamMembersLoaded,
teamReducer,
teamsLoaded,
@ -20,11 +20,17 @@ describe('teams reducer', () => {
it('then state should be correct', () => {
reducerTester<TeamsState>()
.givenReducer(teamsReducer, { ...initialTeamsState })
.whenActionIsDispatched(teamsLoaded([getMockTeam()]))
.whenActionIsDispatched(
teamsLoaded({ teams: [getMockTeam()], page: 1, perPage: 30, noTeams: false, totalCount: 100 })
)
.thenStateShouldEqual({
...initialTeamsState,
hasFetched: true,
teams: [getMockTeam()],
noTeams: false,
totalPages: 4,
perPage: 30,
page: 1,
});
});
});
@ -33,10 +39,10 @@ describe('teams reducer', () => {
it('then state should be correct', () => {
reducerTester<TeamsState>()
.givenReducer(teamsReducer, { ...initialTeamsState })
.whenActionIsDispatched(setSearchQuery('test'))
.whenActionIsDispatched(queryChanged('test'))
.thenStateShouldEqual({
...initialTeamsState,
searchQuery: 'test',
query: 'test',
});
});
});

View File

@ -2,25 +2,43 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', searchPage: 1, hasFetched: false };
export const initialTeamsState: TeamsState = {
teams: [],
page: 1,
query: '',
perPage: 30,
totalPages: 0,
noTeams: false,
hasFetched: false,
};
type TeamsFetched = {
teams: Team[];
page: number;
perPage: number;
noTeams: boolean;
totalCount: number;
};
const teamsSlice = createSlice({
name: 'teams',
initialState: initialTeamsState,
reducers: {
teamsLoaded: (state, action: PayloadAction<Team[]>): TeamsState => {
return { ...state, hasFetched: true, teams: action.payload };
teamsLoaded: (state, action: PayloadAction<TeamsFetched>): TeamsState => {
const { totalCount, perPage, ...rest } = action.payload;
const totalPages = Math.ceil(totalCount / perPage);
return { ...state, ...rest, totalPages, perPage, hasFetched: true };
},
setSearchQuery: (state, action: PayloadAction<string>): TeamsState => {
return { ...state, searchQuery: action.payload, searchPage: initialTeamsState.searchPage };
queryChanged: (state, action: PayloadAction<string>): TeamsState => {
return { ...state, page: 1, query: action.payload };
},
setTeamsSearchPage: (state, action: PayloadAction<number>): TeamsState => {
return { ...state, searchPage: action.payload };
pageChanged: (state, action: PayloadAction<number>): TeamsState => {
return { ...state, page: action.payload };
},
},
});
export const { teamsLoaded, setSearchQuery, setTeamsSearchPage } = teamsSlice.actions;
export const { teamsLoaded, queryChanged, pageChanged } = teamsSlice.actions;
export const teamsReducer = teamsSlice.reducer;

View File

@ -1,29 +1,9 @@
import { User } from 'app/core/services/context_srv';
import { Team, TeamGroup, TeamsState, TeamState, OrgRole } from '../../../types';
import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
import { Team, TeamGroup, TeamState, OrgRole } from '../../../types';
import { getMockTeam, getMockTeamMembers } from '../__mocks__/teamMocks';
import { getTeam, getTeamMembers, getTeams, isSignedInUserTeamAdmin, Config } from './selectors';
describe('Teams selectors', () => {
describe('Get teams', () => {
const mockTeams = getMultipleMockTeams(5);
it('should return teams if no search query', () => {
const mockState: TeamsState = { teams: mockTeams, searchQuery: '', searchPage: 1, hasFetched: false };
const teams = getTeams(mockState);
expect(teams).toEqual(mockTeams);
});
it('Should filter teams if search query', () => {
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5', searchPage: 1, hasFetched: false };
const teams = getTeams(mockState);
expect(teams.length).toEqual(1);
});
});
});
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin, Config } from './selectors';
describe('Team selectors', () => {
describe('Get team', () => {

View File

@ -1,11 +1,8 @@
import { User } from 'app/core/services/context_srv';
import { Team, TeamsState, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types';
import { Team, TeamState, TeamMember, OrgRole, TeamPermissionLevel } from 'app/types';
export const getSearchQuery = (state: TeamsState) => state.searchQuery;
export const getSearchMemberQuery = (state: TeamState) => state.searchMemberQuery;
export const getTeamGroups = (state: TeamState) => state.groups;
export const getTeamsCount = (state: TeamsState) => state.teams.length;
export const getTeamsSearchPage = (state: TeamsState) => state.searchPage;
export const getTeam = (state: TeamState, currentTeamId: any): Team | null => {
if (state.team.id === parseInt(currentTeamId, 10)) {
@ -15,14 +12,6 @@ export const getTeam = (state: TeamState, currentTeamId: any): Team | null => {
return null;
};
export const getTeams = (state: TeamsState) => {
const regex = RegExp(state.searchQuery, 'i');
return state.teams.filter((team) => {
return regex.test(team.name);
});
};
export const getTeamMembers = (state: TeamState) => {
const regex = RegExp(state.searchMemberQuery, 'i');

View File

@ -29,8 +29,11 @@ export interface TeamGroup {
export interface TeamsState {
teams: Team[];
searchQuery: string;
searchPage: number;
page: number;
query: string;
perPage: number;
noTeams: boolean;
totalPages: number;
hasFetched: boolean;
}