mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Revert "Org users to react"
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import InviteesTable, { Props } from './InviteesTable';
|
||||
import { Invitee } from 'app/types';
|
||||
import { getMockInvitees } from './__mocks__/userMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
invitees: [] as Invitee[],
|
||||
revokeInvite: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<InviteesTable {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render invitees', () => {
|
||||
const wrapper = setup({
|
||||
invitees: getMockInvitees(5),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { createRef, PureComponent } from 'react';
|
||||
import { Invitee } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
invitees: Invitee[];
|
||||
revokeInvite: (code: string) => void;
|
||||
}
|
||||
|
||||
export default class InviteesTable extends PureComponent<Props> {
|
||||
private copyUrlRef = createRef<HTMLTextAreaElement>();
|
||||
|
||||
copyToClipboard = () => {
|
||||
const node = this.copyUrlRef.current;
|
||||
|
||||
if (node) {
|
||||
node.select();
|
||||
document.execCommand('copy');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { invitees, revokeInvite } = this.props;
|
||||
|
||||
return (
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th />
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invitees.map((invitee, index) => {
|
||||
return (
|
||||
<tr key={`${invitee.id}-${index}`}>
|
||||
<td>{invitee.email}</td>
|
||||
<td>{invitee.name}</td>
|
||||
<td className="text-right">
|
||||
<button className="btn btn-inverse btn-mini" onClick={this.copyToClipboard}>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
value={invitee.url}
|
||||
style={{ position: 'absolute', right: -1000 }}
|
||||
ref={this.copyUrlRef}
|
||||
/>
|
||||
<i className="fa fa-clipboard" /> Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn btn-danger btn-mini" onClick={() => revokeInvite(invitee.code)}>
|
||||
<i className="fa fa-remove" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UsersActionBar, Props } from './UsersActionBar';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
searchQuery: '',
|
||||
setUsersSearchQuery: jest.fn(),
|
||||
showInvites: jest.fn(),
|
||||
pendingInvitesCount: 0,
|
||||
canInvite: false,
|
||||
externalUserMngLinkUrl: '',
|
||||
externalUserMngLinkName: '',
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<UsersActionBar {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render pending invites button', () => {
|
||||
const wrapper = setup({
|
||||
pendingInvitesCount: 5,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show invite button', () => {
|
||||
const wrapper = setup({
|
||||
canInvite: true,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show external user management button', () => {
|
||||
const wrapper = setup({
|
||||
externalUserMngLinkUrl: 'some/url',
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { setUsersSearchQuery } from './state/actions';
|
||||
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
|
||||
|
||||
export interface Props {
|
||||
searchQuery: string;
|
||||
setUsersSearchQuery: typeof setUsersSearchQuery;
|
||||
showInvites: () => void;
|
||||
pendingInvitesCount: number;
|
||||
canInvite: boolean;
|
||||
externalUserMngLinkUrl: string;
|
||||
externalUserMngLinkName: string;
|
||||
}
|
||||
|
||||
export class UsersActionBar extends PureComponent<Props> {
|
||||
render() {
|
||||
const {
|
||||
canInvite,
|
||||
externalUserMngLinkName,
|
||||
externalUserMngLinkUrl,
|
||||
searchQuery,
|
||||
pendingInvitesCount,
|
||||
setUsersSearchQuery,
|
||||
showInvites,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={searchQuery}
|
||||
onChange={event => setUsersSearchQuery(event.target.value)}
|
||||
placeholder="Filter by name or type"
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
{pendingInvitesCount > 0 && (
|
||||
<button className="btn btn-inverse" onClick={showInvites}>
|
||||
Pending Invites ({pendingInvitesCount})
|
||||
</button>
|
||||
)}
|
||||
{canInvite && (
|
||||
<a className="btn btn-success" href="org/users/invite">
|
||||
<i className="fa fa-plus" />
|
||||
<span>Invite</span>
|
||||
</a>
|
||||
)}
|
||||
{externalUserMngLinkUrl && (
|
||||
<a className="btn btn-success" href={externalUserMngLinkUrl} target="_blank">
|
||||
<i className="fa fa-external-link-square" />
|
||||
{externalUserMngLinkName}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
searchQuery: getUsersSearchQuery(state.users),
|
||||
pendingInvitesCount: getInviteesCount(state.users),
|
||||
externalUserMngLinkName: state.users.externalUserMngLinkName,
|
||||
externalUserMngLinkUrl: state.users.externalUserMngLinkUrl,
|
||||
canInvite: state.users.canInvite,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setUsersSearchQuery,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UsersActionBar);
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UsersListPage, Props } from './UsersListPage';
|
||||
import { Invitee, NavModel, OrgUser } from 'app/types';
|
||||
import { getMockUser } from './__mocks__/userMocks';
|
||||
import appEvents from '../../core/app_events';
|
||||
|
||||
jest.mock('../../core/app_events', () => ({
|
||||
emit: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {} as NavModel,
|
||||
users: [] as OrgUser[],
|
||||
invitees: [] as Invitee[],
|
||||
searchQuery: '',
|
||||
externalUserMngInfo: '',
|
||||
revokeInvite: jest.fn(),
|
||||
loadInvitees: jest.fn(),
|
||||
loadUsers: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
removeUser: jest.fn(),
|
||||
setUsersSearchQuery: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<UsersListPage {...props} />);
|
||||
const instance = wrapper.instance() as UsersListPage;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
it('should emit show remove user modal', () => {
|
||||
const { instance } = setup();
|
||||
const mockUser = getMockUser();
|
||||
|
||||
instance.onRemoveUser(mockUser);
|
||||
|
||||
expect(appEvents.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import UsersActionBar from './UsersActionBar';
|
||||
import UsersTable from 'app/features/users/UsersTable';
|
||||
import InviteesTable from './InviteesTable';
|
||||
import { Invitee, NavModel, OrgUser } from 'app/types';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { loadUsers, loadInvitees, revokeInvite, setUsersSearchQuery, updateUser, removeUser } from './state/actions';
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
invitees: Invitee[];
|
||||
users: OrgUser[];
|
||||
searchQuery: string;
|
||||
externalUserMngInfo: string;
|
||||
loadUsers: typeof loadUsers;
|
||||
loadInvitees: typeof loadInvitees;
|
||||
setUsersSearchQuery: typeof setUsersSearchQuery;
|
||||
updateUser: typeof updateUser;
|
||||
removeUser: typeof removeUser;
|
||||
revokeInvite: typeof revokeInvite;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
showInvites: boolean;
|
||||
}
|
||||
|
||||
export class UsersListPage extends PureComponent<Props, State> {
|
||||
state = {
|
||||
showInvites: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchUsers();
|
||||
this.fetchInvitees();
|
||||
}
|
||||
|
||||
async fetchUsers() {
|
||||
return await this.props.loadUsers();
|
||||
}
|
||||
|
||||
async fetchInvitees() {
|
||||
return await this.props.loadInvitees();
|
||||
}
|
||||
|
||||
onRoleChange = (role, user) => {
|
||||
const updatedUser = { ...user, role: role };
|
||||
|
||||
this.props.updateUser(updatedUser);
|
||||
};
|
||||
|
||||
onRemoveUser = user => {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete user ' + user.login + '?',
|
||||
yesText: 'Delete',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.props.removeUser(user.userId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onRevokeInvite = code => {
|
||||
this.props.revokeInvite(code);
|
||||
};
|
||||
|
||||
showInvites = () => {
|
||||
this.setState(prevState => ({
|
||||
showInvites: !prevState.showInvites,
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { externalUserMngInfo, invitees, navModel, users } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
<div className="page-container page-body">
|
||||
<UsersActionBar showInvites={this.showInvites} />
|
||||
{externalUserMngInfo && (
|
||||
<div className="grafana-info-box">
|
||||
<span>{externalUserMngInfo}</span>
|
||||
</div>
|
||||
)}
|
||||
{this.state.showInvites ? (
|
||||
<InviteesTable invitees={invitees} revokeInvite={code => this.onRevokeInvite(code)} />
|
||||
) : (
|
||||
<UsersTable
|
||||
users={users}
|
||||
onRoleChange={(role, user) => this.onRoleChange(role, user)}
|
||||
onRemoveUser={user => this.onRemoveUser(user)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'users'),
|
||||
users: getUsers(state.users),
|
||||
searchQuery: getUsersSearchQuery(state.users),
|
||||
invitees: getInvitees(state.users),
|
||||
externalUserMngInfo: state.users.externalUserMngInfo,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadUsers,
|
||||
loadInvitees,
|
||||
setUsersSearchQuery,
|
||||
updateUser,
|
||||
removeUser,
|
||||
revokeInvite,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage));
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import UsersTable, { Props } from './UsersTable';
|
||||
import { OrgUser } from 'app/types';
|
||||
import { getMockUsers } from './__mocks__/userMocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
users: [] as OrgUser[],
|
||||
onRoleChange: jest.fn(),
|
||||
onRemoveUser: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<UsersTable {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render users table', () => {
|
||||
const wrapper = setup({
|
||||
users: getMockUsers(5),
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import React, { SFC } from 'react';
|
||||
import { OrgUser } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
users: OrgUser[];
|
||||
onRoleChange: (role: string, user: OrgUser) => void;
|
||||
onRemoveUser: (user: OrgUser) => void;
|
||||
}
|
||||
|
||||
const UsersTable: SFC<Props> = props => {
|
||||
const { users, onRoleChange, onRemoveUser } = props;
|
||||
|
||||
return (
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Seen</th>
|
||||
<th>Role</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, index) => {
|
||||
return (
|
||||
<tr key={`${user.userId}-${index}`}>
|
||||
<td className="width-4 text-center">
|
||||
<img className="filter-table__avatar" src={user.avatarUrl} />
|
||||
</td>
|
||||
<td>{user.login}</td>
|
||||
<td>
|
||||
<span className="ellipsis">{user.email}</span>
|
||||
</td>
|
||||
<td>{user.lastSeenAtAge}</td>
|
||||
<td>
|
||||
<div className="gf-form-select-wrapper width-12">
|
||||
<select
|
||||
value={user.role}
|
||||
className="gf-form-input"
|
||||
onChange={event => onRoleChange(event.target.value, user)}
|
||||
>
|
||||
{['Viewer', 'Editor', 'Admin'].map((option, index) => {
|
||||
return (
|
||||
<option value={option} key={`${option}-${index}`}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div onClick={() => onRemoveUser(user)} className="btn btn-danger btn-mini">
|
||||
<i className="fa fa-remove" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersTable;
|
||||
@@ -1,56 +0,0 @@
|
||||
export const getMockUsers = (amount: number) => {
|
||||
const users = [];
|
||||
|
||||
for (let i = 0; i <= amount; i++) {
|
||||
users.push({
|
||||
avatarUrl: 'url/to/avatar',
|
||||
email: `user-${i}@test.com`,
|
||||
lastSeenAt: '2018-10-01',
|
||||
lastSeenAtAge: '',
|
||||
login: `user-${i}`,
|
||||
orgId: 1,
|
||||
role: 'Admin',
|
||||
userId: i,
|
||||
});
|
||||
}
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
export const getMockUser = () => {
|
||||
return {
|
||||
avatarUrl: 'url/to/avatar',
|
||||
email: `user@test.com`,
|
||||
lastSeenAt: '2018-10-01',
|
||||
lastSeenAtAge: '',
|
||||
login: `user`,
|
||||
orgId: 1,
|
||||
role: 'Admin',
|
||||
userId: 2,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockInvitees = (amount: number) => {
|
||||
const invitees = [];
|
||||
|
||||
for (let i = 0; i <= amount; i++) {
|
||||
invitees.push({
|
||||
code: `asdfasdfsadf-${i}`,
|
||||
createdOn: '2018-10-02',
|
||||
email: `invitee-${i}@test.com`,
|
||||
emailSent: true,
|
||||
emailSentOn: '2018-10-02',
|
||||
id: i,
|
||||
invitedByEmail: 'admin@grafana.com',
|
||||
invitedByLogin: 'admin',
|
||||
invitedByName: 'admin',
|
||||
name: `invitee-${i}`,
|
||||
orgId: 1,
|
||||
role: 'viewer',
|
||||
status: 'not accepted',
|
||||
url: `localhost/invite/$${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
return invitees;
|
||||
};
|
||||
@@ -1,318 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<table
|
||||
className="filter-table form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th />
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "34px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Render should render invitees 1`] = `
|
||||
<table
|
||||
className="filter-table form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th />
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "34px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="0-0"
|
||||
>
|
||||
<td>
|
||||
invitee-0@test.com
|
||||
</td>
|
||||
<td>
|
||||
invitee-0
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": -1000,
|
||||
}
|
||||
}
|
||||
value="localhost/invite/$0"
|
||||
/>
|
||||
<i
|
||||
className="fa fa-clipboard"
|
||||
/>
|
||||
Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="1-1"
|
||||
>
|
||||
<td>
|
||||
invitee-1@test.com
|
||||
</td>
|
||||
<td>
|
||||
invitee-1
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": -1000,
|
||||
}
|
||||
}
|
||||
value="localhost/invite/$1"
|
||||
/>
|
||||
<i
|
||||
className="fa fa-clipboard"
|
||||
/>
|
||||
Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="2-2"
|
||||
>
|
||||
<td>
|
||||
invitee-2@test.com
|
||||
</td>
|
||||
<td>
|
||||
invitee-2
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": -1000,
|
||||
}
|
||||
}
|
||||
value="localhost/invite/$2"
|
||||
/>
|
||||
<i
|
||||
className="fa fa-clipboard"
|
||||
/>
|
||||
Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="3-3"
|
||||
>
|
||||
<td>
|
||||
invitee-3@test.com
|
||||
</td>
|
||||
<td>
|
||||
invitee-3
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": -1000,
|
||||
}
|
||||
}
|
||||
value="localhost/invite/$3"
|
||||
/>
|
||||
<i
|
||||
className="fa fa-clipboard"
|
||||
/>
|
||||
Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="4-4"
|
||||
>
|
||||
<td>
|
||||
invitee-4@test.com
|
||||
</td>
|
||||
<td>
|
||||
invitee-4
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": -1000,
|
||||
}
|
||||
}
|
||||
value="localhost/invite/$4"
|
||||
/>
|
||||
<i
|
||||
className="fa fa-clipboard"
|
||||
/>
|
||||
Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="5-5"
|
||||
>
|
||||
<td>
|
||||
invitee-5@test.com
|
||||
</td>
|
||||
<td>
|
||||
invitee-5
|
||||
</td>
|
||||
<td
|
||||
className="text-right"
|
||||
>
|
||||
<button
|
||||
className="btn btn-inverse btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<textarea
|
||||
readOnly={true}
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": -1000,
|
||||
}
|
||||
}
|
||||
value="localhost/invite/$5"
|
||||
/>
|
||||
<i
|
||||
className="fa fa-clipboard"
|
||||
/>
|
||||
Copy Invite
|
||||
</button>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
@@ -1,141 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render pending invites button 1`] = `
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<button
|
||||
className="btn btn-inverse"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
Pending Invites (
|
||||
5
|
||||
)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should show external user management button 1`] = `
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="btn btn-success"
|
||||
href="some/url"
|
||||
target="_blank"
|
||||
>
|
||||
<i
|
||||
className="fa fa-external-link-square"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should show invite button 1`] = `
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="btn btn-success"
|
||||
href="org/users/invite"
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
<span>
|
||||
Invite
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,21 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<Connect(UsersActionBar)
|
||||
showInvites={[Function]}
|
||||
/>
|
||||
<UsersTable
|
||||
onRemoveUser={[Function]}
|
||||
onRoleChange={[Function]}
|
||||
users={Array []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,444 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<table
|
||||
className="filter-table form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Login
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Seen
|
||||
</th>
|
||||
<th>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "34px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Render should render users table 1`] = `
|
||||
<table
|
||||
className="filter-table form-inline"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
Login
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Seen
|
||||
</th>
|
||||
<th>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "34px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
key="0-0"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
user-0
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="ellipsis"
|
||||
>
|
||||
user-0@test.com
|
||||
</span>
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-12"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
key="Viewer-0"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor-1"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin-2"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="1-1"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
user-1
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="ellipsis"
|
||||
>
|
||||
user-1@test.com
|
||||
</span>
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-12"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
key="Viewer-0"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor-1"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin-2"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="2-2"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
user-2
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="ellipsis"
|
||||
>
|
||||
user-2@test.com
|
||||
</span>
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-12"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
key="Viewer-0"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor-1"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin-2"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="3-3"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
user-3
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="ellipsis"
|
||||
>
|
||||
user-3@test.com
|
||||
</span>
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-12"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
key="Viewer-0"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor-1"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin-2"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="4-4"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
user-4
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="ellipsis"
|
||||
>
|
||||
user-4@test.com
|
||||
</span>
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-12"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
key="Viewer-0"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor-1"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin-2"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
key="5-5"
|
||||
>
|
||||
<td
|
||||
className="width-4 text-center"
|
||||
>
|
||||
<img
|
||||
className="filter-table__avatar"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
user-5
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="ellipsis"
|
||||
>
|
||||
user-5@test.com
|
||||
</span>
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-12"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
key="Viewer-0"
|
||||
value="Viewer"
|
||||
>
|
||||
Viewer
|
||||
</option>
|
||||
<option
|
||||
key="Editor-1"
|
||||
value="Editor"
|
||||
>
|
||||
Editor
|
||||
</option>
|
||||
<option
|
||||
key="Admin-2"
|
||||
value="Admin"
|
||||
>
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="btn btn-danger btn-mini"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-remove"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { StoreState } from '../../../types';
|
||||
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||
import { Invitee, OrgUser } from 'app/types';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadUsers = 'LOAD_USERS',
|
||||
LoadInvitees = 'LOAD_INVITEES',
|
||||
SetUsersSearchQuery = 'SET_USERS_SEARCH_QUERY',
|
||||
}
|
||||
|
||||
export interface LoadUsersAction {
|
||||
type: ActionTypes.LoadUsers;
|
||||
payload: OrgUser[];
|
||||
}
|
||||
|
||||
export interface LoadInviteesAction {
|
||||
type: ActionTypes.LoadInvitees;
|
||||
payload: Invitee[];
|
||||
}
|
||||
|
||||
export interface SetUsersSearchQueryAction {
|
||||
type: ActionTypes.SetUsersSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const usersLoaded = (users: OrgUser[]): LoadUsersAction => ({
|
||||
type: ActionTypes.LoadUsers,
|
||||
payload: users,
|
||||
});
|
||||
|
||||
const inviteesLoaded = (invitees: Invitee[]): LoadInviteesAction => ({
|
||||
type: ActionTypes.LoadInvitees,
|
||||
payload: invitees,
|
||||
});
|
||||
|
||||
export const setUsersSearchQuery = (query: string): SetUsersSearchQueryAction => ({
|
||||
type: ActionTypes.SetUsersSearchQuery,
|
||||
payload: query,
|
||||
});
|
||||
|
||||
export type Action = LoadUsersAction | SetUsersSearchQueryAction | LoadInviteesAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
|
||||
export function loadUsers(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const users = await getBackendSrv().get('/api/org/users');
|
||||
dispatch(usersLoaded(users));
|
||||
};
|
||||
}
|
||||
|
||||
export function loadInvitees(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const invitees = await getBackendSrv().get('/api/org/invites');
|
||||
dispatch(inviteesLoaded(invitees));
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUser(user: OrgUser): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().patch(`/api/org/users/${user.userId}`, user);
|
||||
dispatch(loadUsers());
|
||||
};
|
||||
}
|
||||
|
||||
export function removeUser(userId: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().delete(`/api/org/users/${userId}`);
|
||||
dispatch(loadUsers());
|
||||
};
|
||||
}
|
||||
|
||||
export function revokeInvite(code: string): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().patch(`/api/org/invites/${code}/revoke`, {});
|
||||
dispatch(loadInvitees());
|
||||
};
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Invitee, OrgUser, UsersState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import config from '../../../core/config';
|
||||
|
||||
export const initialState: UsersState = {
|
||||
invitees: [] as Invitee[],
|
||||
users: [] as OrgUser[],
|
||||
searchQuery: '',
|
||||
canInvite: !config.disableLoginForm && !config.externalUserMngLinkName,
|
||||
externalUserMngInfo: config.externalUserMngInfo,
|
||||
externalUserMngLinkName: config.externalUserMngLinkName,
|
||||
externalUserMngLinkUrl: config.externalUserMngLinkUrl,
|
||||
};
|
||||
|
||||
export const usersReducer = (state = initialState, action: Action): UsersState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadUsers:
|
||||
return { ...state, users: action.payload };
|
||||
|
||||
case ActionTypes.LoadInvitees:
|
||||
return { ...state, invitees: action.payload };
|
||||
|
||||
case ActionTypes.SetUsersSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
users: usersReducer,
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
export const getUsers = state => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.users.filter(user => {
|
||||
return regex.test(user.login) || regex.test(user.email);
|
||||
});
|
||||
};
|
||||
|
||||
export const getInvitees = state => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.invitees.filter(invitee => {
|
||||
return regex.test(invitee.name) || regex.test(invitee.email);
|
||||
});
|
||||
};
|
||||
|
||||
export const getInviteesCount = state => state.invitees.length;
|
||||
export const getUsersSearchQuery = state => state.searchQuery;
|
||||
Reference in New Issue
Block a user