mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
Redux: Factor Invites out to separate slice (#45552)
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Invitee } from 'app/types';
|
||||
import { revokeInvite } from './state/actions';
|
||||
import { Button, ClipboardButton } from '@grafana/ui';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
revokeInvite,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {
|
||||
invitee: Invitee;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
class InviteeRow extends PureComponent<Props> {
|
||||
render() {
|
||||
const { invitee, revokeInvite } = this.props;
|
||||
return (
|
||||
<tr>
|
||||
<td>{invitee.email}</td>
|
||||
<td>{invitee.name}</td>
|
||||
<td className="text-right">
|
||||
<ClipboardButton variant="secondary" size="sm" getText={() => invitee.url}>
|
||||
Copy Invite
|
||||
</ClipboardButton>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<Button variant="destructive" size="sm" icon="times" onClick={() => revokeInvite(invitee.code)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connector(InviteeRow);
|
||||
@@ -1,31 +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[],
|
||||
};
|
||||
|
||||
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,31 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Invitee } from 'app/types';
|
||||
import InviteeRow from './InviteeRow';
|
||||
|
||||
export interface Props {
|
||||
invitees: Invitee[];
|
||||
}
|
||||
|
||||
export default class InviteesTable extends PureComponent<Props> {
|
||||
render() {
|
||||
const { invitees } = 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 <InviteeRow key={`${invitee.id}-${index}`} invitee={invitee} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { SignupInvitedPage, Props } from './SignupInvited';
|
||||
import { backendSrv } from '../../core/services/backend_srv';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
|
||||
jest.mock('app/core/core', () => ({
|
||||
contextSrv: {
|
||||
user: { orgName: 'Invited to Org Name' },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
const defaultGet = {
|
||||
email: 'some.user@localhost',
|
||||
name: 'Some User',
|
||||
invitedBy: 'Invited By User',
|
||||
username: 'someuser',
|
||||
};
|
||||
|
||||
async function setupTestContext({ get = defaultGet }: { get?: typeof defaultGet | null } = {}) {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const getSpy = jest.spyOn(backendSrv, 'get');
|
||||
getSpy.mockResolvedValue(get);
|
||||
|
||||
const postSpy = jest.spyOn(backendSrv, 'post');
|
||||
postSpy.mockResolvedValue([]);
|
||||
|
||||
const props: Props = {
|
||||
...getRouteComponentProps({
|
||||
match: {
|
||||
params: { code: 'some code' },
|
||||
} as any,
|
||||
}),
|
||||
};
|
||||
|
||||
render(<SignupInvitedPage {...props} />);
|
||||
|
||||
await waitFor(() => expect(getSpy).toHaveBeenCalled());
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
return { getSpy, postSpy };
|
||||
}
|
||||
|
||||
describe('SignupInvitedPage', () => {
|
||||
describe('when initialized but invite data has not been retrieved yet', () => {
|
||||
it('then it should not render', async () => {
|
||||
await setupTestContext({ get: null });
|
||||
|
||||
expect(screen.queryByText(/email/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initialized and invite data has been retrieved', () => {
|
||||
it('then the greeting should be correct', async () => {
|
||||
await setupTestContext();
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
name: /hello some user\./i,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('then the invited by should be correct', async () => {
|
||||
await setupTestContext();
|
||||
|
||||
const view = screen.getByText(
|
||||
/has invited you to join grafana and the organization please complete the following and choose a password to accept your invitation and continue:/i
|
||||
);
|
||||
|
||||
expect(within(view).getByText(/invited by user/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('then the organization invited to should be correct', async () => {
|
||||
await setupTestContext();
|
||||
|
||||
const view = screen.getByText(
|
||||
/has invited you to join grafana and the organization please complete the following and choose a password to accept your invitation and continue:/i
|
||||
);
|
||||
|
||||
expect(within(view).getByText(/invited to org name/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('then the form should include form data', async () => {
|
||||
await setupTestContext();
|
||||
|
||||
expect(screen.getByPlaceholderText(/email@example\.com/i)).toHaveValue('some.user@localhost');
|
||||
expect(screen.getByPlaceholderText(/name \(optional\)/i)).toHaveValue('Some User');
|
||||
expect(screen.getByPlaceholderText(/username/i)).toHaveValue('some.user@localhost');
|
||||
expect(screen.getByPlaceholderText(/password/i)).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user submits the form and the required fields are not filled in', () => {
|
||||
it('then required fields should show error messages and nothing should be posted', async () => {
|
||||
const { postSpy } = await setupTestContext({ get: { email: '', invitedBy: '', name: '', username: '' } });
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: /sign up/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/email is required/i)).toBeInTheDocument());
|
||||
expect(screen.getByText(/username is required/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
|
||||
expect(postSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user submits the form and the required fields are filled in', () => {
|
||||
it('then correct form data should be posted', async () => {
|
||||
const { postSpy } = await setupTestContext();
|
||||
|
||||
userEvent.type(screen.getByPlaceholderText(/password/i), 'pass@word1');
|
||||
userEvent.click(screen.getByRole('button', { name: /sign up/i }));
|
||||
|
||||
await waitFor(() => expect(postSpy).toHaveBeenCalledTimes(1));
|
||||
expect(postSpy).toHaveBeenCalledWith('/api/user/invite/complete', {
|
||||
email: 'some.user@localhost',
|
||||
name: 'Some User',
|
||||
username: 'some.user@localhost',
|
||||
password: 'pass@word1',
|
||||
inviteCode: 'some code',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Button, Field, Form, Input } from '@grafana/ui';
|
||||
import { useAsync } from 'react-use';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
interface FormModel {
|
||||
email: string;
|
||||
name?: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
const navModel = {
|
||||
main: {
|
||||
icon: 'grafana',
|
||||
text: 'Invite',
|
||||
subTitle: 'Register your Grafana account',
|
||||
breadcrumbs: [{ title: 'Login', url: 'login' }],
|
||||
},
|
||||
node: {
|
||||
text: '',
|
||||
},
|
||||
};
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<{ code: string }> {}
|
||||
|
||||
export const SignupInvitedPage: FC<Props> = ({ match }) => {
|
||||
const code = match.params.code;
|
||||
const [initFormModel, setInitFormModel] = useState<FormModel>();
|
||||
const [greeting, setGreeting] = useState<string>();
|
||||
const [invitedBy, setInvitedBy] = useState<string>();
|
||||
|
||||
useAsync(async () => {
|
||||
const invite = await getBackendSrv().get(`/api/user/invite/${code}`);
|
||||
|
||||
setInitFormModel({
|
||||
email: invite.email,
|
||||
name: invite.name,
|
||||
username: invite.email,
|
||||
});
|
||||
|
||||
setGreeting(invite.name || invite.email || invite.username);
|
||||
setInvitedBy(invite.invitedBy);
|
||||
}, [code]);
|
||||
|
||||
const onSubmit = async (formData: FormModel) => {
|
||||
await getBackendSrv().post('/api/user/invite/complete', { ...formData, inviteCode: code });
|
||||
window.location.href = getConfig().appSubUrl + '/';
|
||||
};
|
||||
|
||||
if (!initFormModel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h3 className="page-sub-heading">Hello {greeting || 'there'}.</h3>
|
||||
|
||||
<div className="modal-tagline p-b-2">
|
||||
<em>{invitedBy || 'Someone'}</em> has invited you to join Grafana and the organization{' '}
|
||||
<span className="highlight-word">{contextSrv.user.orgName}</span>
|
||||
<br />
|
||||
Please complete the following and choose a password to accept your invitation and continue:
|
||||
</div>
|
||||
<Form defaultValues={initFormModel} onSubmit={onSubmit}>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Field invalid={!!errors.email} error={errors.email && errors.email.message} label="Email">
|
||||
<Input
|
||||
placeholder="email@example.com"
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^\S+@\S+$/,
|
||||
message: 'Email is invalid',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
<Field invalid={!!errors.name} error={errors.name && errors.name.message} label="Name">
|
||||
<Input placeholder="Name (optional)" {...register('name')} />
|
||||
</Field>
|
||||
<Field invalid={!!errors.username} error={errors.username && errors.username.message} label="Username">
|
||||
<Input {...register('username', { required: 'Username is required' })} placeholder="Username" />
|
||||
</Field>
|
||||
<Field invalid={!!errors.password} error={errors.password && errors.password.message} label="Password">
|
||||
<Input
|
||||
{...register('password', { required: 'Password is required' })}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Button type="submit">Sign up</Button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupInvitedPage;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { setUsersSearchQuery } from './state/reducers';
|
||||
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
|
||||
import { selectTotal } from '../invites/state/selectors';
|
||||
import { getUsersSearchQuery } from './state/selectors';
|
||||
import { RadioButtonGroup, LinkButton, FilterInput } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@@ -63,7 +64,7 @@ export class UsersActionBar extends PureComponent<Props> {
|
||||
function mapStateToProps(state: any) {
|
||||
return {
|
||||
searchQuery: getUsersSearchQuery(state.users),
|
||||
pendingInvitesCount: getInviteesCount(state.users),
|
||||
pendingInvitesCount: selectTotal(state.invites),
|
||||
externalUserMngLinkName: state.users.externalUserMngLinkName,
|
||||
externalUserMngLinkUrl: state.users.externalUserMngLinkUrl,
|
||||
canInvite: state.users.canInvite,
|
||||
|
||||
@@ -26,7 +26,7 @@ const setup = (propOverrides?: object) => {
|
||||
searchQuery: '',
|
||||
searchPage: 1,
|
||||
externalUserMngInfo: '',
|
||||
loadInvitees: jest.fn(),
|
||||
fetchInvitees: jest.fn(),
|
||||
loadUsers: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
removeUser: jest.fn(),
|
||||
|
||||
@@ -6,20 +6,23 @@ import { HorizontalGroup, Pagination, VerticalGroup } from '@grafana/ui';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import UsersActionBar from './UsersActionBar';
|
||||
import UsersTable from './UsersTable';
|
||||
import InviteesTable from './InviteesTable';
|
||||
import InviteesTable from '../invites/InviteesTable';
|
||||
import { OrgUser, OrgRole, StoreState } from 'app/types';
|
||||
import { loadInvitees, loadUsers, removeUser, updateUser } from './state/actions';
|
||||
import { loadUsers, removeUser, updateUser } from './state/actions';
|
||||
import { fetchInvitees } from '../invites/state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getInvitees, getUsers, getUsersSearchQuery, getUsersSearchPage } from './state/selectors';
|
||||
import { getUsers, getUsersSearchQuery, getUsersSearchPage } from './state/selectors';
|
||||
import { setUsersSearchQuery, setUsersSearchPage } from './state/reducers';
|
||||
import { selectInvitesMatchingQuery } from '../invites/state/selectors';
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
const searchQuery = getUsersSearchQuery(state.users);
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'users'),
|
||||
users: getUsers(state.users),
|
||||
searchQuery: getUsersSearchQuery(state.users),
|
||||
searchPage: getUsersSearchPage(state.users),
|
||||
invitees: getInvitees(state.users),
|
||||
invitees: selectInvitesMatchingQuery(state.invites, searchQuery),
|
||||
externalUserMngInfo: state.users.externalUserMngInfo,
|
||||
hasFetched: state.users.hasFetched,
|
||||
};
|
||||
@@ -27,7 +30,7 @@ function mapStateToProps(state: StoreState) {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadUsers,
|
||||
loadInvitees,
|
||||
fetchInvitees,
|
||||
setUsersSearchQuery,
|
||||
setUsersSearchPage,
|
||||
updateUser,
|
||||
@@ -69,7 +72,7 @@ export class UsersListPage extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async fetchInvitees() {
|
||||
return await this.props.loadInvitees();
|
||||
return await this.props.fetchInvitees();
|
||||
}
|
||||
|
||||
onRoleChange = (role: OrgRole, user: OrgUser) => {
|
||||
|
||||
@@ -1,180 +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>
|
||||
<Connect(InviteeRow)
|
||||
invitee={
|
||||
Object {
|
||||
"code": "asdfasdfsadf-0",
|
||||
"createdOn": "2018-10-02",
|
||||
"email": "invitee-0@test.com",
|
||||
"emailSent": true,
|
||||
"emailSentOn": "2018-10-02",
|
||||
"id": 0,
|
||||
"invitedByEmail": "admin@grafana.com",
|
||||
"invitedByLogin": "admin",
|
||||
"invitedByName": "admin",
|
||||
"name": "invitee-0",
|
||||
"orgId": 1,
|
||||
"role": "viewer",
|
||||
"status": "not accepted",
|
||||
"url": "localhost/invite/0",
|
||||
}
|
||||
}
|
||||
key="0-0"
|
||||
/>
|
||||
<Connect(InviteeRow)
|
||||
invitee={
|
||||
Object {
|
||||
"code": "asdfasdfsadf-1",
|
||||
"createdOn": "2018-10-02",
|
||||
"email": "invitee-1@test.com",
|
||||
"emailSent": true,
|
||||
"emailSentOn": "2018-10-02",
|
||||
"id": 1,
|
||||
"invitedByEmail": "admin@grafana.com",
|
||||
"invitedByLogin": "admin",
|
||||
"invitedByName": "admin",
|
||||
"name": "invitee-1",
|
||||
"orgId": 1,
|
||||
"role": "viewer",
|
||||
"status": "not accepted",
|
||||
"url": "localhost/invite/1",
|
||||
}
|
||||
}
|
||||
key="1-1"
|
||||
/>
|
||||
<Connect(InviteeRow)
|
||||
invitee={
|
||||
Object {
|
||||
"code": "asdfasdfsadf-2",
|
||||
"createdOn": "2018-10-02",
|
||||
"email": "invitee-2@test.com",
|
||||
"emailSent": true,
|
||||
"emailSentOn": "2018-10-02",
|
||||
"id": 2,
|
||||
"invitedByEmail": "admin@grafana.com",
|
||||
"invitedByLogin": "admin",
|
||||
"invitedByName": "admin",
|
||||
"name": "invitee-2",
|
||||
"orgId": 1,
|
||||
"role": "viewer",
|
||||
"status": "not accepted",
|
||||
"url": "localhost/invite/2",
|
||||
}
|
||||
}
|
||||
key="2-2"
|
||||
/>
|
||||
<Connect(InviteeRow)
|
||||
invitee={
|
||||
Object {
|
||||
"code": "asdfasdfsadf-3",
|
||||
"createdOn": "2018-10-02",
|
||||
"email": "invitee-3@test.com",
|
||||
"emailSent": true,
|
||||
"emailSentOn": "2018-10-02",
|
||||
"id": 3,
|
||||
"invitedByEmail": "admin@grafana.com",
|
||||
"invitedByLogin": "admin",
|
||||
"invitedByName": "admin",
|
||||
"name": "invitee-3",
|
||||
"orgId": 1,
|
||||
"role": "viewer",
|
||||
"status": "not accepted",
|
||||
"url": "localhost/invite/3",
|
||||
}
|
||||
}
|
||||
key="3-3"
|
||||
/>
|
||||
<Connect(InviteeRow)
|
||||
invitee={
|
||||
Object {
|
||||
"code": "asdfasdfsadf-4",
|
||||
"createdOn": "2018-10-02",
|
||||
"email": "invitee-4@test.com",
|
||||
"emailSent": true,
|
||||
"emailSentOn": "2018-10-02",
|
||||
"id": 4,
|
||||
"invitedByEmail": "admin@grafana.com",
|
||||
"invitedByLogin": "admin",
|
||||
"invitedByName": "admin",
|
||||
"name": "invitee-4",
|
||||
"orgId": 1,
|
||||
"role": "viewer",
|
||||
"status": "not accepted",
|
||||
"url": "localhost/invite/4",
|
||||
}
|
||||
}
|
||||
key="4-4"
|
||||
/>
|
||||
<Connect(InviteeRow)
|
||||
invitee={
|
||||
Object {
|
||||
"code": "asdfasdfsadf-5",
|
||||
"createdOn": "2018-10-02",
|
||||
"email": "invitee-5@test.com",
|
||||
"emailSent": true,
|
||||
"emailSentOn": "2018-10-02",
|
||||
"id": 5,
|
||||
"invitedByEmail": "admin@grafana.com",
|
||||
"invitedByLogin": "admin",
|
||||
"invitedByName": "admin",
|
||||
"name": "invitee-5",
|
||||
"orgId": 1,
|
||||
"role": "viewer",
|
||||
"status": "not accepted",
|
||||
"url": "localhost/invite/5",
|
||||
}
|
||||
}
|
||||
key="5-5"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AccessControlAction, ThunkResult } from '../../../types';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { OrgUser } from 'app/types';
|
||||
import { inviteesLoaded, usersLoaded } from './reducers';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { usersLoaded } from './reducers';
|
||||
import { accessControlQueryParam } from 'app/core/utils/accessControl';
|
||||
|
||||
export function loadUsers(): ThunkResult<void> {
|
||||
@@ -12,17 +11,6 @@ export function loadUsers(): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadInvitees(): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
if (!contextSrv.hasPermission(AccessControlAction.UsersCreate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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}`, { role: user.role });
|
||||
@@ -36,10 +24,3 @@ export function removeUser(userId: number): ThunkResult<void> {
|
||||
dispatch(loadUsers());
|
||||
};
|
||||
}
|
||||
|
||||
export function revokeInvite(code: string): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
await getBackendSrv().patch(`/api/org/invites/${code}/revoke`, {});
|
||||
dispatch(loadInvitees());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { UsersState } from '../../../types';
|
||||
import { initialState, inviteesLoaded, setUsersSearchQuery, usersLoaded, usersReducer } from './reducers';
|
||||
import { getMockInvitees, getMockUsers } from '../__mocks__/userMocks';
|
||||
import { initialState, setUsersSearchQuery, usersLoaded, usersReducer } from './reducers';
|
||||
import { getMockUsers } from '../__mocks__/userMocks';
|
||||
|
||||
describe('usersReducer', () => {
|
||||
describe('when usersLoaded is dispatched', () => {
|
||||
@@ -17,19 +17,6 @@ describe('usersReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inviteesLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UsersState>()
|
||||
.givenReducer(usersReducer, { ...initialState })
|
||||
.whenActionIsDispatched(inviteesLoaded(getMockInvitees(1)))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
invitees: getMockInvitees(1),
|
||||
hasFetched: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setUsersSearchQuery is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UsersState>()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Invitee, OrgUser, UsersState } from 'app/types';
|
||||
import { OrgUser, UsersState } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const initialState: UsersState = {
|
||||
invitees: [] as Invitee[],
|
||||
users: [] as OrgUser[],
|
||||
searchQuery: '',
|
||||
searchPage: 1,
|
||||
@@ -22,9 +21,6 @@ const usersSlice = createSlice({
|
||||
usersLoaded: (state, action: PayloadAction<OrgUser[]>): UsersState => {
|
||||
return { ...state, hasFetched: true, users: action.payload };
|
||||
},
|
||||
inviteesLoaded: (state, action: PayloadAction<Invitee[]>): UsersState => {
|
||||
return { ...state, hasFetched: true, invitees: action.payload };
|
||||
},
|
||||
setUsersSearchQuery: (state, action: PayloadAction<string>): UsersState => {
|
||||
// reset searchPage otherwise search results won't appear
|
||||
return { ...state, searchQuery: action.payload, searchPage: initialState.searchPage };
|
||||
@@ -35,7 +31,7 @@ const usersSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { inviteesLoaded, setUsersSearchQuery, setUsersSearchPage, usersLoaded } = usersSlice.actions;
|
||||
export const { setUsersSearchQuery, setUsersSearchPage, usersLoaded } = usersSlice.actions;
|
||||
|
||||
export const usersReducer = usersSlice.reducer;
|
||||
|
||||
|
||||
@@ -8,14 +8,5 @@ export const getUsers = (state: UsersState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getInvitees = (state: UsersState) => {
|
||||
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: UsersState) => state.invitees.length;
|
||||
export const getUsersSearchQuery = (state: UsersState) => state.searchQuery;
|
||||
export const getUsersSearchPage = (state: UsersState) => state.searchPage;
|
||||
|
||||
Reference in New Issue
Block a user