set search query action and tests

This commit is contained in:
Peter Holmberg
2018-09-05 16:49:36 +02:00
parent 7e340b7aa5
commit f68ac20218
10 changed files with 354 additions and 29 deletions
+16 -14
View File
@@ -2,6 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Props, TeamList } from './TeamList';
import { NavModel, Team } from '../../types';
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
const setup = (propOverrides?: object) => {
const props: Props = {
@@ -9,7 +10,8 @@ const setup = (propOverrides?: object) => {
teams: [] as Team[],
loadTeams: jest.fn(),
deleteTeam: jest.fn(),
search: '',
setSearchQuery: jest.fn(),
searchQuery: '',
};
Object.assign(props, propOverrides);
@@ -23,17 +25,6 @@ const setup = (propOverrides?: object) => {
};
};
const mockTeam: Team = {
id: 1,
name: 'test',
avatarUrl: 'some/url/',
email: 'test@test.com',
memberCount: 1,
search: '',
members: [],
groups: [],
};
describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
@@ -42,7 +33,7 @@ describe('Render', () => {
it('should render teams table', () => {
const { wrapper } = setup({
teams: [mockTeam],
teams: getMultipleMockTeams(5),
});
expect(wrapper).toMatchSnapshot();
@@ -63,9 +54,20 @@ describe('Functions', () => {
describe('Delete team', () => {
it('should call delete team', () => {
const { instance } = setup();
instance.deleteTeam(mockTeam);
instance.deleteTeam(getMockTeam());
expect(instance.props.deleteTeam).toHaveBeenCalledWith(1);
});
});
describe('on search query change', () => {
it('should call setSearchQuery', () => {
const { instance } = setup();
const mockEvent = { target: { value: 'test' } };
instance.onSearchQueryChange(mockEvent);
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
});
});
});
+11 -9
View File
@@ -4,8 +4,8 @@ import { hot } from 'react-hot-loader';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { NavModel, Team } from '../../types';
import { loadTeams, deleteTeam } from './state/actions';
import { getTeams } from './state/selectors';
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
import { getSearchQuery, getTeams } from './state/selectors';
import { getNavModel } from 'app/core/selectors/navModel';
export interface Props {
@@ -13,7 +13,8 @@ export interface Props {
teams: Team[];
loadTeams: typeof loadTeams;
deleteTeam: typeof deleteTeam;
search: string;
setSearchQuery: typeof setSearchQuery;
searchQuery: string;
}
export class TeamList extends PureComponent<Props, any> {
@@ -30,10 +31,10 @@ export class TeamList extends PureComponent<Props, any> {
};
onSearchQueryChange = event => {
console.log('set search', event.target.value);
this.props.setSearchQuery(event.target.value);
};
renderTeamMember(team: Team) {
renderTeam(team: Team) {
const teamUrl = `org/teams/edit/${team.id}`;
return (
@@ -60,7 +61,7 @@ export class TeamList extends PureComponent<Props, any> {
}
render() {
const { navModel, teams, search } = this.props;
const { navModel, teams, searchQuery } = this.props;
return (
<div>
@@ -73,7 +74,7 @@ export class TeamList extends PureComponent<Props, any> {
type="text"
className="gf-form-input"
placeholder="Search teams"
value={search}
value={searchQuery}
onChange={this.onSearchQueryChange}
/>
<i className="gf-form-input-icon fa fa-search" />
@@ -98,7 +99,7 @@ export class TeamList extends PureComponent<Props, any> {
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>{teams.map(team => this.renderTeamMember(team))}</tbody>
<tbody>{teams.map(team => this.renderTeam(team))}</tbody>
</table>
</div>
</div>
@@ -111,13 +112,14 @@ function mapStateToProps(state) {
return {
navModel: getNavModel(state.navIndex, 'teams'),
teams: getTeams(state.teams),
search: '',
searchQuery: getSearchQuery(state.teams),
};
}
const mapDispatchToProps = {
loadTeams,
deleteTeam,
setSearchQuery,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList));
@@ -0,0 +1,32 @@
import { Team } from '../../../types';
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
let teams: Team[] = [];
for (let i = 1; i <= numberOfTeams; i++) {
teams.push({
id: i,
name: `test-${i}`,
avatarUrl: 'some/url/',
email: `test-${i}@test.com`,
memberCount: i,
search: '',
members: [],
groups: [],
});
}
return teams;
};
export const getMockTeam = (): Team => {
return {
id: 1,
name: 'test',
avatarUrl: 'some/url/',
email: 'test@test.com',
memberCount: 1,
search: '',
members: [],
groups: [],
};
};
@@ -167,7 +167,7 @@ exports[`Render should render teams table 1`] = `
<a
href="org/teams/edit/1"
>
test
test-1
</a>
</td>
<td
@@ -176,7 +176,7 @@ exports[`Render should render teams table 1`] = `
<a
href="org/teams/edit/1"
>
test@test.com
test-1@test.com
</a>
</td>
<td
@@ -196,6 +196,206 @@ exports[`Render should render teams table 1`] = `
/>
</td>
</tr>
<tr
key="2"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/2"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/2"
>
test-2
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/2"
>
test-2@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/2"
>
2
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="3"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/3"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/3"
>
test-3
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/3"
>
test-3@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/3"
>
3
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="4"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/4"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/4"
>
test-4
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/4"
>
test-4@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/4"
>
4
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="5"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/5"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/5"
>
test-5
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/5"
>
test-5@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/5"
>
5
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
</tbody>
</table>
</div>
+12 -1
View File
@@ -4,6 +4,7 @@ import { StoreState, Team } from '../../../types';
export enum ActionTypes {
LoadTeams = 'LOAD_TEAMS',
SetSearchQuery = 'SET_SEARCH_QUERY',
}
export interface LoadTeamsAction {
@@ -11,7 +12,12 @@ export interface LoadTeamsAction {
payload: Team[];
}
export type Action = LoadTeamsAction;
export interface SetSearchQueryAction {
type: ActionTypes.SetSearchQuery;
payload: string;
}
export type Action = LoadTeamsAction | SetSearchQueryAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
@@ -20,6 +26,11 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({
payload: teams,
});
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
type: ActionTypes.SetSearchQuery,
payload: searchQuery,
});
export function loadTeams(): ThunkResult<void> {
return async dispatch => {
const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 });
@@ -0,0 +1,41 @@
import { Action, ActionTypes } from './actions';
import { initialState, teamsReducer } from './reducers';
describe('teams reducer', () => {
it('should set teams', () => {
const payload = [
{
id: 1,
name: 'test',
avatarUrl: 'some/url/',
email: 'test@test.com',
memberCount: 1,
search: '',
members: [],
groups: [],
},
];
const action: Action = {
type: ActionTypes.LoadTeams,
payload,
};
const result = teamsReducer(initialState, action);
expect(result.teams).toEqual(payload);
});
it('should set search query', () => {
const payload = 'test';
const action: Action = {
type: ActionTypes.SetSearchQuery,
payload,
};
const result = teamsReducer(initialState, action);
expect(result.searchQuery).toEqual('test');
});
});
+5 -2
View File
@@ -1,12 +1,15 @@
import { TeamsState } from '../../../types';
import { Action, ActionTypes } from './actions';
const initialState: TeamsState = { teams: [] };
export const initialState: TeamsState = { teams: [], searchQuery: '' };
export const teamsReducer = (state = initialState, action: Action): TeamsState => {
switch (action.type) {
case ActionTypes.LoadTeams:
return { teams: action.payload };
return { ...state, teams: action.payload };
case ActionTypes.SetSearchQuery:
return { ...state, searchQuery: action.payload };
}
return state;
};
@@ -0,0 +1,25 @@
import { getTeams } from './selectors';
import { getMultipleMockTeams } from '../__mocks__/teamMocks';
import { TeamsState } from '../../../types';
describe('Team selectors', () => {
describe('Get teams', () => {
const mockTeams = getMultipleMockTeams(5);
it('should return teams if no search query', () => {
const mockState: TeamsState = { teams: mockTeams, searchQuery: '' };
const teams = getTeams(mockState);
expect(teams).toEqual(mockTeams);
});
it('Should filter teams if search query', () => {
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' };
const teams = getTeams(mockState);
expect(teams.length).toEqual(1);
});
});
});
+9 -1
View File
@@ -1 +1,9 @@
export const getTeams = state => state.teams;
export const getSearchQuery = state => state.searchQuery;
export const getTeams = state => {
const regex = RegExp(state.searchQuery, 'i');
return state.teams.filter(team => {
return regex.test(team.name);
});
};
+1
View File
@@ -119,6 +119,7 @@ export interface AlertRulesState {
export interface TeamsState {
teams: Team[];
searchQuery: string;
}
export interface StoreState {