api.js refactor using classes (#250)

Refactor api.js into an api module where endpoint specific models can be imported and used in components.
This commit is contained in:
Michael Abashian 2019-06-07 15:48:09 -04:00 committed by GitHub
parent a8c51670af
commit 2ae93261d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 839 additions and 727 deletions

View File

@ -12,6 +12,7 @@ Have questions about this document or anything not covered here? Feel free to re
* [Node and npm](#node-and-npm)
* [Build the user interface](#build-the-user-interface)
* [Accessing the AWX web interface](#accessing-the-awx-web-interface)
* [AWX REST API Interaction](#awx-rest-api-interaction)
* [Working with React](#working-with-react)
* [App structure](#app-structure)
* [Naming files](#naming-files)
@ -58,6 +59,57 @@ Run the following to build the AWX UI:
You can now log into the AWX web interface at [https://127.0.0.1:3001](https://127.0.0.1:3001).
## AWX REST API Interaction
This interface is built on top of the AWX REST API. If a component needs to interact with the API then the model that corresponds to that base endpoint will need to be imported from the api module.
Example:
`import { OrganizationsAPI, UsersAPI } from '../../../api';`
All models extend a `Base` class which provides an interface to the standard HTTP methods (GET, POST, PUT etc). Methods that are specific to that endpoint should be added directly to model's class.
**Mixins** - For related endpoints that apply to several different models a mixin should be used. Mixins are classes with a number of methods and can be used to avoid adding the same methods to a number of different models. A good example of this is the Notifications mixin. This mixin provides generic methods for reading notification templates and toggling them on and off.
Note that mixins can be chained. See the example below.
Example of a model using multiple mixins:
```
import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
...
}
export default Organizations;
```
**Testing** - The easiest way to mock the api module in tests is to use jest's [automatic mock](https://jestjs.io/docs/en/es6-class-mocks#automatic-mock). This syntax will replace the class with a mock constructor and mock out all methods to return undefined by default. If necessary, you can still override these mocks for specific tests. See the example below.
Example of mocking a specific method for every test in a suite:
```
import { OrganizationsAPI } from '../../../../src/api';
// Mocks out all available methods. Comparable to:
// OrganizationsAPI.readAccessList = jest.fn();
// but for every available method
jest.mock('../../../../src/api');
// Return a specific mock value for the readAccessList method
beforeEach(() => {
OrganizationsAPI.readAccessList.mockReturnValue({ foo: 'bar' });
});
// Reset mocks
afterEach(() => {
jest.clearAllMocks();
});
...
```
## Working with React
### App structure
@ -191,20 +243,19 @@ this.state = {
We have several React contexts that wrap much of the app, including those from react-router, lingui, and some of our own. When testing a component that depends on one or more of these, you can use the `mountWithContexts()` helper function found in `__tests__/enzymeHelpers.jsx`. This can be used just like Enzyme's `mount()` function, except it will wrap the component tree with the necessary context providers and basic stub data.
If you want to stub the value of a context, or assert actions taken on it, you can customize a contexts value by passing a second parameter to `mountWithContexts`. For example, this provides a custom value for the `Network` context:
If you want to stub the value of a context, or assert actions taken on it, you can customize a contexts value by passing a second parameter to `mountWithContexts`. For example, this provides a custom value for the `Config` context:
```
const network = {
api: {
getOrganizationInstanceGroups: jest.fn(),
}
const config = {
custom_virtualenvs: ['foo', 'bar'],
};
mountWithContexts(<OrganizationForm />, {
context: { network },
context: { config },
});
```
In this test, when the `OrganizationForm` calls `api.getOrganizationInstanceGroups` from the network context, it will invoke the provided stub. You can assert that this stub is invoked when you expect or to provide stubbed data.
Now that these custom virtual environments are available in this `OrganizationForm` test we can assert that the component that displays
them is rendering properly.
The object containing context values looks for five known contexts, identified by the keys `linguiPublisher`, `router`, `config`, `network`, and `dialog` — the latter three each referring to the contexts defined in `src/contexts`. You can pass `false` for any of these values, and the corresponding context will be omitted from your test. For example, this will mount your component without the dialog context:

View File

@ -6,6 +6,10 @@ import { asyncFlush } from '../jest.setup';
import App from '../src/App';
import { RootAPI } from '../src/api';
jest.mock('../src/api');
describe('<App />', () => {
test('expected content is rendered', () => {
const appWrapper = mountWithContexts(
@ -89,15 +93,13 @@ describe('<App />', () => {
});
test('onLogout makes expected call to api client', async (done) => {
const logout = jest.fn(() => Promise.resolve());
const appWrapper = mountWithContexts(<App />, {
context: { network: { api: { logout }, handleHttpError: () => {} } }
context: { network: { handleHttpError: () => {} } }
}).find('App');
appWrapper.instance().onLogout();
await asyncFlush();
expect(logout).toHaveBeenCalledTimes(1);
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
done();
});

View File

@ -46,17 +46,6 @@ exports[`mountWithContexts injected I18nProvider should mount and render deeply
</Parent>
`;
exports[`mountWithContexts injected Network should mount and render 1`] = `
<Foo
api={"/api/"}
handleHttpError={[Function]}
>
<div>
test
</div>
</Foo>
`;
exports[`mountWithContexts injected Router should mount and render 1`] = `
<div>
<Link

View File

@ -1,173 +0,0 @@
import APIClient from '../src/api';
const invalidCookie = 'invalid';
const validLoggedOutCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=false; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
const validLoggedInCookie = 'current_user=%7B%22id%22%3A1%2C%22type%22%3A%22user%22%2C%22url%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2F%22%2C%22related%22%3A%7B%22admin_of_organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fadmin_of_organizations%2F%22%2C%22authorized_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fauthorized_tokens%2F%22%2C%22roles%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Froles%2F%22%2C%22organizations%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Forganizations%2F%22%2C%22access_list%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Faccess_list%2F%22%2C%22teams%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fteams%2F%22%2C%22tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Ftokens%2F%22%2C%22personal_tokens%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fpersonal_tokens%2F%22%2C%22credentials%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fcredentials%2F%22%2C%22activity_stream%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Factivity_stream%2F%22%2C%22projects%22%3A%22%2Fapi%2Fv2%2Fusers%2F1%2Fprojects%2F%22%7D%2C%22summary_fields%22%3A%7B%7D%2C%22created%22%3A%222018-10-19T16%3A30%3A59.141963Z%22%2C%22username%22%3A%22admin%22%2C%22first_name%22%3A%22%22%2C%22last_name%22%3A%22%22%2C%22email%22%3A%22%22%2C%22is_superuser%22%3Atrue%2C%22is_system_auditor%22%3Afalse%2C%22ldap_dn%22%3A%22%22%2C%22external_account%22%3Anull%2C%22auth%22%3A%5B%5D%7D; userLoggedIn=true; csrftoken=lhOHpLQUFHlIVqx8CCZmEpdEZAz79GIRBIT3asBzTbPE7HS7wizt7WBsgJClz8Ge';
describe('APIClient (api.js)', () => {
test('isAuthenticated returns false when cookie is invalid', () => {
APIClient.getCookie = jest.fn(() => invalidCookie);
const api = new APIClient();
expect(api.isAuthenticated()).toBe(false);
});
test('isAuthenticated returns false when cookie is unauthenticated', () => {
APIClient.getCookie = jest.fn(() => validLoggedOutCookie);
const api = new APIClient();
expect(api.isAuthenticated()).toBe(false);
});
test('isAuthenticated returns true when cookie is valid and authenticated', () => {
APIClient.getCookie = jest.fn(() => validLoggedInCookie);
const api = new APIClient();
expect(api.isAuthenticated()).toBe(true);
});
test('login calls get and post with expected content headers', async (done) => {
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.login('username', 'password');
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0]).toContainEqual({ headers });
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual({ headers });
done();
});
test('login sends expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.login('foo', 'bar');
await api.login('foo', 'bar', 'baz');
expect(mockHttp.post).toHaveBeenCalledTimes(2);
expect(mockHttp.post.mock.calls[0]).toContainEqual('username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F');
expect(mockHttp.post.mock.calls[1]).toContainEqual('username=foo&password=bar&next=baz');
done();
});
test('logout calls expected http method', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.logout();
expect(mockHttp.get).toHaveBeenCalledTimes(1);
done();
});
test('getConfig calls expected http method', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.getConfig();
expect(mockHttp.get).toHaveBeenCalledTimes(1);
done();
});
test('getOrganizations calls http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const defaultParams = {};
const testParams = { foo: 'bar' };
await api.getOrganizations(testParams);
await api.getOrganizations();
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: testParams });
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: defaultParams });
done();
});
test('createOrganization calls http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const data = { name: 'test ' };
await api.createOrganization(data);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
done();
});
test('getOrganizationDetails calls http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.getOrganizationDetails(99);
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0][0]).toContain('99');
done();
});
test('getInstanceGroups calls expected http method', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
await api.getInstanceGroups();
expect(mockHttp.get).toHaveBeenCalledTimes(1);
done();
});
test('associateInstanceGroup calls expected http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const url = 'foo/bar/';
const id = 1;
await api.associateInstanceGroup(url, id);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][0]).toEqual(url);
expect(mockHttp.post.mock.calls[0][1]).toEqual({ id });
done();
});
test('disassociate calls expected http method with expected data', async (done) => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const api = new APIClient(mockHttp);
const url = 'foo/bar/';
const id = 1;
await api.disassociate(url, id);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][0]).toEqual(url);
expect(mockHttp.post.mock.calls[0][1]).toEqual({ id, disassociate: true });
done();
});
});

View File

@ -0,0 +1,97 @@
import Base from '../../src/api/Base';
describe('Base', () => {
const createPromise = () => Promise.resolve();
const mockBaseURL = '/api/v2/organizations/';
const mockHttp = ({
delete: jest.fn(createPromise),
get: jest.fn(createPromise),
options: jest.fn(createPromise),
patch: jest.fn(createPromise),
post: jest.fn(createPromise),
put: jest.fn(createPromise)
});
const BaseAPI = new Base(mockHttp, mockBaseURL);
afterEach(() => {
jest.clearAllMocks();
});
test('create calls http method with expected data', async (done) => {
const data = { name: 'test ' };
await BaseAPI.create(data);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0][1]).toEqual(data);
done();
});
test('destroy calls http method with expected data', async (done) => {
const resourceId = 1;
await BaseAPI.destroy(resourceId);
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
expect(mockHttp.delete.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
done();
});
test('read calls http method with expected data', async (done) => {
const defaultParams = {};
const testParams = { foo: 'bar' };
await BaseAPI.read(testParams);
await BaseAPI.read();
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: testParams });
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: defaultParams });
done();
});
test('readDetail calls http method with expected data', async (done) => {
const resourceId = 1;
await BaseAPI.readDetail(resourceId);
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
done();
});
test('readOptions calls http method with expected data', async (done) => {
await BaseAPI.readOptions();
expect(mockHttp.options).toHaveBeenCalledTimes(1);
expect(mockHttp.options.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
done();
});
test('replace calls http method with expected data', async (done) => {
const resourceId = 1;
const data = { name: 'test ' };
await BaseAPI.replace(resourceId, data);
expect(mockHttp.put).toHaveBeenCalledTimes(1);
expect(mockHttp.put.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
done();
});
test('update calls http method with expected data', async (done) => {
const resourceId = 1;
const data = { name: 'test ' };
await BaseAPI.update(resourceId, data);
expect(mockHttp.patch).toHaveBeenCalledTimes(1);
expect(mockHttp.patch.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
done();
});
});

View File

@ -0,0 +1,36 @@
import Organizations from '../../src/api/models/Organizations';
describe('OrganizationsAPI', () => {
const orgId = 1;
const searchParams = { foo: 'bar' };
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const OrganizationsAPI = new Organizations(mockHttp);
afterEach(() => {
jest.clearAllMocks();
});
test('read access list calls get with expected params', async (done) => {
await OrganizationsAPI.readAccessList(orgId);
await OrganizationsAPI.readAccessList(orgId, searchParams);
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0]).toContainEqual(`/api/v2/organizations/${orgId}/access_list/`, { params: {} });
expect(mockHttp.get.mock.calls[1]).toContainEqual(`/api/v2/organizations/${orgId}/access_list/`, { params: searchParams });
done();
});
test('read teams calls get with expected params', async (done) => {
await OrganizationsAPI.readTeams(orgId);
await OrganizationsAPI.readTeams(orgId, searchParams);
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0]).toContainEqual(`/api/v2/organizations/${orgId}/teams/`, { params: {} });
expect(mockHttp.get.mock.calls[1]).toContainEqual(`/api/v2/organizations/${orgId}/teams/`, { params: searchParams });
done();
});
});

View File

@ -0,0 +1,45 @@
import Root from '../../src/api/models/Root';
describe('RootAPI', () => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
const RootAPI = new Root(mockHttp);
afterEach(() => {
jest.clearAllMocks();
});
test('login calls get and post with expected content headers', async (done) => {
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
await RootAPI.login('username', 'password');
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0]).toContainEqual({ headers });
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual({ headers });
done();
});
test('login sends expected data', async (done) => {
await RootAPI.login('foo', 'bar');
await RootAPI.login('foo', 'bar', 'baz');
expect(mockHttp.post).toHaveBeenCalledTimes(2);
expect(mockHttp.post.mock.calls[0]).toContainEqual('username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F');
expect(mockHttp.post.mock.calls[1]).toContainEqual('username=foo&password=bar&next=baz');
done();
});
test('logout calls expected http method', async (done) => {
await RootAPI.logout();
expect(mockHttp.get).toHaveBeenCalledTimes(1);
done();
});
});

View File

@ -0,0 +1,35 @@
import Teams from '../../src/api/models/Teams';
describe('TeamsAPI', () => {
const teamId = 1;
const roleId = 7;
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const TeamsAPI = new Teams(mockHttp);
afterEach(() => {
jest.clearAllMocks();
});
test('associate role calls post with expected params', async (done) => {
await TeamsAPI.associateRole(teamId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/teams/${teamId}/roles/`, { id: roleId });
done();
});
test('read teams calls post with expected params', async (done) => {
await TeamsAPI.disassociateRole(teamId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/teams/${teamId}/roles/`, {
id: roleId,
disassociate: true
});
done();
});
});

View File

@ -0,0 +1,35 @@
import Users from '../../src/api/models/Users';
describe('UsersAPI', () => {
const userId = 1;
const roleId = 7;
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const UsersAPI = new Users(mockHttp);
afterEach(() => {
jest.clearAllMocks();
});
test('associate role calls post with expected params', async (done) => {
await UsersAPI.associateRole(userId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/users/${userId}/roles/`, { id: roleId });
done();
});
test('read users calls post with expected params', async (done) => {
await UsersAPI.disassociateRole(userId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/users/${userId}/roles/`, {
id: roleId,
disassociate: true
});
done();
});
});

View File

@ -2,9 +2,12 @@ import React from 'react';
import { shallow } from 'enzyme';
import { mountWithContexts } from '../enzymeHelpers';
import AddResourceRole, { _AddResourceRole } from '../../src/components/AddRole/AddResourceRole';
import { TeamsAPI, UsersAPI } from '../../src/api';
jest.mock('../../src/api');
describe('<_AddResourceRole />', () => {
const readUsers = jest.fn().mockResolvedValue({
UsersAPI.read.mockResolvedValue({
data: {
count: 2,
results: [
@ -13,10 +16,6 @@ describe('<_AddResourceRole />', () => {
]
}
});
const readTeams = jest.fn();
const createUserRole = jest.fn();
const createTeamRole = jest.fn();
const api = { readUsers, readTeams, createUserRole, createTeamRole };
const roles = {
admin_role: {
description: 'Can manage all aspects of the organization',
@ -32,7 +31,6 @@ describe('<_AddResourceRole />', () => {
test('initially renders without crashing', () => {
shallow(
<_AddResourceRole
api={api}
onClose={() => {}}
onSave={() => {}}
roles={roles}
@ -43,7 +41,6 @@ describe('<_AddResourceRole />', () => {
test('handleRoleCheckboxClick properly updates state', () => {
const wrapper = shallow(
<_AddResourceRole
api={api}
onClose={() => {}}
onSave={() => {}}
roles={roles}
@ -79,7 +76,6 @@ describe('<_AddResourceRole />', () => {
test('handleResourceCheckboxClick properly updates state', () => {
const wrapper = shallow(
<_AddResourceRole
api={api}
onClose={() => {}}
onSave={() => {}}
roles={roles}
@ -115,7 +111,7 @@ describe('<_AddResourceRole />', () => {
onClose={() => {}}
onSave={() => {}}
roles={roles}
/>, { context: { network: { api, handleHttpError: () => {} } } }
/>, { context: { network: { handleHttpError: () => {} } } }
).find('AddResourceRole');
const selectableCardWrapper = wrapper.find('SelectableCard');
expect(selectableCardWrapper.length).toBe(2);
@ -126,35 +122,9 @@ describe('<_AddResourceRole />', () => {
expect(spy).toHaveBeenCalledWith('teams');
expect(wrapper.state('selectedResource')).toBe('teams');
});
test('readUsers and readTeams call out to corresponding api functions', () => {
const wrapper = shallow(
<_AddResourceRole
api={api}
onClose={() => {}}
onSave={() => {}}
roles={roles}
i18n={{ _: val => val.toString() }}
/>
);
wrapper.instance().readUsers({
foo: 'bar'
});
expect(readUsers).toHaveBeenCalledWith({
foo: 'bar',
is_superuser: false
});
wrapper.instance().readTeams({
foo: 'bar'
});
expect(readTeams).toHaveBeenCalledWith({
foo: 'bar'
});
});
test('handleResourceSelect clears out selected lists and sets selectedResource', () => {
const wrapper = shallow(
<_AddResourceRole
api={api}
onClose={() => {}}
onSave={() => {}}
roles={roles}
@ -192,7 +162,6 @@ describe('<_AddResourceRole />', () => {
currentStepId: 1
});
});
test('handleWizardSave makes correct api calls, calls onSave when done', async () => {
const handleSave = jest.fn();
const wrapper = mountWithContexts(
@ -200,7 +169,7 @@ describe('<_AddResourceRole />', () => {
onClose={() => {}}
onSave={handleSave}
roles={roles}
/>, { context: { network: { api, handleHttpError: () => {} } } }
/>, { context: { network: { handleHttpError: () => {} } } }
).find('AddResourceRole');
wrapper.setState({
selectedResource: 'users',
@ -224,7 +193,7 @@ describe('<_AddResourceRole />', () => {
]
});
await wrapper.instance().handleWizardSave();
expect(createUserRole).toHaveBeenCalledTimes(2);
expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2);
expect(handleSave).toHaveBeenCalled();
wrapper.setState({
selectedResource: 'teams',
@ -248,7 +217,7 @@ describe('<_AddResourceRole />', () => {
]
});
await wrapper.instance().handleWizardSave();
expect(createTeamRole).toHaveBeenCalledTimes(2);
expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2);
expect(handleSave).toHaveBeenCalled();
});
});

View File

@ -70,10 +70,6 @@ const defaultContexts = {
toJSON: () => '/router/',
},
network: {
api: {
getConfig: () => {},
toJSON: () => '/api/',
},
handleHttpError: () => {},
},
dialog: {}
@ -146,7 +142,6 @@ export function mountWithContexts (node, options = {}) {
history: shape({}).isRequired,
}),
network: shape({
api: shape({}).isRequired,
handleHttpError: func.isRequired,
}),
dialog: shape({

View File

@ -4,7 +4,6 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { mountWithContexts, waitForElement } from './enzymeHelpers';
import { Config } from '../src/contexts/Config';
import { withNetwork } from '../src/contexts/Network';
import { withRootDialog } from '../src/contexts/RootDialog';
describe('mountWithContexts', () => {
@ -111,32 +110,6 @@ describe('mountWithContexts', () => {
});
});
describe('injected Network', () => {
it('should mount and render', () => {
const Foo = () => (
<div>test</div>
);
const Bar = withNetwork(Foo);
const wrapper = mountWithContexts(<Bar />);
expect(wrapper.find('Foo')).toMatchSnapshot();
});
it('should mount and render with stubbed api', () => {
const network = {
api: {
getFoo: jest.fn().mockReturnValue('foo value'),
},
};
const Foo = ({ api }) => (
<div>{api.getFoo()}</div>
);
const Bar = withNetwork(Foo);
const wrapper = mountWithContexts(<Bar />, { context: { network } });
expect(network.api.getFoo).toHaveBeenCalledTimes(1);
expect(wrapper.find('div').text()).toEqual('foo value');
});
});
describe('injected root dialog', () => {
it('should mount and render', () => {
const Foo = ({ title, setRootDialogMessage }) => (

View File

@ -2,7 +2,9 @@ import React from 'react';
import { mountWithContexts } from '../enzymeHelpers';
import { asyncFlush } from '../../jest.setup';
import AWXLogin from '../../src/pages/Login';
import APIClient from '../../src/api';
import { RootAPI } from '../../src/api';
jest.mock('../../src/api');
describe('<Login />', () => {
let loginWrapper;
@ -14,12 +16,8 @@ describe('<Login />', () => {
let submitButton;
let loginHeaderLogo;
const api = new APIClient({});
const mountLogin = () => {
loginWrapper = mountWithContexts(<AWXLogin />, { context: { network: {
api, handleHttpError: () => {}
} } });
loginWrapper = mountWithContexts(<AWXLogin />, { context: { network: {} } });
};
const findChildren = () => {
@ -33,6 +31,7 @@ describe('<Login />', () => {
};
afterEach(() => {
jest.clearAllMocks();
loginWrapper.unmount();
});
@ -98,32 +97,31 @@ describe('<Login />', () => {
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
});
test('api.login not called when loading', () => {
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
test('login API not called when loading', () => {
mountLogin();
findChildren();
expect(awxLogin.state().isLoading).toBe(false);
awxLogin.setState({ isLoading: true });
submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(0);
expect(RootAPI.login).toHaveBeenCalledTimes(0);
});
test('submit calls api.login successfully', async () => {
api.login = jest.fn().mockImplementation(() => Promise.resolve({}));
test('submit calls login API successfully', async () => {
RootAPI.login = jest.fn().mockImplementation(() => Promise.resolve({}));
mountLogin();
findChildren();
expect(awxLogin.state().isLoading).toBe(false);
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(1);
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(RootAPI.login).toHaveBeenCalledTimes(1);
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(awxLogin.state().isLoading).toBe(true);
await asyncFlush();
expect(awxLogin.state().isLoading).toBe(false);
});
test('submit calls api.login handles 401 error', async () => {
api.login = jest.fn().mockImplementation(() => {
test('submit calls login API and handles 401 error', async () => {
RootAPI.login = jest.fn().mockImplementation(() => {
const err = new Error('401 error');
err.response = { status: 401, message: 'problem' };
return Promise.reject(err);
@ -134,16 +132,16 @@ describe('<Login />', () => {
expect(awxLogin.state().isInputValid).toBe(true);
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(1);
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(RootAPI.login).toHaveBeenCalledTimes(1);
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(awxLogin.state().isLoading).toBe(true);
await asyncFlush();
expect(awxLogin.state().isInputValid).toBe(false);
expect(awxLogin.state().isLoading).toBe(false);
});
test('submit calls api.login handles non-401 error', async () => {
api.login = jest.fn().mockImplementation(() => {
test('submit calls login API and handles non-401 error', async () => {
RootAPI.login = jest.fn().mockImplementation(() => {
const err = new Error('500 error');
err.response = { status: 500, message: 'problem' };
return Promise.reject(err);
@ -153,8 +151,8 @@ describe('<Login />', () => {
expect(awxLogin.state().isLoading).toBe(false);
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('click');
expect(api.login).toHaveBeenCalledTimes(1);
expect(api.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(RootAPI.login).toHaveBeenCalledTimes(1);
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(awxLogin.state().isLoading).toBe(true);
await asyncFlush();
expect(awxLogin.state().isLoading).toBe(false);

View File

@ -2,9 +2,12 @@ import React from 'react';
import { mountWithContexts } from '../../../enzymeHelpers';
import { sleep } from '../../../testUtils';
import OrganizationForm from '../../../../src/pages/Organizations/components/OrganizationForm';
import { OrganizationsAPI } from '../../../../src/api';
jest.mock('../../../../src/api');
describe('<OrganizationForm />', () => {
let network;
const network = {};
const mockData = {
id: 1,
@ -16,24 +19,11 @@ describe('<OrganizationForm />', () => {
}
};
beforeEach(() => {
network = {};
});
afterEach(() => {
jest.clearAllMocks();
});
test('should request related instance groups from api', () => {
const mockInstanceGroups = [
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
network.api = {
getOrganizationInstanceGroups: jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
))
};
mountWithContexts(
(
<OrganizationForm
@ -46,7 +36,7 @@ describe('<OrganizationForm />', () => {
}
);
expect(network.api.getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
});
test('componentDidMount should set instanceGroups to state', async () => {
@ -54,11 +44,11 @@ describe('<OrganizationForm />', () => {
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
network.api = {
getOrganizationInstanceGroups: jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
))
};
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups
}
});
const wrapper = mountWithContexts(
(
<OrganizationForm
@ -72,7 +62,7 @@ describe('<OrganizationForm />', () => {
);
await sleep(0);
expect(network.api.getOrganizationInstanceGroups).toHaveBeenCalled();
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalled();
expect(wrapper.find('OrganizationForm').state().instanceGroups).toEqual(mockInstanceGroups);
});
@ -165,20 +155,20 @@ describe('<OrganizationForm />', () => {
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
network.api = {
getOrganizationInstanceGroups: jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
))
};
OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: {
results: mockInstanceGroups
}
});
const mockDataForm = {
name: 'Foo',
description: 'Bar',
custom_virtualenv: 'Fizz',
};
const handleSubmit = jest.fn();
network.api.updateOrganizationDetails = jest.fn().mockResolvedValue(1, mockDataForm);
network.api.associateInstanceGroup = jest.fn().mockResolvedValue('done');
network.api.disassociate = jest.fn().mockResolvedValue('done');
OrganizationsAPI.update.mockResolvedValue(1, mockDataForm);
OrganizationsAPI.associateInstanceGroup.mockResolvedValue('done');
OrganizationsAPI.disassociateInstanceGroup.mockResolvedValue('done');
const wrapper = mountWithContexts(
(
<OrganizationForm

View File

@ -1,6 +1,11 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../../enzymeHelpers';
import { sleep } from '../../../../testUtils';
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe.only('<Organization />', () => {
const me = {
@ -12,8 +17,41 @@ describe.only('<Organization />', () => {
mountWithContexts(<Organization me={me} />);
});
test('notifications tab shown/hidden based on permissions', () => {
const wrapper = mountWithContexts(<Organization me={me} />);
test('notifications tab shown/hidden based on permissions', async () => {
OrganizationsAPI.readDetail.mockResolvedValue({
data: {
id: 1,
name: 'foo'
}
});
OrganizationsAPI.read.mockResolvedValue({
data: {
results: []
}
});
const history = createMemoryHistory({
initialEntries: ['/organizations/1/details'],
});
const match = { path: '/organizations/:id', url: '/organizations/1' };
const wrapper = mountWithContexts(
<Organization
me={me}
setBreadcrumb={() => {}}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match
}
}
}
}
);
await sleep(0);
wrapper.update();
expect(wrapper.find('.pf-c-tabs__item').length).toBe(3);
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(0);
wrapper.find('Organization').setState({

View File

@ -3,8 +3,12 @@ import { mountWithContexts } from '../../../../enzymeHelpers';
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
import { sleep } from '../../../../testUtils';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe('<OrganizationAccess />', () => {
let network;
const network = {};
const organization = {
id: 1,
name: 'Default',
@ -60,15 +64,11 @@ describe('<OrganizationAccess />', () => {
};
beforeEach(() => {
network = {
api: {
getOrganizationAccessList: jest.fn()
.mockReturnValue(Promise.resolve({ data })),
disassociateTeamRole: jest.fn(),
disassociateUserRole: jest.fn(),
toJSON: () => '/api/',
},
};
OrganizationsAPI.readAccessList.mockReturnValue({ data });
});
afterEach(() => {
jest.clearAllMocks();
});
test('initially renders succesfully', () => {
@ -86,7 +86,7 @@ describe('<OrganizationAccess />', () => {
);
await sleep(0);
wrapper.update();
expect(network.api.getOrganizationAccessList).toHaveBeenCalled();
expect(OrganizationsAPI.readAccessList).toHaveBeenCalled();
expect(wrapper.find('OrganizationAccess').state('isInitialized')).toBe(true);
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(data.results);
expect(wrapper.find('OrganizationAccessItem')).toHaveLength(2);
@ -127,8 +127,8 @@ describe('<OrganizationAccess />', () => {
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete')).toBeNull();
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
expect(network.api.disassociateTeamRole).not.toHaveBeenCalled();
expect(network.api.disassociateUserRole).not.toHaveBeenCalled();
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
});
it('should delete user role', async () => {
@ -149,9 +149,9 @@ describe('<OrganizationAccess />', () => {
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete')).toBeNull();
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
expect(network.api.disassociateTeamRole).not.toHaveBeenCalled();
expect(network.api.disassociateUserRole).toHaveBeenCalledWith(1, 1);
expect(network.api.getOrganizationAccessList).toHaveBeenCalledTimes(2);
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1);
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
});
it('should delete team role', async () => {
@ -172,8 +172,8 @@ describe('<OrganizationAccess />', () => {
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete')).toBeNull();
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
expect(network.api.disassociateTeamRole).toHaveBeenCalledWith(5, 3);
expect(network.api.disassociateUserRole).not.toHaveBeenCalled();
expect(network.api.getOrganizationAccessList).toHaveBeenCalledTimes(2);
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
});
});

View File

@ -1,6 +1,9 @@
import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import OrganizationDetail from '../../../../../src/pages/Organizations/screens/Organization/OrganizationDetail';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe('<OrganizationDetail />', () => {
const mockDetails = {
@ -16,6 +19,10 @@ describe('<OrganizationDetail />', () => {
}
};
afterEach(() => {
jest.clearAllMocks();
});
test('initially renders succesfully', () => {
mountWithContexts(
<OrganizationDetail
@ -25,16 +32,15 @@ describe('<OrganizationDetail />', () => {
});
test('should request instance groups from api', () => {
const getOrganizationInstanceGroups = jest.fn();
mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>, { context: {
network: { api: { getOrganizationInstanceGroups }, handleHttpError: () => {} }
network: { handleHttpError: () => {} }
} }
).find('OrganizationDetail');
expect(getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
});
test('should handle setting instance groups to state', async () => {
@ -42,18 +48,18 @@ describe('<OrganizationDetail />', () => {
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
const getOrganizationInstanceGroups = jest.fn(() => (
Promise.resolve({ data: { results: mockInstanceGroups } })
));
OrganizationsAPI.readInstanceGroups.mockResolvedValue({
data: { results: mockInstanceGroups }
});
const wrapper = mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>, { context: {
network: { api: { getOrganizationInstanceGroups }, handleHttpError: () => {} }
network: { handleHttpError: () => {} }
} }
).find('OrganizationDetail');
await getOrganizationInstanceGroups();
await OrganizationsAPI.readInstanceGroups();
expect(wrapper.state().instanceGroups).toEqual(mockInstanceGroups);
});

View File

@ -3,6 +3,10 @@ import { mountWithContexts } from '../../../../enzymeHelpers';
import OrganizationEdit from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
describe('<OrganizationEdit />', () => {
@ -44,10 +48,7 @@ describe('<OrganizationEdit />', () => {
};
wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [], []);
expect(api.updateOrganizationDetails).toHaveBeenCalledWith(
1,
updatedOrgData
);
expect(OrganizationsAPI.update).toHaveBeenCalledWith(1, updatedOrgData);
});
test('handleSubmit associates and disassociates instance groups', async () => {
@ -68,18 +69,9 @@ describe('<OrganizationEdit />', () => {
wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [3, 4], [2]);
await sleep(1);
expect(api.associateInstanceGroup).toHaveBeenCalledWith(
'/api/v2/organizations/1/instance_groups',
3
);
expect(api.associateInstanceGroup).toHaveBeenCalledWith(
'/api/v2/organizations/1/instance_groups',
4
);
expect(api.disassociate).toHaveBeenCalledWith(
'/api/v2/organizations/1/instance_groups',
2
);
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 3);
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(1, 4);
expect(OrganizationsAPI.disassociateInstanceGroup).toHaveBeenCalledWith(1, 2);
});
test('should navigate to organization detail when cancel is clicked', () => {

View File

@ -2,10 +2,13 @@ import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import OrganizationNotifications from '../../../../../src/pages/Organizations/screens/Organization/OrganizationNotifications';
import { sleep } from '../../../../testUtils';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe('<OrganizationNotifications />', () => {
let data;
let network;
const network = {};
beforeEach(() => {
data = {
@ -22,23 +25,13 @@ describe('<OrganizationNotifications />', () => {
notification_type: 'email',
}]
};
network = {
api: {
getOrganizationNotifications: jest.fn()
.mockReturnValue(Promise.resolve({ data })),
getOrganizationNotificationSuccess: jest.fn()
.mockReturnValue(Promise.resolve({
data: { results: [{ id: 1 }] },
})),
getOrganizationNotificationError: jest.fn()
.mockReturnValue(Promise.resolve({
data: { results: [{ id: 2 }] },
})),
createOrganizationNotificationSuccess: jest.fn(),
createOrganizationNotificationError: jest.fn(),
toJSON: () => '/api/',
}
};
OrganizationsAPI.readNotificationTemplates.mockReturnValue({ data });
OrganizationsAPI.readNotificationTemplatesSuccess.mockReturnValue({
data: { results: [{ id: 1 }] },
});
OrganizationsAPI.readNotificationTemplatesError.mockReturnValue({
data: { results: [{ id: 2 }] },
});
});
afterEach(() => {
@ -65,7 +58,7 @@ describe('<OrganizationNotifications />', () => {
await sleep(0);
wrapper.update();
expect(network.api.getOrganizationNotifications).toHaveBeenCalled();
expect(OrganizationsAPI.readNotificationTemplates).toHaveBeenCalled();
expect(wrapper.find('OrganizationNotifications').state('notifications'))
.toEqual(data.results);
const items = wrapper.find('NotificationListItem');
@ -91,7 +84,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([1]);
const items = wrapper.find('NotificationListItem');
items.at(1).find('Switch').at(0).prop('onChange')();
expect(network.api.createOrganizationNotificationSuccess).toHaveBeenCalled();
expect(OrganizationsAPI.associateNotificationTemplatesSuccess).toHaveBeenCalled();
await sleep(0);
wrapper.update();
expect(
@ -114,7 +107,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([2]);
const items = wrapper.find('NotificationListItem');
items.at(0).find('Switch').at(1).prop('onChange')();
expect(network.api.createOrganizationNotificationError).toHaveBeenCalled();
expect(OrganizationsAPI.associateNotificationTemplatesError).toHaveBeenCalled();
await sleep(0);
wrapper.update();
expect(
@ -137,7 +130,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([1]);
const items = wrapper.find('NotificationListItem');
items.at(0).find('Switch').at(0).prop('onChange')();
expect(network.api.createOrganizationNotificationSuccess).toHaveBeenCalled();
expect(OrganizationsAPI.disassociateNotificationTemplatesSuccess).toHaveBeenCalled();
await sleep(0);
wrapper.update();
expect(
@ -160,7 +153,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([2]);
const items = wrapper.find('NotificationListItem');
items.at(1).find('Switch').at(1).prop('onChange')();
expect(network.api.createOrganizationNotificationError).toHaveBeenCalled();
expect(OrganizationsAPI.disassociateNotificationTemplatesError).toHaveBeenCalled();
await sleep(0);
wrapper.update();
expect(

View File

@ -3,6 +3,9 @@ import { shallow } from 'enzyme';
import { mountWithContexts } from '../../../../enzymeHelpers';
import { sleep } from '../../../../testUtils';
import OrganizationTeams, { _OrganizationTeams } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationTeams';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
const listData = {
data: {
@ -17,6 +20,14 @@ const listData = {
}
};
beforeEach(() => {
OrganizationsAPI.readTeams.mockResolvedValue(listData);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('<OrganizationTeams />', () => {
test('renders succesfully', () => {
shallow(
@ -24,25 +35,21 @@ describe('<OrganizationTeams />', () => {
id={1}
searchString=""
location={{ search: '', pathname: '/organizations/1/teams' }}
api={{
readOrganizationTeamsList: jest.fn(),
}}
handleHttpError={() => {}}
/>
);
});
test('should load teams on mount', () => {
const readOrganizationTeamsList = jest.fn(() => Promise.resolve(listData));
mountWithContexts(
<OrganizationTeams
id={1}
searchString=""
/>, { context: {
network: { api: { readOrganizationTeamsList }, handleHttpError: () => {} } }
network: {} }
}
).find('OrganizationTeams');
expect(readOrganizationTeamsList).toHaveBeenCalledWith(1, {
expect(OrganizationsAPI.readTeams).toHaveBeenCalledWith(1, {
page: 1,
page_size: 5,
order_by: 'name',
@ -50,13 +57,12 @@ describe('<OrganizationTeams />', () => {
});
test('should pass fetched teams to PaginatedDatalist', async () => {
const readOrganizationTeamsList = jest.fn(() => Promise.resolve(listData));
const wrapper = mountWithContexts(
<OrganizationTeams
id={1}
searchString=""
/>, { context: {
network: { api: { readOrganizationTeamsList }, handleHttpError: () => {} } }
network: { handleHttpError: () => {} } }
}
);

View File

@ -2,7 +2,6 @@
exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
<OrganizationAccess
api={"/api/"}
handleHttpError={[Function]}
history={"/history/"}
i18n={"/i18n/"}

View File

@ -8,7 +8,6 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
<Provider
value={
Object {
"api": "/api/",
"handleHttpError": [Function],
}
}
@ -18,7 +17,6 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
value={"/config/"}
>
<Provider
api={"/api/"}
handleHttpError={[Function]}
i18n={"/i18n/"}
value={"/config/"}
@ -28,14 +26,12 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
id={1}
>
<withRouter(OrganizationNotifications)
api={"/api/"}
canToggleNotifications={true}
handleHttpError={[Function]}
id={1}
>
<Route>
<OrganizationNotifications
api={"/api/"}
canToggleNotifications={true}
handleHttpError={[Function]}
history={"/history/"}

View File

@ -3,23 +3,17 @@ import React from 'react';
import { mountWithContexts } from '../../../enzymeHelpers';
import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
import { OrganizationsAPI } from '../../../../src/api';
jest.mock('../../../../src/api');
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
describe('<OrganizationAdd />', () => {
let api;
let networkProviderValue;
beforeEach(() => {
api = {
getInstanceGroups: jest.fn(),
createOrganization: jest.fn(),
associateInstanceGroup: jest.fn(),
disassociate: jest.fn(),
};
networkProviderValue = {
api,
handleHttpError: () => {}
};
});
@ -34,7 +28,7 @@ describe('<OrganizationAdd />', () => {
custom_virtualenv: 'Buzz',
};
wrapper.find('OrganizationForm').prop('handleSubmit')(updatedOrgData, [], []);
expect(api.createOrganization).toHaveBeenCalledWith(updatedOrgData);
expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData);
});
test('should navigate to organizations list when cancel is clicked', () => {
@ -70,7 +64,7 @@ describe('<OrganizationAdd />', () => {
description: 'new description',
custom_virtualenv: 'Buzz',
};
api.createOrganization.mockReturnValueOnce({
OrganizationsAPI.create.mockReturnValueOnce({
data: {
id: 5,
related: {
@ -96,7 +90,7 @@ describe('<OrganizationAdd />', () => {
description: 'new description',
custom_virtualenv: 'Buzz',
};
api.createOrganization.mockReturnValueOnce({
OrganizationsAPI.create.mockReturnValueOnce({
data: {
id: 5,
related: {
@ -107,8 +101,8 @@ describe('<OrganizationAdd />', () => {
});
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
await sleep(0);
expect(api.associateInstanceGroup)
.toHaveBeenCalledWith('/api/v2/organizations/5/instance_groups', 3);
expect(OrganizationsAPI.associateInstanceGroup)
.toHaveBeenCalledWith(5, 3);
});
test('AnsibleSelect component renders if there are virtual environments', () => {

View File

@ -2,6 +2,9 @@ import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../enzymeHelpers';
import OrganizationsList, { _OrganizationsList } from '../../../../src/pages/Organizations/screens/OrganizationsList';
import { OrganizationsAPI } from '../../../../src/api';
jest.mock('../../../../src/api');
const mockAPIOrgsList = {
data: {
@ -124,7 +127,7 @@ describe('<OrganizationsList />', () => {
selected: mockAPIOrgsList.data.results
});
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
expect(api.destroyOrganization).toHaveBeenCalledTimes(component.state('selected').length);
expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(component.state('selected').length);
});
test('call fetchOrganizations after org(s) have been deleted', () => {

View File

@ -15,6 +15,7 @@ import styled from 'styled-components';
import { RootDialog } from './contexts/RootDialog';
import { withNetwork } from './contexts/Network';
import { Config } from './contexts/Config';
import { RootAPI } from './api';
import AlertModal from './components/AlertModal';
import About from './components/About';
@ -56,9 +57,9 @@ class App extends Component {
}
async onLogout () {
const { api, handleHttpError } = this.props;
const { handleHttpError } = this.props;
try {
await api.logout();
await RootAPI.logout();
window.location.replace('/#/login');
} catch (err) {
handleHttpError(err);

View File

@ -1,182 +0,0 @@
const API_ROOT = '/api/';
const API_LOGIN = `${API_ROOT}login/`;
const API_LOGOUT = `${API_ROOT}logout/`;
const API_V2 = `${API_ROOT}v2/`;
const API_CONFIG = `${API_V2}config/`;
const API_ME = `${API_V2}me/`;
const API_ORGANIZATIONS = `${API_V2}organizations/`;
const API_INSTANCE_GROUPS = `${API_V2}instance_groups/`;
const API_USERS = `${API_V2}users/`;
const API_TEAMS = `${API_V2}teams/`;
const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
class APIClient {
static getCookie () {
return document.cookie;
}
constructor (httpAdapter) {
this.http = httpAdapter;
}
isAuthenticated () {
const cookie = this.constructor.getCookie();
const parsed = (`; ${cookie}`).split('; userLoggedIn=');
let authenticated = false;
if (parsed.length === 2) {
authenticated = parsed.pop().split(';').shift() === 'true';
}
return authenticated;
}
async login (username, password, redirect = API_CONFIG) {
const un = encodeURIComponent(username);
const pw = encodeURIComponent(password);
const next = encodeURIComponent(redirect);
const data = `username=${un}&password=${pw}&next=${next}`;
const headers = { 'Content-Type': LOGIN_CONTENT_TYPE };
await this.http.get(API_LOGIN, { headers });
const response = await this.http.post(API_LOGIN, data, { headers });
return response;
}
logout () {
return this.http.get(API_LOGOUT);
}
getRoot () {
return this.http.get(API_ROOT);
}
getConfig () {
return this.http.get(API_CONFIG);
}
getMe () {
return this.http.get(API_ME);
}
destroyOrganization (id) {
const endpoint = `${API_ORGANIZATIONS}${id}/`;
return (this.http.delete(endpoint));
}
getOrganizations (params = {}) {
return this.http.get(API_ORGANIZATIONS, { params });
}
createOrganization (data) {
return this.http.post(API_ORGANIZATIONS, data);
}
optionsOrganizations () {
return this.http.options(API_ORGANIZATIONS);
}
getOrganizationAccessList (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
return this.http.get(endpoint, { params });
}
readOrganizationTeamsList (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/teams/`;
return this.http.get(endpoint, { params });
}
getOrganizationDetails (id) {
const endpoint = `${API_ORGANIZATIONS}${id}/`;
return this.http.get(endpoint);
}
updateOrganizationDetails (id, data) {
const endpoint = `${API_ORGANIZATIONS}${id}/`;
return this.http.patch(endpoint, data);
}
getOrganizationInstanceGroups (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/instance_groups/`;
return this.http.get(endpoint, { params });
}
getOrganizationNotifications (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates/`;
return this.http.get(endpoint, { params });
}
getOrganizationNotificationSuccess (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_success/`;
return this.http.get(endpoint, { params });
}
getOrganizationNotificationError (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_error/`;
return this.http.get(endpoint, { params });
}
createOrganizationNotificationSuccess (id, data) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_success/`;
return this.http.post(endpoint, data);
}
createOrganizationNotificationError (id, data) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_error/`;
return this.http.post(endpoint, data);
}
getInstanceGroups (params) {
return this.http.get(API_INSTANCE_GROUPS, { params });
}
associateInstanceGroup (url, id) {
return this.http.post(url, { id });
}
disassociateTeamRole (teamId, roleId) {
const url = `/api/v2/teams/${teamId}/roles/`;
return this.disassociate(url, roleId);
}
disassociateUserRole (accessRecordId, roleId) {
const url = `/api/v2/users/${accessRecordId}/roles/`;
return this.disassociate(url, roleId);
}
disassociate (url, id) {
return this.http.post(url, { id, disassociate: true });
}
readUsers (params) {
return this.http.get(API_USERS, { params });
}
readTeams (params) {
return this.http.get(API_TEAMS, { params });
}
createUserRole (userId, roleId) {
return this.http.post(`${API_USERS}${userId}/roles/`, { id: roleId });
}
createTeamRole (teamId, roleId) {
return this.http.post(`${API_TEAMS}${teamId}/roles/`, { id: roleId });
}
}
export default APIClient;

43
src/api/Base.js Normal file
View File

@ -0,0 +1,43 @@
import axios from 'axios';
const defaultHttp = axios.create({
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken'
});
class Base {
constructor (http = defaultHttp, baseURL) {
this.http = http;
this.baseUrl = baseURL;
}
create (data) {
return this.http.post(this.baseUrl, data);
}
destroy (id) {
return this.http.delete(`${this.baseUrl}${id}/`);
}
read (params = {}) {
return this.http.get(this.baseUrl, { params });
}
readDetail (id) {
return this.http.get(`${this.baseUrl}${id}/`);
}
readOptions () {
return this.http.options(this.baseUrl);
}
replace (id, data) {
return this.http.put(`${this.baseUrl}${id}/`, data);
}
update (id, data) {
return this.http.patch(`${this.baseUrl}${id}/`, data);
}
}
export default Base;

25
src/api/index.js Normal file
View File

@ -0,0 +1,25 @@
import Config from './models/Config';
import InstanceGroups from './models/InstanceGroups';
import Me from './models/Me';
import Organizations from './models/Organizations';
import Root from './models/Root';
import Teams from './models/Teams';
import Users from './models/Users';
const ConfigAPI = new Config();
const InstanceGroupsAPI = new InstanceGroups();
const MeAPI = new Me();
const OrganizationsAPI = new Organizations();
const RootAPI = new Root();
const TeamsAPI = new Teams();
const UsersAPI = new Users();
export {
ConfigAPI,
InstanceGroupsAPI,
MeAPI,
OrganizationsAPI,
RootAPI,
TeamsAPI,
UsersAPI
};

View File

@ -0,0 +1,15 @@
const InstanceGroupsMixin = (parent) => class extends parent {
readInstanceGroups (resourceId, params = {}) {
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, { params });
}
associateInstanceGroup (resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId });
}
disassociateInstanceGroup (resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true });
}
};
export default InstanceGroupsMixin;

View File

@ -0,0 +1,31 @@
const NotificationsMixin = (parent) => class extends parent {
readNotificationTemplates (id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/notification_templates/`, { params });
}
readNotificationTemplatesSuccess (id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/notification_templates_success/`, { params });
}
readNotificationTemplatesError (id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/notification_templates_error/`, { params });
}
associateNotificationTemplatesSuccess (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId });
}
disassociateNotificationTemplatesSuccess (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId, disassociate: true });
}
associateNotificationTemplatesError (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId });
}
disassociateNotificationTemplatesError (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true });
}
};
export default NotificationsMixin;

10
src/api/models/Config.js Normal file
View File

@ -0,0 +1,10 @@
import Base from '../Base';
class Config extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/config/';
}
}
export default Config;

View File

@ -0,0 +1,10 @@
import Base from '../Base';
class InstanceGroups extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/instance_groups/';
}
}
export default InstanceGroups;

10
src/api/models/Me.js Normal file
View File

@ -0,0 +1,10 @@
import Base from '../Base';
class Me extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/me/';
}
}
export default Me;

View File

@ -0,0 +1,20 @@
import Base from '../Base';
import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/organizations/';
}
readAccessList (id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
}
readTeams (id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
}
}
export default Organizations;

30
src/api/models/Root.js Normal file
View File

@ -0,0 +1,30 @@
import Base from '../Base';
class Root extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/';
this.redirectURL = '/api/v2/config/';
}
async login (username, password, redirect = this.redirectURL) {
const loginUrl = `${this.baseUrl}login/`;
const un = encodeURIComponent(username);
const pw = encodeURIComponent(password);
const next = encodeURIComponent(redirect);
const data = `username=${un}&password=${pw}&next=${next}`;
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
await this.http.get(loginUrl, { headers });
const response = await this.http.post(loginUrl, data, { headers });
return response;
}
logout () {
return this.http.get(`${this.baseUrl}logout/`);
}
}
export default Root;

18
src/api/models/Teams.js Normal file
View File

@ -0,0 +1,18 @@
import Base from '../Base';
class Teams extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/teams/';
}
associateRole (teamId, roleId) {
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId });
}
disassociateRole (teamId, roleId) {
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId, disassociate: true });
}
}
export default Teams;

18
src/api/models/Users.js Normal file
View File

@ -0,0 +1,18 @@
import Base from '../Base';
class Users extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/users/';
}
associateRole (userId, roleId) {
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId });
}
disassociateRole (userId, roleId) {
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId, disassociate: true });
}
}
export default Users;

View File

@ -7,6 +7,13 @@ import { withNetwork } from '../../contexts/Network';
import SelectResourceStep from './SelectResourceStep';
import SelectRoleStep from './SelectRoleStep';
import SelectableCard from './SelectableCard';
import { TeamsAPI, UsersAPI } from '../../api';
const readUsers = async (queryParams) => UsersAPI.read(
Object.assign(queryParams, { is_superuser: false })
);
const readTeams = async (queryParams) => TeamsAPI.read(queryParams);
class AddResourceRole extends React.Component {
constructor (props) {
@ -24,8 +31,6 @@ class AddResourceRole extends React.Component {
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
this.handleWizardNext = this.handleWizardNext.bind(this);
this.handleWizardSave = this.handleWizardSave.bind(this);
this.readTeams = this.readTeams.bind(this);
this.readUsers = this.readUsers.bind(this);
}
handleResourceCheckboxClick (user) {
@ -76,8 +81,7 @@ class AddResourceRole extends React.Component {
async handleWizardSave () {
const {
onSave,
api
onSave
} = this.props;
const {
selectedResourceRows,
@ -92,11 +96,11 @@ class AddResourceRole extends React.Component {
for (let j = 0; j < selectedRoleRows.length; j++) {
if (selectedResource === 'users') {
roleRequests.push(
api.createUserRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
);
} else if (selectedResource === 'teams') {
roleRequests.push(
api.createTeamRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
TeamsAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
);
}
}
@ -109,16 +113,6 @@ class AddResourceRole extends React.Component {
}
}
async readUsers (queryParams) {
const { api } = this.props;
return api.readUsers(Object.assign(queryParams, { is_superuser: false }));
}
async readTeams (queryParams) {
const { api } = this.props;
return api.readTeams(queryParams);
}
render () {
const {
selectedResource,
@ -183,7 +177,7 @@ class AddResourceRole extends React.Component {
columns={userColumns}
displayKey="username"
onRowClick={this.handleResourceCheckboxClick}
onSearch={this.readUsers}
onSearch={readUsers}
selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows}
sortedColumnKey="username"
@ -194,7 +188,7 @@ class AddResourceRole extends React.Component {
<SelectResourceStep
columns={teamColumns}
onRowClick={this.handleResourceCheckboxClick}
onSearch={this.readTeams}
onSearch={readTeams}
selectedLabel={i18n._(t`Selected`)}
selectedResourceRows={selectedResourceRows}
itemName="team"

View File

@ -2,6 +2,8 @@ import React, { Component } from 'react';
import { withNetwork } from './Network';
import { ConfigAPI, MeAPI, RootAPI } from '../api';
const ConfigContext = React.createContext({});
class Provider extends Component {
@ -46,13 +48,13 @@ class Provider extends Component {
};
async fetchMe () {
const { api, handleHttpError } = this.props;
const { handleHttpError } = this.props;
try {
const {
data: {
results: [me]
}
} = await api.getMe();
} = await MeAPI.read();
this.setState(prevState => ({
value: {
...prevState.value,
@ -75,13 +77,13 @@ class Provider extends Component {
}
async fetchConfig () {
const { api, handleHttpError } = this.props;
const { handleHttpError } = this.props;
try {
const [configRes, rootRes, meRes] = await Promise.all([
api.getConfig(),
api.getRoot(),
api.getMe()
ConfigAPI.read(),
RootAPI.read(),
MeAPI.read()
]);
this.setState({
value: {

View File

@ -1,5 +1,4 @@
import axios from 'axios';
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
@ -9,8 +8,6 @@ import { t } from '@lingui/macro';
import { withRootDialog } from './RootDialog';
import APIClient from '../api';
const NetworkContext = React.createContext({});
class Provider extends Component {
@ -19,7 +16,6 @@ class Provider extends Component {
this.state = {
value: {
api: new APIClient(axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' })),
handleHttpError: err => {
if (err.response.status === 401) {
this.handle401();

View File

@ -9,6 +9,7 @@ import {
import { withRootDialog } from '../contexts/RootDialog';
import { withNetwork } from '../contexts/Network';
import { RootAPI } from '../api';
import towerLogo from '../../images/tower-logo-header.svg';
@ -39,7 +40,7 @@ class AWXLogin extends Component {
async onLoginButtonClick (event) {
const { username, password, isLoading } = this.state;
const { api, handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
const { handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
event.preventDefault();
@ -51,7 +52,7 @@ class AWXLogin extends Component {
this.setState({ isLoading: true });
try {
const { data } = await api.login(username, password);
const { data } = await RootAPI.login(username, password);
updateConfig(data);
await fetchMe();
this.setState({ isAuthenticated: true, isLoading: false });

View File

@ -9,19 +9,11 @@ import Lookup from '../../../components/Lookup';
import { withNetwork } from '../../../contexts/Network';
import { InstanceGroupsAPI } from '../../../api';
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params);
class InstanceGroupsLookup extends React.Component {
constructor (props) {
super(props);
this.getInstanceGroups = this.getInstanceGroups.bind(this);
}
async getInstanceGroups (params) {
const { api } = this.props;
const data = await api.getInstanceGroups(params);
return data;
}
render () {
const { value, tooltip, onChange, i18n } = this.props;
@ -51,7 +43,7 @@ class InstanceGroupsLookup extends React.Component {
name="instanceGroups"
value={value}
onLookupSave={onChange}
getItems={this.getInstanceGroups}
getItems={getInstanceGroups}
columns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
{ name: i18n._(t`Modified`), key: 'modified', isSortable: false, isNumeric: true },

View File

@ -17,6 +17,7 @@ import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup
import AnsibleSelect from '../../../components/AnsibleSelect';
import InstanceGroupsLookup from './InstanceGroupsLookup';
import { required } from '../../../util/validators';
import { OrganizationsAPI } from '../../../api';
class OrganizationForm extends Component {
constructor (props) {
@ -52,10 +53,9 @@ class OrganizationForm extends Component {
async getRelatedInstanceGroups () {
const {
api,
organization: { id }
} = this.props;
const { data } = await api.getOrganizationInstanceGroups(id);
const { data } = await OrganizationsAPI.readInstanceGroups(id);
return data.results;
}

View File

@ -12,6 +12,7 @@ import OrganizationEdit from './OrganizationEdit';
import OrganizationNotifications from './OrganizationNotifications';
import OrganizationTeams from './OrganizationTeams';
import RoutedTabs from '../../../../components/Tabs/RoutedTabs';
import { OrganizationsAPI } from '../../../../api';
class Organization extends Component {
constructor (props) {
@ -45,22 +46,21 @@ class Organization extends Component {
const {
match,
setBreadcrumb,
api,
handleHttpError
} = this.props;
try {
const [{ data }, notifAdminRest, auditorRes, adminRes] = await Promise.all([
api.getOrganizationDetails(parseInt(match.params.id, 10)),
api.getOrganizations({
OrganizationsAPI.readDetail(parseInt(match.params.id, 10)),
OrganizationsAPI.read({
role_level: 'notification_admin_role',
page_size: 1
}),
api.getOrganizations({
OrganizationsAPI.read({
role_level: 'auditor_role',
id: parseInt(match.params.id, 10)
}),
api.getOrganizations({
OrganizationsAPI.read({
role_level: 'admin_role',
id: parseInt(match.params.id, 10)
})
@ -82,12 +82,11 @@ class Organization extends Component {
const {
match,
setBreadcrumb,
api,
handleHttpError
} = this.props;
try {
const { data } = await api.getOrganizationDetails(parseInt(match.params.id, 10));
const { data } = await OrganizationsAPI.readDetail(parseInt(match.params.id, 10));
setBreadcrumb(data);
this.setState({ organization: data, loading: false });
} catch (error) {

View File

@ -10,6 +10,7 @@ import AddResourceRole from '../../../../components/AddRole/AddResourceRole';
import { withNetwork } from '../../../../contexts/Network';
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
import { Organization } from '../../../../types';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../api';
const QS_CONFIG = getQSConfig('access', {
page: 1,
@ -56,10 +57,10 @@ class OrganizationAccess extends React.Component {
}
async readOrgAccessList () {
const { organization, api, handleHttpError, location } = this.props;
const { organization, handleHttpError, location } = this.props;
this.setState({ isLoading: true });
try {
const { data } = await api.getOrganizationAccessList(
const { data } = await OrganizationsAPI.readAccessList(
organization.id,
parseNamespacedQueryString(QS_CONFIG, location.search)
);
@ -92,7 +93,7 @@ class OrganizationAccess extends React.Component {
}
async removeRole () {
const { api, handleHttpError } = this.props;
const { handleHttpError } = this.props;
const { roleToDelete: role, roleToDeleteAccessRecord: accessRecord } = this.state;
if (!role || !accessRecord) {
return;
@ -101,9 +102,9 @@ class OrganizationAccess extends React.Component {
this.setState({ isLoading: true });
try {
if (type === 'teams') {
await api.disassociateTeamRole(role.team_id, role.id);
await TeamsAPI.disassociateRole(role.team_id, role.id);
} else {
await api.disassociateUserRole(accessRecord.id, role.id);
await UsersAPI.disassociateRole(accessRecord.id, role.id);
}
this.setState({
isLoading: false,

View File

@ -7,6 +7,7 @@ import styled from 'styled-components';
import { DetailList, Detail } from '../../../../components/DetailList';
import { withNetwork } from '../../../../contexts/Network';
import { ChipGroup, Chip } from '../../../../components/Chip';
import { OrganizationsAPI } from '../../../../api';
const CardBody = styled(PFCardBody)`
padding-top: 20px;
@ -29,14 +30,13 @@ class OrganizationDetail extends Component {
async loadInstanceGroups () {
const {
api,
handleHttpError,
match
} = this.props;
try {
const {
data
} = await api.getOrganizationInstanceGroups(match.params.id);
} = await OrganizationsAPI.readInstanceGroups(match.params.id);
this.setState({
instanceGroups: [...data.results]
});

View File

@ -4,6 +4,7 @@ import { withRouter } from 'react-router-dom';
import { CardBody } from '@patternfly/react-core';
import OrganizationForm from '../../components/OrganizationForm';
import { withNetwork } from '../../../../contexts/Network';
import { OrganizationsAPI } from '../../../../api';
class OrganizationEdit extends Component {
constructor (props) {
@ -20,9 +21,9 @@ class OrganizationEdit extends Component {
}
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
const { api, organization, handleHttpError } = this.props;
const { organization, handleHttpError } = this.props;
try {
await api.updateOrganizationDetails(organization.id, values);
await OrganizationsAPI.update(organization.id, values);
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
this.handleSuccess();
} catch (err) {
@ -41,12 +42,17 @@ class OrganizationEdit extends Component {
}
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
const { api, organization, handleHttpError } = this.props;
const url = organization.related.instance_groups;
const { organization, handleHttpError } = this.props;
try {
await Promise.all(groupsToAssociate.map(id => api.associateInstanceGroup(url, id)));
await Promise.all(groupsToDisassociate.map(id => api.disassociate(url, id)));
await Promise.all(
groupsToAssociate.map(id => OrganizationsAPI.associateInstanceGroup(organization.id, id))
);
await Promise.all(
groupsToDisassociate.map(
id => OrganizationsAPI.disassociateInstanceGroup(organization.id, id)
)
);
} catch (err) {
handleHttpError(err) || this.setState({ error: err });
}

View File

@ -5,6 +5,7 @@ import { withNetwork } from '../../../../contexts/Network';
import PaginatedDataList from '../../../../components/PaginatedDataList';
import NotificationListItem from '../../../../components/NotificationsList/NotificationListItem';
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
import { OrganizationsAPI } from '../../../../api';
const QS_CONFIG = getQSConfig('notification', {
page: 1,
@ -49,11 +50,11 @@ class OrganizationNotifications extends Component {
}
async readNotifications () {
const { id, api, handleHttpError, location } = this.props;
const { id, handleHttpError, location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ isLoading: true });
try {
const { data } = await api.getOrganizationNotifications(id, params);
const { data } = await OrganizationsAPI.readNotificationTemplates(id, params);
this.setState(
{
itemCount: data.count || 0,
@ -72,21 +73,22 @@ class OrganizationNotifications extends Component {
}
async readSuccessesAndErrors () {
const { api, handleHttpError, id } = this.props;
const { handleHttpError, id } = this.props;
const { notifications } = this.state;
if (!notifications.length) {
return;
}
const ids = notifications.map(n => n.id).join(',');
try {
const successTemplatesPromise = api.getOrganizationNotificationSuccess(
const successTemplatesPromise = OrganizationsAPI.readNotificationTemplatesSuccess(
id,
{ id__in: ids }
);
const errorTemplatesPromise = api.getOrganizationNotificationError(
const errorTemplatesPromise = OrganizationsAPI.readNotificationTemplatesError(
id,
{ id__in: ids }
);
const { data: successTemplates } = await successTemplatesPromise;
const { data: errorTemplates } = await errorTemplatesPromise;
@ -104,53 +106,65 @@ class OrganizationNotifications extends Component {
toggleNotification = (notificationId, isCurrentlyOn, status) => {
if (status === 'success') {
this.createSuccess(notificationId, isCurrentlyOn);
if (isCurrentlyOn) {
this.disassociateSuccess(notificationId);
} else {
this.associateSuccess(notificationId);
}
} else if (status === 'error') {
this.createError(notificationId, isCurrentlyOn);
if (isCurrentlyOn) {
this.disassociateError(notificationId);
} else {
this.associateError(notificationId);
}
}
};
async createSuccess (notificationId, isCurrentlyOn) {
const { id, api, handleHttpError } = this.props;
const postParams = { id: notificationId };
if (isCurrentlyOn) {
postParams.disassociate = true;
}
async associateSuccess (notificationId) {
const { id, handleHttpError } = this.props;
try {
await api.createOrganizationNotificationSuccess(id, postParams);
if (isCurrentlyOn) {
this.setState((prevState) => ({
successTemplateIds: prevState.successTemplateIds
.filter((templateId) => templateId !== notificationId)
}));
} else {
this.setState(prevState => ({
successTemplateIds: [...prevState.successTemplateIds, notificationId]
}));
}
await OrganizationsAPI.associateNotificationTemplatesSuccess(id, notificationId);
this.setState(prevState => ({
successTemplateIds: [...prevState.successTemplateIds, notificationId]
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
}
async createError (notificationId, isCurrentlyOn) {
const { id, api, handleHttpError } = this.props;
const postParams = { id: notificationId };
if (isCurrentlyOn) {
postParams.disassociate = true;
}
async disassociateSuccess (notificationId) {
const { id, handleHttpError } = this.props;
try {
await api.createOrganizationNotificationError(id, postParams);
if (isCurrentlyOn) {
this.setState((prevState) => ({
errorTemplateIds: prevState.errorTemplateIds
.filter((templateId) => templateId !== notificationId)
}));
} else {
this.setState(prevState => ({
errorTemplateIds: [...prevState.errorTemplateIds, notificationId]
}));
}
await OrganizationsAPI.disassociateNotificationTemplatesSuccess(id, notificationId);
this.setState((prevState) => ({
successTemplateIds: prevState.successTemplateIds
.filter((templateId) => templateId !== notificationId)
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
}
async associateError (notificationId) {
const { id, handleHttpError } = this.props;
try {
await OrganizationsAPI.associateNotificationTemplatesError(id, notificationId);
this.setState(prevState => ({
errorTemplateIds: [...prevState.errorTemplateIds, notificationId]
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
}
async disassociateError (notificationId) {
const { id, handleHttpError } = this.props;
try {
await OrganizationsAPI.disassociateNotificationTemplatesError(id, notificationId);
this.setState((prevState) => ({
errorTemplateIds: prevState.errorTemplateIds
.filter((templateId) => templateId !== notificationId)
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
@ -205,13 +219,6 @@ OrganizationNotifications.propTypes = {
id: number.isRequired,
canToggleNotifications: bool.isRequired,
handleHttpError: func.isRequired,
api: shape({
getOrganizationNotifications: func.isRequired,
getOrganizationNotificationSuccess: func.isRequired,
getOrganizationNotificationError: func.isRequired,
createOrganizationNotificationSuccess: func.isRequired,
createOrganizationNotificationError: func.isRequired,
}).isRequired,
location: shape({
search: string.isRequired,
}).isRequired,

View File

@ -4,6 +4,7 @@ import { withRouter } from 'react-router-dom';
import PaginatedDataList from '../../../../components/PaginatedDataList';
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
import { withNetwork } from '../../../../contexts/Network';
import { OrganizationsAPI } from '../../../../api';
const QS_CONFIG = getQSConfig('team', {
page: 1,
@ -38,13 +39,13 @@ class OrganizationTeams extends React.Component {
}
async readOrganizationTeamsList () {
const { id, api, handleHttpError, location } = this.props;
const { id, handleHttpError, location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ isLoading: true, error: null });
try {
const {
data: { count = 0, results = [] },
} = await api.readOrganizationTeamsList(id, params);
} = await OrganizationsAPI.readTeams(id, params);
this.setState({
itemCount: count,
teams: results,

View File

@ -14,6 +14,7 @@ import {
import { withNetwork } from '../../../contexts/Network';
import CardCloseButton from '../../../components/CardCloseButton';
import OrganizationForm from '../components/OrganizationForm';
import { OrganizationsAPI } from '../../../api';
class OrganizationAdd extends React.Component {
constructor (props) {
@ -29,13 +30,12 @@ class OrganizationAdd extends React.Component {
}
async handleSubmit (values, groupsToAssociate) {
const { api, handleHttpError } = this.props;
const { handleHttpError } = this.props;
try {
const { data: response } = await api.createOrganization(values);
const instanceGroupsUrl = response.related.instance_groups;
const { data: response } = await OrganizationsAPI.create(values);
try {
await Promise.all(groupsToAssociate.map(id => api
.associateInstanceGroup(instanceGroupsUrl, id)));
await Promise.all(groupsToAssociate.map(id => OrganizationsAPI
.associateInstanceGroup(response.id, id)));
this.handleSuccess(response.id);
} catch (err) {
handleHttpError(err) || this.setState({ error: err });
@ -83,10 +83,6 @@ class OrganizationAdd extends React.Component {
}
}
OrganizationAdd.propTypes = {
api: PropTypes.shape().isRequired,
};
OrganizationAdd.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
};

View File

@ -16,6 +16,7 @@ import PaginatedDataList, {
import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem';
import { getQSConfig, parseNamespacedQueryString } from '../../../util/qs';
import { OrganizationsAPI } from '../../../api';
const QS_CONFIG = getQSConfig('organization', {
page: 1,
@ -73,11 +74,11 @@ class OrganizationsList extends Component {
async handleOrgDelete () {
const { selected } = this.state;
const { api, handleHttpError } = this.props;
const { handleHttpError } = this.props;
let errorHandled;
try {
await Promise.all(selected.map((org) => api.destroyOrganization(org.id)));
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
this.setState({
selected: []
});
@ -91,13 +92,13 @@ class OrganizationsList extends Component {
}
async fetchOrganizations () {
const { api, handleHttpError, location } = this.props;
const { handleHttpError, location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ error: false, isLoading: true });
try {
const { data } = await api.getOrganizations(params);
const { data } = await OrganizationsAPI.read(params);
const { count, results } = data;
const stateToUpdate = {
@ -115,10 +116,8 @@ class OrganizationsList extends Component {
}
async fetchOptionsOrganizations () {
const { api } = this.props;
try {
const { data } = await api.optionsOrganizations();
const { data } = await OrganizationsAPI.readOptions();
const { actions } = data;
const stateToUpdate = {