mirror of
https://github.com/ansible/awx.git
synced 2024-11-01 08:21:15 +03:00
update content loading and error handling
unwind error handling use auth cookie as source of truth, fetch config only when authenticated
This commit is contained in:
parent
534418c81a
commit
e72f0bcfd4
@ -13,6 +13,7 @@ Have questions about this document or anything not covered here? Feel free to re
|
|||||||
* [Build the user interface](#build-the-user-interface)
|
* [Build the user interface](#build-the-user-interface)
|
||||||
* [Accessing the AWX web interface](#accessing-the-awx-web-interface)
|
* [Accessing the AWX web interface](#accessing-the-awx-web-interface)
|
||||||
* [AWX REST API Interaction](#awx-rest-api-interaction)
|
* [AWX REST API Interaction](#awx-rest-api-interaction)
|
||||||
|
* [Handling API Errors](#handling-api-errors)
|
||||||
* [Working with React](#working-with-react)
|
* [Working with React](#working-with-react)
|
||||||
* [App structure](#app-structure)
|
* [App structure](#app-structure)
|
||||||
* [Naming files](#naming-files)
|
* [Naming files](#naming-files)
|
||||||
@ -110,6 +111,15 @@ afterEach(() => {
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Handling API Errors
|
||||||
|
API requests can and will fail occasionally so they should include explicit error handling. The three _main_ categories of errors from our perspective are: content loading errors, form submission errors, and other errors. The patterns currently in place for these are described below:
|
||||||
|
|
||||||
|
- **content loading errors** - These are any errors that occur when fetching data to initialize a page or populate a list. For these, we conditionally render a _content error component_ in place of the unresolved content.
|
||||||
|
|
||||||
|
- **form submission errors** - If an error is encountered when submitting a form, we display the error message on the form. For field-specific validation errors, we display the error message beneath the specific field(s). For general errors, we display the error message at the bottom of the form near the action buttons. An error that happens when requesting data to populate a form is not a form submission error, it is still a content error and is handled as such (see above).
|
||||||
|
|
||||||
|
- **other errors** - Most errors will fall into the first two categories, but for miscellaneous actions like toggling notifications, deleting a list item, etc. we display an alert modal to notify the user that their requested action couldn't be performed.
|
||||||
|
|
||||||
## Working with React
|
## Working with React
|
||||||
|
|
||||||
### App structure
|
### App structure
|
||||||
|
@ -24,6 +24,6 @@ To run the unit tests on files that you've changed:
|
|||||||
* `npm test`
|
* `npm test`
|
||||||
|
|
||||||
To run a single test (in this case the login page test):
|
To run a single test (in this case the login page test):
|
||||||
* `npm test -- __tests__/pages/Login.jsx`
|
* `npm test -- __tests__/pages/Login.test.jsx`
|
||||||
|
|
||||||
**note:** Once the test watcher is up and running you can hit `a` to run all the tests
|
**note:** Once the test watcher is up and running you can hit `a` to run all the tests
|
||||||
|
@ -1,16 +1,33 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mountWithContexts, waitForElement } from './enzymeHelpers';
|
import { mountWithContexts, waitForElement } from './enzymeHelpers';
|
||||||
|
|
||||||
import { asyncFlush } from '../jest.setup';
|
import { asyncFlush } from '../jest.setup';
|
||||||
|
|
||||||
import App from '../src/App';
|
import App from '../src/App';
|
||||||
|
import { ConfigAPI, MeAPI, RootAPI } from '../src/api';
|
||||||
import { RootAPI } from '../src/api';
|
|
||||||
|
|
||||||
jest.mock('../src/api');
|
jest.mock('../src/api');
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
|
const ansible_version = '111';
|
||||||
|
const custom_virtualenvs = [];
|
||||||
|
const version = '222';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ConfigAPI.read = () => Promise.resolve({
|
||||||
|
data: {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
test('expected content is rendered', () => {
|
test('expected content is rendered', () => {
|
||||||
const appWrapper = mountWithContexts(
|
const appWrapper = mountWithContexts(
|
||||||
<App
|
<App
|
||||||
@ -34,7 +51,7 @@ describe('<App />', () => {
|
|||||||
render={({ routeGroups }) => (
|
render={({ routeGroups }) => (
|
||||||
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
|
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
|
||||||
)}
|
)}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// page components
|
// page components
|
||||||
@ -54,12 +71,8 @@ describe('<App />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('opening the about modal renders prefetched config data', async (done) => {
|
test('opening the about modal renders prefetched config data', async (done) => {
|
||||||
const ansible_version = '111';
|
const wrapper = mountWithContexts(<App />);
|
||||||
const version = '222';
|
wrapper.update();
|
||||||
|
|
||||||
const config = { ansible_version, version };
|
|
||||||
|
|
||||||
const wrapper = mountWithContexts(<App />, { context: { config } });
|
|
||||||
|
|
||||||
// open about modal
|
// open about modal
|
||||||
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||||
@ -67,9 +80,11 @@ describe('<App />', () => {
|
|||||||
const aboutModalContent = 'AboutModalBoxContent';
|
const aboutModalContent = 'AboutModalBoxContent';
|
||||||
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
const aboutModalClose = 'button[aria-label="Close Dialog"]';
|
||||||
|
|
||||||
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
|
await waitForElement(wrapper, aboutDropdown);
|
||||||
wrapper.find(aboutDropdown).simulate('click');
|
wrapper.find(aboutDropdown).simulate('click');
|
||||||
wrapper.find(aboutButton).simulate('click');
|
|
||||||
|
const button = await waitForElement(wrapper, aboutButton, el => !el.props().disabled);
|
||||||
|
button.simulate('click');
|
||||||
|
|
||||||
// check about modal content
|
// check about modal content
|
||||||
const content = await waitForElement(wrapper, aboutModalContent);
|
const content = await waitForElement(wrapper, aboutModalContent);
|
||||||
@ -83,24 +98,21 @@ describe('<App />', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
test('handleNavToggle sets state.isNavOpen to opposite', () => {
|
||||||
const appWrapper = mountWithContexts(<App />).find('App');
|
const appWrapper = mountWithContexts(<App />).find('App');
|
||||||
const { onNavToggle } = appWrapper.instance();
|
|
||||||
|
const { handleNavToggle } = appWrapper.instance();
|
||||||
[true, false, true, false, true].forEach(expected => {
|
[true, false, true, false, true].forEach(expected => {
|
||||||
expect(appWrapper.state().isNavOpen).toBe(expected);
|
expect(appWrapper.state().isNavOpen).toBe(expected);
|
||||||
onNavToggle();
|
handleNavToggle();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onLogout makes expected call to api client', async (done) => {
|
test('onLogout makes expected call to api client', async (done) => {
|
||||||
const appWrapper = mountWithContexts(<App />, {
|
const appWrapper = mountWithContexts(<App />).find('App');
|
||||||
context: { network: { handleHttpError: () => {} } }
|
appWrapper.instance().handleLogout();
|
||||||
}).find('App');
|
|
||||||
|
|
||||||
appWrapper.instance().onLogout();
|
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
|
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,20 +2,16 @@
|
|||||||
|
|
||||||
exports[`mountWithContexts injected ConfigProvider should mount and render with custom Config value 1`] = `
|
exports[`mountWithContexts injected ConfigProvider should mount and render with custom Config value 1`] = `
|
||||||
<Foo>
|
<Foo>
|
||||||
<Config>
|
|
||||||
<div>
|
<div>
|
||||||
Fizz
|
Fizz
|
||||||
1.1
|
1.1
|
||||||
</div>
|
</div>
|
||||||
</Config>
|
|
||||||
</Foo>
|
</Foo>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`mountWithContexts injected ConfigProvider should mount and render with default values 1`] = `
|
exports[`mountWithContexts injected ConfigProvider should mount and render with default values 1`] = `
|
||||||
<Foo>
|
<Foo>
|
||||||
<Config>
|
|
||||||
<div />
|
<div />
|
||||||
</Config>
|
|
||||||
</Foo>
|
</Foo>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
handleHttpError={() => {}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -34,7 +33,6 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
|
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
handleHttpError={() => {}}
|
|
||||||
/>
|
/>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
|
|
||||||
@ -57,7 +55,6 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
handleHttpError={() => {}}
|
|
||||||
/>
|
/>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mountWithContexts } from '../enzymeHelpers';
|
|
||||||
|
|
||||||
import { _NotifyAndRedirect } from '../../src/components/NotifyAndRedirect';
|
|
||||||
|
|
||||||
describe('<NotifyAndRedirect />', () => {
|
|
||||||
test('initially renders succesfully and calls setRootDialogMessage', () => {
|
|
||||||
const setRootDialogMessage = jest.fn();
|
|
||||||
mountWithContexts(
|
|
||||||
<_NotifyAndRedirect
|
|
||||||
to="foo"
|
|
||||||
setRootDialogMessage={setRootDialogMessage}
|
|
||||||
location={{ pathname: 'foo' }}
|
|
||||||
i18n={{ _: val => val.toString() }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(setRootDialogMessage).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
@ -3,12 +3,10 @@
|
|||||||
* derived from https://lingui.js.org/guides/testing.html
|
* derived from https://lingui.js.org/guides/testing.html
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shape, object, string, arrayOf, func } from 'prop-types';
|
import { shape, object, string, arrayOf } from 'prop-types';
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { ConfigProvider } from '../src/contexts/Config';
|
import { ConfigProvider } from '../src/contexts/Config';
|
||||||
import { _NetworkProvider } from '../src/contexts/Network';
|
|
||||||
import { RootDialogProvider } from '../src/contexts/RootDialog';
|
|
||||||
|
|
||||||
const language = 'en-US';
|
const language = 'en-US';
|
||||||
const intlProvider = new I18nProvider(
|
const intlProvider = new I18nProvider(
|
||||||
@ -36,8 +34,6 @@ const defaultContexts = {
|
|||||||
ansible_version: null,
|
ansible_version: null,
|
||||||
custom_virtualenvs: [],
|
custom_virtualenvs: [],
|
||||||
version: null,
|
version: null,
|
||||||
custom_logo: null,
|
|
||||||
custom_login_info: null,
|
|
||||||
toJSON: () => '/config/'
|
toJSON: () => '/config/'
|
||||||
},
|
},
|
||||||
router: {
|
router: {
|
||||||
@ -69,30 +65,19 @@ const defaultContexts = {
|
|||||||
},
|
},
|
||||||
toJSON: () => '/router/',
|
toJSON: () => '/router/',
|
||||||
},
|
},
|
||||||
network: {
|
|
||||||
handleHttpError: () => {},
|
|
||||||
},
|
|
||||||
dialog: {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function wrapContexts (node, context) {
|
function wrapContexts (node, context) {
|
||||||
const { config, network, dialog } = context;
|
const { config } = context;
|
||||||
class Wrap extends React.Component {
|
class Wrap extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
// eslint-disable-next-line react/no-this-in-sfc
|
// eslint-disable-next-line react/no-this-in-sfc
|
||||||
const { children, ...props } = this.props;
|
const { children, ...props } = this.props;
|
||||||
const component = React.cloneElement(children, props);
|
const component = React.cloneElement(children, props);
|
||||||
return (
|
return (
|
||||||
<RootDialogProvider value={dialog}>
|
<ConfigProvider value={config}>
|
||||||
<_NetworkProvider value={network}>
|
|
||||||
<ConfigProvider
|
|
||||||
value={config}
|
|
||||||
i18n={defaultContexts.linguiPublisher.i18n}
|
|
||||||
>
|
|
||||||
{component}
|
{component}
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</_NetworkProvider>
|
|
||||||
</RootDialogProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,8 +116,6 @@ export function mountWithContexts (node, options = {}) {
|
|||||||
ansible_version: string,
|
ansible_version: string,
|
||||||
custom_virtualenvs: arrayOf(string),
|
custom_virtualenvs: arrayOf(string),
|
||||||
version: string,
|
version: string,
|
||||||
custom_logo: string,
|
|
||||||
custom_login_info: string,
|
|
||||||
}),
|
}),
|
||||||
router: shape({
|
router: shape({
|
||||||
route: shape({
|
route: shape({
|
||||||
@ -141,36 +124,31 @@ export function mountWithContexts (node, options = {}) {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
history: shape({}).isRequired,
|
history: shape({}).isRequired,
|
||||||
}),
|
}),
|
||||||
network: shape({
|
|
||||||
handleHttpError: func.isRequired,
|
|
||||||
}),
|
|
||||||
dialog: shape({
|
|
||||||
title: string,
|
|
||||||
setRootDialogMessage: func,
|
|
||||||
clearRootDialogMessage: func,
|
|
||||||
}),
|
|
||||||
...options.childContextTypes
|
...options.childContextTypes
|
||||||
};
|
};
|
||||||
return mount(wrapContexts(node, context), { context, childContextTypes });
|
return mount(wrapContexts(node, context), { context, childContextTypes });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for element to exist.
|
* Wait for element(s) to achieve a desired state.
|
||||||
*
|
*
|
||||||
* @param[wrapper] - A ReactWrapper instance
|
* @param[wrapper] - A ReactWrapper instance
|
||||||
* @param[selector] - The selector of the element to wait for.
|
* @param[selector] - The selector of the element(s) to wait for.
|
||||||
|
* @param[callback] - Callback to poll - by default this checks for a node count of 1.
|
||||||
*/
|
*/
|
||||||
export function waitForElement (wrapper, selector) {
|
export function waitForElement (wrapper, selector, callback = el => el.length === 1) {
|
||||||
const interval = 100;
|
const interval = 100;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let attempts = 30;
|
let attempts = 30;
|
||||||
(function pollElement () {
|
(function pollElement () {
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
if (wrapper.exists(selector)) {
|
const el = wrapper.find(selector);
|
||||||
return resolve(wrapper.find(selector));
|
if (callback(el)) {
|
||||||
|
return resolve(el);
|
||||||
}
|
}
|
||||||
if (--attempts <= 0) {
|
if (--attempts <= 0) {
|
||||||
return reject(new Error(`Element not found using ${selector}`));
|
const message = `Expected condition for <${selector}> not met: ${callback.toString()}`;
|
||||||
|
return reject(new Error(message));
|
||||||
}
|
}
|
||||||
return setTimeout(pollElement, interval);
|
return setTimeout(pollElement, interval);
|
||||||
}());
|
}());
|
||||||
|
@ -4,7 +4,6 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { mountWithContexts, waitForElement } from './enzymeHelpers';
|
import { mountWithContexts, waitForElement } from './enzymeHelpers';
|
||||||
import { Config } from '../src/contexts/Config';
|
import { Config } from '../src/contexts/Config';
|
||||||
import { withRootDialog } from '../src/contexts/RootDialog';
|
|
||||||
|
|
||||||
describe('mountWithContexts', () => {
|
describe('mountWithContexts', () => {
|
||||||
describe('injected I18nProvider', () => {
|
describe('injected I18nProvider', () => {
|
||||||
@ -109,68 +108,6 @@ describe('mountWithContexts', () => {
|
|||||||
expect(wrapper.find('Foo')).toMatchSnapshot();
|
expect(wrapper.find('Foo')).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('injected root dialog', () => {
|
|
||||||
it('should mount and render', () => {
|
|
||||||
const Foo = ({ title, setRootDialogMessage }) => (
|
|
||||||
<div>
|
|
||||||
<span>{title}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRootDialogMessage({ title: 'error' })}
|
|
||||||
>
|
|
||||||
click
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const Bar = withRootDialog(Foo);
|
|
||||||
const wrapper = mountWithContexts(<Bar />);
|
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toEqual('');
|
|
||||||
wrapper.find('button').simulate('click');
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.find('span').text()).toEqual('error');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should mount and render with stubbed value', () => {
|
|
||||||
const dialog = {
|
|
||||||
title: 'this be the title',
|
|
||||||
setRootDialogMessage: jest.fn(),
|
|
||||||
};
|
|
||||||
const Foo = ({ title, setRootDialogMessage }) => (
|
|
||||||
<div>
|
|
||||||
<span>{title}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRootDialogMessage('error')}
|
|
||||||
>
|
|
||||||
click
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const Bar = withRootDialog(Foo);
|
|
||||||
const wrapper = mountWithContexts(<Bar />, { context: { dialog } });
|
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toEqual('this be the title');
|
|
||||||
wrapper.find('button').simulate('click');
|
|
||||||
expect(dialog.setRootDialogMessage).toHaveBeenCalledWith('error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set props on wrapped component', () => {
|
|
||||||
function TestComponent ({ text }) {
|
|
||||||
return (<div>{text}</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<TestComponent text="foo" />
|
|
||||||
);
|
|
||||||
expect(wrapper.find('div').text()).toEqual('foo');
|
|
||||||
wrapper.setProps({
|
|
||||||
text: 'bar'
|
|
||||||
});
|
|
||||||
expect(wrapper.find('div').text()).toEqual('bar');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -184,9 +121,7 @@ class TestAsyncComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
setTimeout(() => {
|
setTimeout(() => this.setState({ displayElement: true }), 500);
|
||||||
this.setState({ displayElement: true });
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@ -211,16 +146,15 @@ describe('waitForElement', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('eventually throws an error for elements that don\'t exist', async (done) => {
|
it('eventually throws an error for elements that don\'t exist', async (done) => {
|
||||||
const selector = '#does-not-exist';
|
|
||||||
const wrapper = mountWithContexts(<div />);
|
const wrapper = mountWithContexts(<div />);
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
await waitForElement(wrapper, selector);
|
await waitForElement(wrapper, '#does-not-exist');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
} finally {
|
} finally {
|
||||||
expect(error).toEqual(new Error(`Element not found using ${selector}`));
|
expect(error).toEqual(new Error('Expected condition for <#does-not-exist> not met: el => el.length === 1'));
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,169 +1,215 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '../enzymeHelpers';
|
||||||
import { asyncFlush } from '../../jest.setup';
|
|
||||||
import AWXLogin from '../../src/pages/Login';
|
import AWXLogin from '../../src/pages/Login';
|
||||||
import { RootAPI } from '../../src/api';
|
import { RootAPI } from '../../src/api';
|
||||||
|
|
||||||
jest.mock('../../src/api');
|
jest.mock('../../src/api');
|
||||||
|
|
||||||
describe('<Login />', () => {
|
describe('<Login />', () => {
|
||||||
let loginWrapper;
|
async function findChildren (wrapper) {
|
||||||
let awxLogin;
|
const [
|
||||||
let loginPage;
|
awxLogin,
|
||||||
let loginForm;
|
loginPage,
|
||||||
let usernameInput;
|
loginForm,
|
||||||
let passwordInput;
|
usernameInput,
|
||||||
let submitButton;
|
passwordInput,
|
||||||
let loginHeaderLogo;
|
submitButton,
|
||||||
|
loginHeaderLogo,
|
||||||
const mountLogin = () => {
|
] = await Promise.all([
|
||||||
loginWrapper = mountWithContexts(<AWXLogin />, { context: { network: {} } });
|
waitForElement(wrapper, 'AWXLogin', (el) => el.length === 1),
|
||||||
|
waitForElement(wrapper, 'LoginPage', (el) => el.length === 1),
|
||||||
|
waitForElement(wrapper, 'LoginForm', (el) => el.length === 1),
|
||||||
|
waitForElement(wrapper, 'input#pf-login-username-id', (el) => el.length === 1),
|
||||||
|
waitForElement(wrapper, 'input#pf-login-password-id', (el) => el.length === 1),
|
||||||
|
waitForElement(wrapper, 'Button[type="submit"]', (el) => el.length === 1),
|
||||||
|
waitForElement(wrapper, 'img', (el) => el.length === 1),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
awxLogin,
|
||||||
|
loginPage,
|
||||||
|
loginForm,
|
||||||
|
usernameInput,
|
||||||
|
passwordInput,
|
||||||
|
submitButton,
|
||||||
|
loginHeaderLogo,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const findChildren = () => {
|
beforeEach(() => {
|
||||||
awxLogin = loginWrapper.find('AWXLogin');
|
RootAPI.read.mockResolvedValue({
|
||||||
loginPage = loginWrapper.find('LoginPage');
|
data: {
|
||||||
loginForm = loginWrapper.find('LoginForm');
|
custom_login_info: '',
|
||||||
usernameInput = loginWrapper.find('input#pf-login-username-id');
|
custom_logo: 'images/foo.jpg'
|
||||||
passwordInput = loginWrapper.find('input#pf-login-password-id');
|
}
|
||||||
submitButton = loginWrapper.find('Button[type="submit"]');
|
});
|
||||||
loginHeaderLogo = loginPage.find('img');
|
});
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
loginWrapper.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', async (done) => {
|
||||||
mountLogin();
|
const loginWrapper = mountWithContexts(
|
||||||
findChildren();
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
expect(loginWrapper.length).toBe(1);
|
);
|
||||||
expect(loginPage.length).toBe(1);
|
const {
|
||||||
expect(loginForm.length).toBe(1);
|
awxLogin,
|
||||||
expect(usernameInput.length).toBe(1);
|
usernameInput,
|
||||||
|
passwordInput,
|
||||||
|
submitButton,
|
||||||
|
} = await findChildren(loginWrapper);
|
||||||
expect(usernameInput.props().value).toBe('');
|
expect(usernameInput.props().value).toBe('');
|
||||||
expect(passwordInput.length).toBe(1);
|
|
||||||
expect(passwordInput.props().value).toBe('');
|
expect(passwordInput.props().value).toBe('');
|
||||||
expect(awxLogin.state().isInputValid).toBe(true);
|
expect(awxLogin.state('validationError')).toBe(false);
|
||||||
expect(submitButton.length).toBe(1);
|
|
||||||
expect(submitButton.props().isDisabled).toBe(false);
|
expect(submitButton.props().isDisabled).toBe(false);
|
||||||
expect(loginHeaderLogo.length).toBe(1);
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('custom logo renders Brand component with correct src and alt', () => {
|
test('custom logo renders Brand component with correct src and alt', async (done) => {
|
||||||
loginWrapper = mountWithContexts(<AWXLogin logo="images/foo.jpg" alt="Foo Application" />);
|
const loginWrapper = mountWithContexts(
|
||||||
findChildren();
|
<AWXLogin alt="Foo Application" isAuthenticated={() => false} />
|
||||||
expect(loginHeaderLogo.length).toBe(1);
|
);
|
||||||
expect(loginHeaderLogo.props().src).toBe('data:image/jpeg;images/foo.jpg');
|
const { loginHeaderLogo } = await findChildren(loginWrapper);
|
||||||
expect(loginHeaderLogo.props().alt).toBe('Foo Application');
|
const { alt, src } = loginHeaderLogo.props();
|
||||||
|
expect([alt, src]).toEqual(['Foo Application', 'data:image/jpeg;images/foo.jpg']);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('default logo renders Brand component with correct src and alt', () => {
|
test('default logo renders Brand component with correct src and alt', async (done) => {
|
||||||
mountLogin();
|
RootAPI.read.mockResolvedValue({ data: {} });
|
||||||
findChildren();
|
const loginWrapper = mountWithContexts(
|
||||||
expect(loginHeaderLogo.length).toBe(1);
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
expect(loginHeaderLogo.props().src).toBe('brand-logo.svg');
|
);
|
||||||
expect(loginHeaderLogo.props().alt).toBe('AWX');
|
const { loginHeaderLogo } = await findChildren(loginWrapper);
|
||||||
|
const { alt, src } = loginHeaderLogo.props();
|
||||||
|
expect([alt, src]).toEqual(['AWX', 'brand-logo.svg']);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('state maps to un/pw input value props', () => {
|
test('default logo renders on data initialization error', async (done) => {
|
||||||
mountLogin();
|
RootAPI.read.mockRejectedValueOnce({ response: { status: 500 } });
|
||||||
findChildren();
|
const loginWrapper = mountWithContexts(
|
||||||
awxLogin.setState({ username: 'un', password: 'pw' });
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
expect(awxLogin.state().username).toBe('un');
|
);
|
||||||
expect(awxLogin.state().password).toBe('pw');
|
const { loginHeaderLogo } = await findChildren(loginWrapper);
|
||||||
findChildren();
|
const { alt, src } = loginHeaderLogo.props();
|
||||||
expect(usernameInput.props().value).toBe('un');
|
expect([alt, src]).toEqual(['AWX', 'brand-logo.svg']);
|
||||||
expect(passwordInput.props().value).toBe('pw');
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updating un/pw clears out error', () => {
|
test('state maps to un/pw input value props', async (done) => {
|
||||||
mountLogin();
|
const loginWrapper = mountWithContexts(
|
||||||
findChildren();
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
awxLogin.setState({ isInputValid: false });
|
);
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
const { usernameInput, passwordInput } = await findChildren(loginWrapper);
|
||||||
usernameInput.instance().value = 'uname';
|
usernameInput.props().onChange({ currentTarget: { value: 'un' } });
|
||||||
usernameInput.simulate('change');
|
passwordInput.props().onChange({ currentTarget: { value: 'pw' } });
|
||||||
expect(awxLogin.state().username).toBe('uname');
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'un');
|
||||||
expect(awxLogin.state().isInputValid).toBe(true);
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'pw');
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
done();
|
||||||
awxLogin.setState({ isInputValid: false });
|
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
|
|
||||||
passwordInput.instance().value = 'pword';
|
|
||||||
passwordInput.simulate('change');
|
|
||||||
expect(awxLogin.state().password).toBe('pword');
|
|
||||||
expect(awxLogin.state().isInputValid).toBe(true);
|
|
||||||
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('login API not called when loading', () => {
|
test('handles input validation errors and clears on input value change', async (done) => {
|
||||||
mountLogin();
|
const formError = '.pf-c-form__helper-text.pf-m-error';
|
||||||
findChildren();
|
const loginWrapper = mountWithContexts(
|
||||||
expect(awxLogin.state().isLoading).toBe(false);
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
awxLogin.setState({ isLoading: true });
|
);
|
||||||
|
const {
|
||||||
|
usernameInput,
|
||||||
|
passwordInput,
|
||||||
|
submitButton
|
||||||
|
} = await findChildren(loginWrapper);
|
||||||
|
|
||||||
|
RootAPI.login.mockRejectedValueOnce({ response: { status: 401 } });
|
||||||
|
usernameInput.props().onChange({ currentTarget: { value: 'invalid' } });
|
||||||
|
passwordInput.props().onChange({ currentTarget: { value: 'invalid' } });
|
||||||
|
submitButton.simulate('click');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'invalid');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'invalid');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('validationError') === true);
|
||||||
|
await waitForElement(loginWrapper, formError, (el) => el.length === 1);
|
||||||
|
|
||||||
|
usernameInput.props().onChange({ currentTarget: { value: 'dsarif' } });
|
||||||
|
passwordInput.props().onChange({ currentTarget: { value: 'freneticpny' } });
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'dsarif');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'freneticpny');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('validationError') === false);
|
||||||
|
await waitForElement(loginWrapper, formError, (el) => el.length === 0);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles other errors and clears on resubmit', async (done) => {
|
||||||
|
const loginWrapper = mountWithContexts(
|
||||||
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
usernameInput,
|
||||||
|
passwordInput,
|
||||||
|
submitButton
|
||||||
|
} = await findChildren(loginWrapper);
|
||||||
|
|
||||||
|
RootAPI.login.mockRejectedValueOnce({ response: { status: 500 } });
|
||||||
|
submitButton.simulate('click');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('authenticationError') === true);
|
||||||
|
|
||||||
|
usernameInput.props().onChange({ currentTarget: { value: 'sgrimes' } });
|
||||||
|
passwordInput.props().onChange({ currentTarget: { value: 'ovid' } });
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'sgrimes');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'ovid');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('authenticationError') === true);
|
||||||
|
|
||||||
|
submitButton.simulate('click');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('authenticationError') === false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no login requests are made when already authenticating', async (done) => {
|
||||||
|
const loginWrapper = mountWithContexts(
|
||||||
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
|
);
|
||||||
|
const { awxLogin, submitButton } = await findChildren(loginWrapper);
|
||||||
|
|
||||||
|
awxLogin.setState({ isAuthenticating: true });
|
||||||
|
submitButton.simulate('click');
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(RootAPI.login).toHaveBeenCalledTimes(0);
|
expect(RootAPI.login).toHaveBeenCalledTimes(0);
|
||||||
});
|
|
||||||
|
|
||||||
test('submit calls login API successfully', async () => {
|
awxLogin.setState({ isAuthenticating: false });
|
||||||
RootAPI.login = jest.fn().mockImplementation(() => Promise.resolve({}));
|
submitButton.simulate('click');
|
||||||
mountLogin();
|
|
||||||
findChildren();
|
|
||||||
expect(awxLogin.state().isLoading).toBe(false);
|
|
||||||
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
expect(RootAPI.login).toHaveBeenCalledTimes(1);
|
expect(RootAPI.login).toHaveBeenCalledTimes(1);
|
||||||
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
|
||||||
expect(awxLogin.state().isLoading).toBe(true);
|
done();
|
||||||
await asyncFlush();
|
|
||||||
expect(awxLogin.state().isLoading).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls login API and handles 401 error', async () => {
|
test('submit calls api.login successfully', async (done) => {
|
||||||
RootAPI.login = jest.fn().mockImplementation(() => {
|
const loginWrapper = mountWithContexts(
|
||||||
const err = new Error('401 error');
|
<AWXLogin isAuthenticated={() => false} />
|
||||||
err.response = { status: 401, message: 'problem' };
|
);
|
||||||
return Promise.reject(err);
|
const {
|
||||||
});
|
usernameInput,
|
||||||
mountLogin();
|
passwordInput,
|
||||||
findChildren();
|
submitButton,
|
||||||
expect(awxLogin.state().isLoading).toBe(false);
|
} = await findChildren(loginWrapper);
|
||||||
expect(awxLogin.state().isInputValid).toBe(true);
|
|
||||||
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
usernameInput.props().onChange({ currentTarget: { value: 'gthorpe' } });
|
||||||
|
passwordInput.props().onChange({ currentTarget: { value: 'hydro' } });
|
||||||
submitButton.simulate('click');
|
submitButton.simulate('click');
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('isAuthenticating') === true);
|
||||||
|
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('isAuthenticating') === false);
|
||||||
expect(RootAPI.login).toHaveBeenCalledTimes(1);
|
expect(RootAPI.login).toHaveBeenCalledTimes(1);
|
||||||
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
|
expect(RootAPI.login).toHaveBeenCalledWith('gthorpe', 'hydro');
|
||||||
expect(awxLogin.state().isLoading).toBe(true);
|
|
||||||
await asyncFlush();
|
done();
|
||||||
expect(awxLogin.state().isInputValid).toBe(false);
|
|
||||||
expect(awxLogin.state().isLoading).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submit calls login API and handles non-401 error', async () => {
|
test('render Redirect to / when already authenticated', async (done) => {
|
||||||
RootAPI.login = jest.fn().mockImplementation(() => {
|
const loginWrapper = mountWithContexts(
|
||||||
const err = new Error('500 error');
|
<AWXLogin isAuthenticated={() => true} />
|
||||||
err.response = { status: 500, message: 'problem' };
|
);
|
||||||
return Promise.reject(err);
|
await waitForElement(loginWrapper, 'Redirect', (el) => el.length === 1);
|
||||||
});
|
await waitForElement(loginWrapper, 'Redirect', (el) => el.props().to === '/');
|
||||||
mountLogin();
|
done();
|
||||||
findChildren();
|
|
||||||
expect(awxLogin.state().isLoading).toBe(false);
|
|
||||||
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
|
|
||||||
submitButton.simulate('click');
|
|
||||||
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('render Redirect to / when already authenticated', () => {
|
|
||||||
mountLogin();
|
|
||||||
findChildren();
|
|
||||||
awxLogin.setState({ isAuthenticated: true });
|
|
||||||
const redirectElem = loginWrapper.find('Redirect');
|
|
||||||
expect(redirectElem.length).toBe(1);
|
|
||||||
expect(redirectElem.props().to).toBe('/');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import { mountWithContexts } from '../../enzymeHelpers';
|
import { mountWithContexts } from '../../enzymeHelpers';
|
||||||
import Organizations from '../../../src/pages/Organizations/Organizations';
|
import Organizations from '../../../src/pages/Organizations/Organizations';
|
||||||
|
|
||||||
|
jest.mock('../../../src/api');
|
||||||
|
|
||||||
describe('<Organizations />', () => {
|
describe('<Organizations />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
|
@ -1,63 +1,232 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createMemoryHistory } from 'history';
|
import { mountWithContexts, waitForElement } from '../../../../enzymeHelpers';
|
||||||
import { mountWithContexts } from '../../../../enzymeHelpers';
|
|
||||||
import { sleep } from '../../../../testUtils';
|
|
||||||
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
|
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
|
||||||
import { OrganizationsAPI } from '../../../../../src/api';
|
import { OrganizationsAPI } from '../../../../../src/api';
|
||||||
|
|
||||||
jest.mock('../../../../../src/api');
|
jest.mock('../../../../../src/api');
|
||||||
|
|
||||||
describe.only('<Organization />', () => {
|
const mockMe = {
|
||||||
const me = {
|
|
||||||
is_super_user: true,
|
is_super_user: true,
|
||||||
is_system_auditor: false
|
is_system_auditor: false
|
||||||
};
|
};
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
const mockNoResults = {
|
||||||
mountWithContexts(<Organization me={me} />);
|
count: 0,
|
||||||
});
|
next: null,
|
||||||
|
previous: null,
|
||||||
|
data: { results: [] }
|
||||||
|
};
|
||||||
|
|
||||||
test('notifications tab shown/hidden based on permissions', async () => {
|
const mockDetails = {
|
||||||
OrganizationsAPI.readDetail.mockResolvedValue({
|
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
type: 'organization',
|
||||||
|
url: '/api/v2/organizations/1/',
|
||||||
|
related: {
|
||||||
|
notification_templates: '/api/v2/organizations/1/notification_templates/',
|
||||||
|
notification_templates_any: '/api/v2/organizations/1/notification_templates_any/',
|
||||||
|
notification_templates_success: '/api/v2/organizations/1/notification_templates_success/',
|
||||||
|
notification_templates_error: '/api/v2/organizations/1/notification_templates_error/',
|
||||||
|
object_roles: '/api/v2/organizations/1/object_roles/',
|
||||||
|
access_list: '/api/v2/organizations/1/access_list/',
|
||||||
|
instance_groups: '/api/v2/organizations/1/instance_groups/'
|
||||||
|
},
|
||||||
|
summary_fields: {
|
||||||
|
created_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
modified_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
object_roles: {
|
||||||
|
admin_role: {
|
||||||
|
description: 'Can manage all aspects of the organization',
|
||||||
|
name: 'Admin',
|
||||||
|
id: 42
|
||||||
|
},
|
||||||
|
notification_admin_role: {
|
||||||
|
description: 'Can manage all notifications of the organization',
|
||||||
|
name: 'Notification Admin',
|
||||||
|
id: 1683
|
||||||
|
},
|
||||||
|
auditor_role: {
|
||||||
|
description: 'Can view all aspects of the organization',
|
||||||
|
name: 'Auditor',
|
||||||
|
id: 41
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true
|
||||||
|
},
|
||||||
|
related_field_counts: {
|
||||||
|
users: 51,
|
||||||
|
admins: 19,
|
||||||
|
inventories: 23,
|
||||||
|
teams: 12,
|
||||||
|
projects: 33,
|
||||||
|
job_templates: 30
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
OrganizationsAPI.read.mockResolvedValue({
|
created: '2015-07-07T17:21:26.429745Z',
|
||||||
data: {
|
modified: '2017-09-05T19:23:15.418808Z',
|
||||||
results: []
|
name: 'Sarif Industries',
|
||||||
|
description: '',
|
||||||
|
max_hosts: 0,
|
||||||
|
custom_virtualenv: null
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
const history = createMemoryHistory({
|
|
||||||
initialEntries: ['/organizations/1/details'],
|
const adminOrganization = {
|
||||||
});
|
id: 1,
|
||||||
const match = { path: '/organizations/:id', url: '/organizations/1' };
|
type: 'organization',
|
||||||
const wrapper = mountWithContexts(
|
url: '/api/v2/organizations/1/',
|
||||||
<Organization
|
related: {
|
||||||
me={me}
|
instance_groups: '/api/v2/organizations/1/instance_groups/',
|
||||||
setBreadcrumb={() => {}}
|
object_roles: '/api/v2/organizations/1/object_roles/',
|
||||||
/>,
|
access_list: '/api/v2/organizations/1/access_list/',
|
||||||
{
|
},
|
||||||
context: {
|
summary_fields: {
|
||||||
router: {
|
created_by: {
|
||||||
history,
|
id: 1,
|
||||||
route: {
|
username: 'admin',
|
||||||
location: history.location,
|
first_name: 'Super',
|
||||||
match
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
modified_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2015-07-07T17:21:26.429745Z',
|
||||||
|
modified: '2017-09-05T19:23:15.418808Z',
|
||||||
|
name: 'Sarif Industries',
|
||||||
|
description: '',
|
||||||
|
max_hosts: 0,
|
||||||
|
custom_virtualenv: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const auditorOrganization = {
|
||||||
|
id: 2,
|
||||||
|
type: 'organization',
|
||||||
|
url: '/api/v2/organizations/2/',
|
||||||
|
related: {
|
||||||
|
instance_groups: '/api/v2/organizations/2/instance_groups/',
|
||||||
|
object_roles: '/api/v2/organizations/2/object_roles/',
|
||||||
|
access_list: '/api/v2/organizations/2/access_list/',
|
||||||
|
},
|
||||||
|
summary_fields: {
|
||||||
|
created_by: {
|
||||||
|
id: 2,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
modified_by: {
|
||||||
|
id: 2,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2015-07-07T17:21:26.429745Z',
|
||||||
|
modified: '2017-09-05T19:23:15.418808Z',
|
||||||
|
name: 'Autobots',
|
||||||
|
description: '',
|
||||||
|
max_hosts: 0,
|
||||||
|
custom_virtualenv: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationAdminOrganization = {
|
||||||
|
id: 3,
|
||||||
|
type: 'organization',
|
||||||
|
url: '/api/v2/organizations/3/',
|
||||||
|
related: {
|
||||||
|
instance_groups: '/api/v2/organizations/3/instance_groups/',
|
||||||
|
object_roles: '/api/v2/organizations/3/object_roles/',
|
||||||
|
access_list: '/api/v2/organizations/3/access_list/',
|
||||||
|
},
|
||||||
|
summary_fields: {
|
||||||
|
created_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
modified_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: 'Super',
|
||||||
|
last_name: 'User'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2015-07-07T17:21:26.429745Z',
|
||||||
|
modified: '2017-09-05T19:23:15.418808Z',
|
||||||
|
name: 'Decepticons',
|
||||||
|
description: '',
|
||||||
|
max_hosts: 0,
|
||||||
|
custom_virtualenv: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const allOrganizations = [
|
||||||
|
adminOrganization,
|
||||||
|
auditorOrganization,
|
||||||
|
notificationAdminOrganization
|
||||||
|
];
|
||||||
|
|
||||||
|
async function getOrganizations (params) {
|
||||||
|
let results = allOrganizations;
|
||||||
|
if (params && params.role_level) {
|
||||||
|
if (params.role_level === 'admin_role') {
|
||||||
|
results = [adminOrganization];
|
||||||
|
}
|
||||||
|
if (params.role_level === 'auditor_role') {
|
||||||
|
results = [auditorOrganization];
|
||||||
|
}
|
||||||
|
if (params.role_level === 'notification_admin_role') {
|
||||||
|
results = [notificationAdminOrganization];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
count: results.length,
|
||||||
|
next: null,
|
||||||
|
previous: null,
|
||||||
|
data: { results }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
describe.only('<Organization />', () => {
|
||||||
await sleep(0);
|
test('initially renders succesfully', () => {
|
||||||
wrapper.update();
|
OrganizationsAPI.readDetail.mockResolvedValue(mockDetails);
|
||||||
expect(wrapper.find('.pf-c-tabs__item').length).toBe(3);
|
OrganizationsAPI.read.mockImplementation(getOrganizations);
|
||||||
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(0);
|
mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
|
||||||
wrapper.find('Organization').setState({
|
|
||||||
isNotifAdmin: true
|
|
||||||
});
|
});
|
||||||
expect(wrapper.find('.pf-c-tabs__item').length).toBe(4);
|
|
||||||
expect(wrapper.find('button.pf-c-tabs__button[children="Notifications"]').length).toBe(1);
|
test('notifications tab shown for admins', async (done) => {
|
||||||
|
OrganizationsAPI.readDetail.mockResolvedValue(mockDetails);
|
||||||
|
OrganizationsAPI.read.mockImplementation(getOrganizations);
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
|
||||||
|
const tabs = await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 4);
|
||||||
|
expect(tabs.last().text()).toEqual('Notifications');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notifications tab hidden with reduced permissions', async (done) => {
|
||||||
|
OrganizationsAPI.readDetail.mockResolvedValue(mockDetails);
|
||||||
|
OrganizationsAPI.read.mockResolvedValue(mockNoResults);
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
|
||||||
|
const tabs = await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 3);
|
||||||
|
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '../../../../enzymeHelpers';
|
||||||
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
|
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
|
||||||
import { sleep } from '../../../../testUtils';
|
import { sleep } from '../../../../testUtils';
|
||||||
|
|
||||||
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../../src/api';
|
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../../src/api';
|
||||||
|
|
||||||
jest.mock('../../../../../src/api');
|
jest.mock('../../../../../src/api');
|
||||||
|
|
||||||
describe('<OrganizationAccess />', () => {
|
describe('<OrganizationAccess />', () => {
|
||||||
const network = {};
|
|
||||||
const organization = {
|
const organization = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
@ -64,7 +62,9 @@ describe('<OrganizationAccess />', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
OrganizationsAPI.readAccessList.mockReturnValue({ data });
|
OrganizationsAPI.readAccessList.mockResolvedValue({ data });
|
||||||
|
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||||
|
UsersAPI.disassociateRole.mockResolvedValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -72,31 +72,21 @@ describe('<OrganizationAccess />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
<OrganizationAccess organization={organization} />,
|
|
||||||
{ context: { network } }
|
|
||||||
);
|
|
||||||
expect(wrapper.find('OrganizationAccess')).toMatchSnapshot();
|
expect(wrapper.find('OrganizationAccess')).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fetch and display access records on mount', async () => {
|
test('should fetch and display access records on mount', async (done) => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
<OrganizationAccess organization={organization} />,
|
await waitForElement(wrapper, 'OrganizationAccessItem', el => el.length === 2);
|
||||||
{ context: { network } }
|
|
||||||
);
|
|
||||||
await sleep(0);
|
|
||||||
wrapper.update();
|
|
||||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalled();
|
|
||||||
expect(wrapper.find('OrganizationAccess').state('isInitialized')).toBe(true);
|
|
||||||
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(data.results);
|
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(data.results);
|
||||||
expect(wrapper.find('OrganizationAccessItem')).toHaveLength(2);
|
expect(wrapper.find('OrganizationAccess').state('contentLoading')).toBe(false);
|
||||||
|
expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(false);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should open confirmation dialog when deleting role', async () => {
|
test('should open confirmation dialog when deleting role', async (done) => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
<OrganizationAccess organization={organization} />,
|
|
||||||
{ context: { network } }
|
|
||||||
);
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
@ -105,18 +95,16 @@ describe('<OrganizationAccess />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
const component = wrapper.find('OrganizationAccess');
|
const component = wrapper.find('OrganizationAccess');
|
||||||
expect(component.state('roleToDelete'))
|
expect(component.state('deletionRole'))
|
||||||
.toEqual(data.results[0].summary_fields.direct_access[0].role);
|
.toEqual(data.results[0].summary_fields.direct_access[0].role);
|
||||||
expect(component.state('roleToDeleteAccessRecord'))
|
expect(component.state('deletionRecord'))
|
||||||
.toEqual(data.results[0]);
|
.toEqual(data.results[0]);
|
||||||
expect(component.find('DeleteRoleConfirmationModal')).toHaveLength(1);
|
expect(component.find('DeleteRoleConfirmationModal')).toHaveLength(1);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close dialog when cancel button clicked', async () => {
|
it('should close dialog when cancel button clicked', async (done) => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
<OrganizationAccess organization={organization} />,
|
|
||||||
{ context: { network } }
|
|
||||||
);
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
const button = wrapper.find('ChipButton').at(0);
|
const button = wrapper.find('ChipButton').at(0);
|
||||||
@ -125,55 +113,50 @@ describe('<OrganizationAccess />', () => {
|
|||||||
|
|
||||||
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
|
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
|
||||||
const component = wrapper.find('OrganizationAccess');
|
const component = wrapper.find('OrganizationAccess');
|
||||||
expect(component.state('roleToDelete')).toBeNull();
|
expect(component.state('deletionRole')).toBeNull();
|
||||||
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
|
expect(component.state('deletionRecord')).toBeNull();
|
||||||
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
|
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
|
||||||
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
|
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete user role', async () => {
|
it('should delete user role', async (done) => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
<OrganizationAccess organization={organization} />,
|
const button = await waitForElement(wrapper, 'ChipButton', el => el.length === 2);
|
||||||
{ context: { network } }
|
button.at(0).prop('onClick')();
|
||||||
);
|
|
||||||
|
const confirmation = await waitForElement(wrapper, 'DeleteRoleConfirmationModal');
|
||||||
|
confirmation.prop('onConfirm')();
|
||||||
|
await waitForElement(wrapper, 'DeleteRoleConfirmationModal', el => el.length === 0);
|
||||||
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
const button = wrapper.find('ChipButton').at(0);
|
|
||||||
button.prop('onClick')();
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
|
|
||||||
await sleep(0);
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
const component = wrapper.find('OrganizationAccess');
|
const component = wrapper.find('OrganizationAccess');
|
||||||
expect(component.state('roleToDelete')).toBeNull();
|
expect(component.state('deletionRole')).toBeNull();
|
||||||
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
|
expect(component.state('deletionRecord')).toBeNull();
|
||||||
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
|
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
|
||||||
expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1);
|
expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1);
|
||||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete team role', async () => {
|
it('should delete team role', async (done) => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
<OrganizationAccess organization={organization} />,
|
const button = await waitForElement(wrapper, 'ChipButton', el => el.length === 2);
|
||||||
{ context: { network } }
|
button.at(1).prop('onClick')();
|
||||||
);
|
|
||||||
|
const confirmation = await waitForElement(wrapper, 'DeleteRoleConfirmationModal');
|
||||||
|
confirmation.prop('onConfirm')();
|
||||||
|
await waitForElement(wrapper, 'DeleteRoleConfirmationModal', el => el.length === 0);
|
||||||
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
const button = wrapper.find('ChipButton').at(1);
|
|
||||||
button.prop('onClick')();
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
|
|
||||||
await sleep(0);
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
const component = wrapper.find('OrganizationAccess');
|
const component = wrapper.find('OrganizationAccess');
|
||||||
expect(component.state('roleToDelete')).toBeNull();
|
expect(component.state('deletionRole')).toBeNull();
|
||||||
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
|
expect(component.state('deletionRecord')).toBeNull();
|
||||||
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);
|
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);
|
||||||
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
|
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
|
||||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '../../../../enzymeHelpers';
|
||||||
import OrganizationDetail from '../../../../../src/pages/Organizations/screens/Organization/OrganizationDetail';
|
import OrganizationDetail from '../../../../../src/pages/Organizations/screens/Organization/OrganizationDetail';
|
||||||
import { OrganizationsAPI } from '../../../../../src/api';
|
import { OrganizationsAPI } from '../../../../../src/api';
|
||||||
|
|
||||||
jest.mock('../../../../../src/api');
|
jest.mock('../../../../../src/api');
|
||||||
|
|
||||||
describe('<OrganizationDetail />', () => {
|
describe('<OrganizationDetail />', () => {
|
||||||
const mockDetails = {
|
const mockOrganization = {
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
description: 'Bar',
|
description: 'Bar',
|
||||||
custom_virtualenv: 'Fizz',
|
custom_virtualenv: 'Fizz',
|
||||||
@ -19,107 +19,75 @@ describe('<OrganizationDetail />', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const mockInstanceGroups = {
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ name: 'One', id: 1 },
|
||||||
|
{ name: 'Two', id: 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
OrganizationsAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
||||||
<OrganizationDetail
|
|
||||||
organization={mockDetails}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should request instance groups from api', () => {
|
test('should request instance groups from api', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
||||||
<OrganizationDetail
|
|
||||||
organization={mockDetails}
|
|
||||||
/>, { context: {
|
|
||||||
network: { handleHttpError: () => {} }
|
|
||||||
} }
|
|
||||||
).find('OrganizationDetail');
|
|
||||||
|
|
||||||
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle setting instance groups to state', async () => {
|
test('should handle setting instance groups to state', async (done) => {
|
||||||
const mockInstanceGroups = [
|
|
||||||
{ name: 'One', id: 1 },
|
|
||||||
{ name: 'Two', id: 2 }
|
|
||||||
];
|
|
||||||
OrganizationsAPI.readInstanceGroups.mockResolvedValue({
|
|
||||||
data: { results: mockInstanceGroups }
|
|
||||||
});
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationDetail
|
<OrganizationDetail organization={mockOrganization} />
|
||||||
organization={mockDetails}
|
|
||||||
/>, { context: {
|
|
||||||
network: { handleHttpError: () => {} }
|
|
||||||
} }
|
|
||||||
).find('OrganizationDetail');
|
|
||||||
|
|
||||||
await OrganizationsAPI.readInstanceGroups();
|
|
||||||
expect(wrapper.state().instanceGroups).toEqual(mockInstanceGroups);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render Details', async () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<OrganizationDetail
|
|
||||||
organization={mockDetails}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
const component = await waitForElement(wrapper, 'OrganizationDetail');
|
||||||
const detailWrapper = wrapper.find('Detail');
|
expect(component.state().instanceGroups).toEqual(mockInstanceGroups.data.results);
|
||||||
expect(detailWrapper.length).toBe(6);
|
done();
|
||||||
|
|
||||||
const nameDetail = detailWrapper.findWhere(node => node.props().label === 'Name');
|
|
||||||
const descriptionDetail = detailWrapper.findWhere(node => node.props().label === 'Description');
|
|
||||||
const custom_virtualenvDetail = detailWrapper.findWhere(node => node.props().label === 'Ansible Environment');
|
|
||||||
const max_hostsDetail = detailWrapper.findWhere(node => node.props().label === 'Max Hosts');
|
|
||||||
const createdDetail = detailWrapper.findWhere(node => node.props().label === 'Created');
|
|
||||||
const modifiedDetail = detailWrapper.findWhere(node => node.props().label === 'Last Modified');
|
|
||||||
expect(nameDetail.find('dt').text()).toBe('Name');
|
|
||||||
expect(nameDetail.find('dd').text()).toBe('Foo');
|
|
||||||
|
|
||||||
expect(descriptionDetail.find('dt').text()).toBe('Description');
|
|
||||||
expect(descriptionDetail.find('dd').text()).toBe('Bar');
|
|
||||||
|
|
||||||
expect(custom_virtualenvDetail.find('dt').text()).toBe('Ansible Environment');
|
|
||||||
expect(custom_virtualenvDetail.find('dd').text()).toBe('Fizz');
|
|
||||||
|
|
||||||
expect(createdDetail.find('dt').text()).toBe('Created');
|
|
||||||
expect(createdDetail.find('dd').text()).toBe('Bat');
|
|
||||||
|
|
||||||
expect(modifiedDetail.find('dt').text()).toBe('Last Modified');
|
|
||||||
expect(modifiedDetail.find('dd').text()).toBe('Boo');
|
|
||||||
|
|
||||||
expect(max_hostsDetail.find('dt').text()).toBe('Max Hosts');
|
|
||||||
expect(max_hostsDetail.find('dd').text()).toBe('0');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show edit button for users with edit permission', () => {
|
test('should render Details', async (done) => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
||||||
<OrganizationDetail
|
const testParams = [
|
||||||
organization={mockDetails}
|
{ label: 'Name', value: 'Foo' },
|
||||||
/>
|
{ label: 'Description', value: 'Bar' },
|
||||||
).find('OrganizationDetail');
|
{ label: 'Ansible Environment', value: 'Fizz' },
|
||||||
const editButton = wrapper.find('Button');
|
{ label: 'Created', value: 'Bat' },
|
||||||
expect((editButton).prop('to')).toBe('/organizations/undefined/edit');
|
{ label: 'Last Modified', value: 'Boo' },
|
||||||
|
{ label: 'Max Hosts', value: '0' },
|
||||||
|
];
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const { label, value } of testParams) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const detail = await waitForElement(wrapper, `Detail[label="${label}"]`);
|
||||||
|
expect(detail.find('dt').text()).toBe(label);
|
||||||
|
expect(detail.find('dd').text()).toBe(value);
|
||||||
|
}
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should hide edit button for users without edit permission', () => {
|
test('should show edit button for users with edit permission', async (done) => {
|
||||||
const readOnlyOrg = { ...mockDetails };
|
const wrapper = mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
||||||
|
const editButton = await waitForElement(wrapper, 'OrganizationDetail Button');
|
||||||
|
expect(editButton.text()).toEqual('Edit');
|
||||||
|
expect(editButton.prop('to')).toBe('/organizations/undefined/edit');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button for users without edit permission', async (done) => {
|
||||||
|
const readOnlyOrg = { ...mockOrganization };
|
||||||
readOnlyOrg.summary_fields.user_capabilities.edit = false;
|
readOnlyOrg.summary_fields.user_capabilities.edit = false;
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<OrganizationDetail organization={readOnlyOrg} />);
|
||||||
<OrganizationDetail
|
await waitForElement(wrapper, 'OrganizationDetail');
|
||||||
organization={readOnlyOrg}
|
expect(wrapper.find('OrganizationDetail Button').length).toBe(0);
|
||||||
/>
|
done();
|
||||||
).find('OrganizationDetail');
|
|
||||||
|
|
||||||
const editLink = wrapper
|
|
||||||
.findWhere(node => node.props().to === '/organizations/undefined/edit');
|
|
||||||
expect(editLink.length).toBe(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -37,7 +37,6 @@ describe('<OrganizationEdit />', () => {
|
|||||||
organization={mockData}
|
organization={mockData}
|
||||||
/>, { context: { network: {
|
/>, { context: { network: {
|
||||||
api,
|
api,
|
||||||
handleHttpError: () => {}
|
|
||||||
} } }
|
} } }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -57,7 +56,6 @@ describe('<OrganizationEdit />', () => {
|
|||||||
organization={mockData}
|
organization={mockData}
|
||||||
/>, { context: { network: {
|
/>, { context: { network: {
|
||||||
api,
|
api,
|
||||||
handleHttpError: () => {}
|
|
||||||
} } }
|
} } }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -84,7 +82,6 @@ describe('<OrganizationEdit />', () => {
|
|||||||
/>, { context: {
|
/>, { context: {
|
||||||
network: {
|
network: {
|
||||||
api: { api },
|
api: { api },
|
||||||
handleHttpError: () => {}
|
|
||||||
},
|
},
|
||||||
router: { history }
|
router: { history }
|
||||||
} }
|
} }
|
||||||
|
@ -8,7 +8,6 @@ jest.mock('../../../../../src/api');
|
|||||||
|
|
||||||
describe('<OrganizationNotifications />', () => {
|
describe('<OrganizationNotifications />', () => {
|
||||||
let data;
|
let data;
|
||||||
const network = {};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
data = {
|
data = {
|
||||||
@ -40,8 +39,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('initially renders succesfully', async () => {
|
test('initially renders succesfully', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications id={1} canToggleNotifications />,
|
<OrganizationNotifications id={1} canToggleNotifications />
|
||||||
{ context: { network } }
|
|
||||||
);
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@ -50,10 +48,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('should render list fetched of items', async () => {
|
test('should render list fetched of items', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications id={1} canToggleNotifications />,
|
<OrganizationNotifications id={1} canToggleNotifications />
|
||||||
{
|
|
||||||
context: { network }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@ -71,10 +66,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('should enable success notification', async () => {
|
test('should enable success notification', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications id={1} canToggleNotifications />,
|
<OrganizationNotifications id={1} canToggleNotifications />
|
||||||
{
|
|
||||||
context: { network }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@ -84,7 +76,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
).toEqual([1]);
|
).toEqual([1]);
|
||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items.at(1).find('Switch').at(0).prop('onChange')();
|
items.at(1).find('Switch').at(0).prop('onChange')();
|
||||||
expect(OrganizationsAPI.associateNotificationTemplatesSuccess).toHaveBeenCalled();
|
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 2, 'success', true);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(
|
expect(
|
||||||
@ -94,10 +86,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('should enable error notification', async () => {
|
test('should enable error notification', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications id={1} canToggleNotifications />,
|
<OrganizationNotifications id={1} canToggleNotifications />
|
||||||
{
|
|
||||||
context: { network }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@ -107,7 +96,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
).toEqual([2]);
|
).toEqual([2]);
|
||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items.at(0).find('Switch').at(1).prop('onChange')();
|
items.at(0).find('Switch').at(1).prop('onChange')();
|
||||||
expect(OrganizationsAPI.associateNotificationTemplatesError).toHaveBeenCalled();
|
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 1, 'error', true);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(
|
expect(
|
||||||
@ -117,10 +106,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('should disable success notification', async () => {
|
test('should disable success notification', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications id={1} canToggleNotifications />,
|
<OrganizationNotifications id={1} canToggleNotifications />
|
||||||
{
|
|
||||||
context: { network }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@ -130,7 +116,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
).toEqual([1]);
|
).toEqual([1]);
|
||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items.at(0).find('Switch').at(0).prop('onChange')();
|
items.at(0).find('Switch').at(0).prop('onChange')();
|
||||||
expect(OrganizationsAPI.disassociateNotificationTemplatesSuccess).toHaveBeenCalled();
|
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 1, 'success', false);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(
|
expect(
|
||||||
@ -140,10 +126,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('should disable error notification', async () => {
|
test('should disable error notification', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications id={1} canToggleNotifications />,
|
<OrganizationNotifications id={1} canToggleNotifications />
|
||||||
{
|
|
||||||
context: { network }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@ -153,7 +136,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
).toEqual([2]);
|
).toEqual([2]);
|
||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items.at(1).find('Switch').at(1).prop('onChange')();
|
items.at(1).find('Switch').at(1).prop('onChange')();
|
||||||
expect(OrganizationsAPI.disassociateNotificationTemplatesError).toHaveBeenCalled();
|
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 2, 'error', false);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(
|
expect(
|
||||||
|
@ -20,6 +20,7 @@ const listData = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('<OrganizationTeams />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
OrganizationsAPI.readTeams.mockResolvedValue(listData);
|
OrganizationsAPI.readTeams.mockResolvedValue(listData);
|
||||||
});
|
});
|
||||||
@ -28,14 +29,12 @@ afterEach(() => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<OrganizationTeams />', () => {
|
|
||||||
test('renders succesfully', () => {
|
test('renders succesfully', () => {
|
||||||
shallow(
|
shallow(
|
||||||
<_OrganizationTeams
|
<_OrganizationTeams
|
||||||
id={1}
|
id={1}
|
||||||
searchString=""
|
searchString=""
|
||||||
location={{ search: '', pathname: '/organizations/1/teams' }}
|
location={{ search: '', pathname: '/organizations/1/teams' }}
|
||||||
handleHttpError={() => {}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -45,9 +44,7 @@ describe('<OrganizationTeams />', () => {
|
|||||||
<OrganizationTeams
|
<OrganizationTeams
|
||||||
id={1}
|
id={1}
|
||||||
searchString=""
|
searchString=""
|
||||||
/>, { context: {
|
/>
|
||||||
network: {} }
|
|
||||||
}
|
|
||||||
).find('OrganizationTeams');
|
).find('OrganizationTeams');
|
||||||
expect(OrganizationsAPI.readTeams).toHaveBeenCalledWith(1, {
|
expect(OrganizationsAPI.readTeams).toHaveBeenCalledWith(1, {
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -61,9 +58,7 @@ describe('<OrganizationTeams />', () => {
|
|||||||
<OrganizationTeams
|
<OrganizationTeams
|
||||||
id={1}
|
id={1}
|
||||||
searchString=""
|
searchString=""
|
||||||
/>, { context: {
|
/>
|
||||||
network: { handleHttpError: () => {} } }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
||||||
<OrganizationAccess
|
<OrganizationAccess
|
||||||
handleHttpError={[Function]}
|
|
||||||
history={"/history/"}
|
history={"/history/"}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
location={
|
location={
|
||||||
@ -34,8 +33,228 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<WithI18n
|
||||||
|
contentError={false}
|
||||||
|
contentLoading={true}
|
||||||
|
itemCount={0}
|
||||||
|
itemName="role"
|
||||||
|
items={Array []}
|
||||||
|
qsConfig={
|
||||||
|
Object {
|
||||||
|
"defaultParams": Object {
|
||||||
|
"order_by": "first_name",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 5,
|
||||||
|
},
|
||||||
|
"integerFields": Array [
|
||||||
|
"page",
|
||||||
|
"page_size",
|
||||||
|
],
|
||||||
|
"namespace": "access",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderItem={[Function]}
|
||||||
|
renderToolbar={[Function]}
|
||||||
|
toolbarColumns={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "first_name",
|
||||||
|
"name": "Name",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "username",
|
||||||
|
"name": "Username",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "last_name",
|
||||||
|
"name": "Last Name",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<I18n
|
||||||
|
update={true}
|
||||||
|
withHash={true}
|
||||||
|
>
|
||||||
|
<withRouter(PaginatedDataList)
|
||||||
|
contentError={false}
|
||||||
|
contentLoading={true}
|
||||||
|
i18n={"/i18n/"}
|
||||||
|
itemCount={0}
|
||||||
|
itemName="role"
|
||||||
|
items={Array []}
|
||||||
|
qsConfig={
|
||||||
|
Object {
|
||||||
|
"defaultParams": Object {
|
||||||
|
"order_by": "first_name",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 5,
|
||||||
|
},
|
||||||
|
"integerFields": Array [
|
||||||
|
"page",
|
||||||
|
"page_size",
|
||||||
|
],
|
||||||
|
"namespace": "access",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderItem={[Function]}
|
||||||
|
renderToolbar={[Function]}
|
||||||
|
toolbarColumns={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "first_name",
|
||||||
|
"name": "Name",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "username",
|
||||||
|
"name": "Username",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "last_name",
|
||||||
|
"name": "Last Name",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route>
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={false}
|
||||||
|
contentLoading={true}
|
||||||
|
history={"/history/"}
|
||||||
|
i18n={"/i18n/"}
|
||||||
|
itemCount={0}
|
||||||
|
itemName="role"
|
||||||
|
itemNamePlural=""
|
||||||
|
items={Array []}
|
||||||
|
location={
|
||||||
|
Object {
|
||||||
|
"hash": "",
|
||||||
|
"pathname": "",
|
||||||
|
"search": "",
|
||||||
|
"state": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match={
|
||||||
|
Object {
|
||||||
|
"isExact": false,
|
||||||
|
"params": Object {},
|
||||||
|
"path": "",
|
||||||
|
"url": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qsConfig={
|
||||||
|
Object {
|
||||||
|
"defaultParams": Object {
|
||||||
|
"order_by": "first_name",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 5,
|
||||||
|
},
|
||||||
|
"integerFields": Array [
|
||||||
|
"page",
|
||||||
|
"page_size",
|
||||||
|
],
|
||||||
|
"namespace": "access",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderItem={[Function]}
|
||||||
|
renderToolbar={[Function]}
|
||||||
|
showPageSizeOptions={true}
|
||||||
|
toolbarColumns={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "first_name",
|
||||||
|
"name": "Name",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "username",
|
||||||
|
"name": "Username",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"isSortable": true,
|
||||||
|
"key": "last_name",
|
||||||
|
"name": "Last Name",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WithI18n>
|
||||||
|
<I18n
|
||||||
|
update={true}
|
||||||
|
withHash={true}
|
||||||
|
>
|
||||||
|
<ContentLoading
|
||||||
|
i18n={"/i18n/"}
|
||||||
|
>
|
||||||
|
<EmptyState
|
||||||
|
className=""
|
||||||
|
variant="large"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="pf-c-empty-state pf-m-lg"
|
||||||
|
>
|
||||||
|
<EmptyStateBody
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="pf-c-empty-state__body"
|
||||||
|
>
|
||||||
Loading...
|
Loading...
|
||||||
|
</p>
|
||||||
|
</EmptyStateBody>
|
||||||
</div>
|
</div>
|
||||||
|
</EmptyState>
|
||||||
|
</ContentLoading>
|
||||||
|
</I18n>
|
||||||
|
</WithI18n>
|
||||||
|
</PaginatedDataList>
|
||||||
|
</Route>
|
||||||
|
</withRouter(PaginatedDataList)>
|
||||||
|
</I18n>
|
||||||
|
</WithI18n>
|
||||||
|
<_default
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Error!"
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
actions={Array []}
|
||||||
|
ariaDescribedById=""
|
||||||
|
className="awx-c-modal at-c-alertModal at-c-alertModal--danger"
|
||||||
|
hideTitle={false}
|
||||||
|
isLarge={false}
|
||||||
|
isOpen={false}
|
||||||
|
isSmall={false}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Error!"
|
||||||
|
width={null}
|
||||||
|
>
|
||||||
|
<Portal
|
||||||
|
containerInfo={<div />}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
actions={Array []}
|
||||||
|
ariaDescribedById=""
|
||||||
|
className="awx-c-modal at-c-alertModal at-c-alertModal--danger"
|
||||||
|
hideTitle={false}
|
||||||
|
id="pf-modal-0"
|
||||||
|
isLarge={false}
|
||||||
|
isOpen={false}
|
||||||
|
isSmall={false}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Error!"
|
||||||
|
width={null}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
</Modal>
|
||||||
|
</_default>
|
||||||
</OrganizationAccess>
|
</OrganizationAccess>
|
||||||
`;
|
`;
|
||||||
|
@ -2,39 +2,24 @@
|
|||||||
|
|
||||||
exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
||||||
<Wrap>
|
<Wrap>
|
||||||
<RootDialogProvider
|
<WithI18n
|
||||||
value={Object {}}
|
|
||||||
>
|
|
||||||
<Provider
|
|
||||||
value={
|
|
||||||
Object {
|
|
||||||
"handleHttpError": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Component
|
|
||||||
i18n={"/i18n/"}
|
|
||||||
value={"/config/"}
|
|
||||||
>
|
|
||||||
<Provider
|
|
||||||
handleHttpError={[Function]}
|
|
||||||
i18n={"/i18n/"}
|
|
||||||
value={"/config/"}
|
|
||||||
>
|
|
||||||
<Component
|
|
||||||
canToggleNotifications={true}
|
canToggleNotifications={true}
|
||||||
id={1}
|
id={1}
|
||||||
|
>
|
||||||
|
<I18n
|
||||||
|
update={true}
|
||||||
|
withHash={true}
|
||||||
>
|
>
|
||||||
<withRouter(OrganizationNotifications)
|
<withRouter(OrganizationNotifications)
|
||||||
canToggleNotifications={true}
|
canToggleNotifications={true}
|
||||||
handleHttpError={[Function]}
|
i18n={"/i18n/"}
|
||||||
id={1}
|
id={1}
|
||||||
>
|
>
|
||||||
<Route>
|
<Route>
|
||||||
<OrganizationNotifications
|
<OrganizationNotifications
|
||||||
canToggleNotifications={true}
|
canToggleNotifications={true}
|
||||||
handleHttpError={[Function]}
|
|
||||||
history={"/history/"}
|
history={"/history/"}
|
||||||
|
i18n={"/i18n/"}
|
||||||
id={1}
|
id={1}
|
||||||
location={
|
location={
|
||||||
Object {
|
Object {
|
||||||
@ -54,6 +39,8 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<WithI18n
|
<WithI18n
|
||||||
|
contentError={false}
|
||||||
|
contentLoading={false}
|
||||||
itemCount={2}
|
itemCount={2}
|
||||||
itemName="notification"
|
itemName="notification"
|
||||||
items={
|
items={
|
||||||
@ -114,6 +101,8 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
withHash={true}
|
withHash={true}
|
||||||
>
|
>
|
||||||
<withRouter(PaginatedDataList)
|
<withRouter(PaginatedDataList)
|
||||||
|
contentError={false}
|
||||||
|
contentLoading={false}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
itemCount={2}
|
itemCount={2}
|
||||||
itemName="notification"
|
itemName="notification"
|
||||||
@ -172,6 +161,8 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Route>
|
<Route>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
|
contentError={false}
|
||||||
|
contentLoading={false}
|
||||||
history={"/history/"}
|
history={"/history/"}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
itemCount={2}
|
itemCount={2}
|
||||||
@ -1605,11 +1596,11 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
</I18n>
|
</I18n>
|
||||||
</WithI18n>
|
</WithI18n>
|
||||||
<DataList
|
<DataList
|
||||||
aria-label="{0} List"
|
aria-label="{itemDisplayName} List"
|
||||||
className=""
|
className=""
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
aria-label="{0} List"
|
aria-label="{itemDisplayName} List"
|
||||||
className="pf-c-data-list"
|
className="pf-c-data-list"
|
||||||
role="list"
|
role="list"
|
||||||
>
|
>
|
||||||
@ -3303,13 +3294,47 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
</withRouter(PaginatedDataList)>
|
</withRouter(PaginatedDataList)>
|
||||||
</I18n>
|
</I18n>
|
||||||
</WithI18n>
|
</WithI18n>
|
||||||
|
<_default
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Error!"
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
actions={Array []}
|
||||||
|
ariaDescribedById=""
|
||||||
|
className="awx-c-modal at-c-alertModal at-c-alertModal--danger"
|
||||||
|
hideTitle={false}
|
||||||
|
isLarge={false}
|
||||||
|
isOpen={false}
|
||||||
|
isSmall={false}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Error!"
|
||||||
|
width={null}
|
||||||
|
>
|
||||||
|
<Portal
|
||||||
|
containerInfo={<div />}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
actions={Array []}
|
||||||
|
ariaDescribedById=""
|
||||||
|
className="awx-c-modal at-c-alertModal at-c-alertModal--danger"
|
||||||
|
hideTitle={false}
|
||||||
|
id="pf-modal-0"
|
||||||
|
isLarge={false}
|
||||||
|
isOpen={false}
|
||||||
|
isSmall={false}
|
||||||
|
onClose={[Function]}
|
||||||
|
title="Error!"
|
||||||
|
width={null}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
</Modal>
|
||||||
|
</_default>
|
||||||
</OrganizationNotifications>
|
</OrganizationNotifications>
|
||||||
</Route>
|
</Route>
|
||||||
</withRouter(OrganizationNotifications)>
|
</withRouter(OrganizationNotifications)>
|
||||||
</Component>
|
</I18n>
|
||||||
</Provider>
|
</WithI18n>
|
||||||
</Component>
|
|
||||||
</Provider>
|
|
||||||
</RootDialogProvider>
|
|
||||||
</Wrap>
|
</Wrap>
|
||||||
`;
|
`;
|
||||||
|
@ -1,27 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mountWithContexts } from '../../../enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '../../../enzymeHelpers';
|
||||||
|
|
||||||
import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
|
import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
|
||||||
import { OrganizationsAPI } from '../../../../src/api';
|
import { OrganizationsAPI } from '../../../../src/api';
|
||||||
|
|
||||||
jest.mock('../../../../src/api');
|
jest.mock('../../../../src/api');
|
||||||
|
|
||||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
describe('<OrganizationAdd />', () => {
|
describe('<OrganizationAdd />', () => {
|
||||||
let networkProviderValue;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
networkProviderValue = {
|
|
||||||
handleHttpError: () => {}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handleSubmit should post to api', () => {
|
test('handleSubmit should post to api', () => {
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
const wrapper = mountWithContexts(<OrganizationAdd />);
|
||||||
context: { network: networkProviderValue }
|
|
||||||
});
|
|
||||||
const updatedOrgData = {
|
const updatedOrgData = {
|
||||||
name: 'new name',
|
name: 'new name',
|
||||||
description: 'new description',
|
description: 'new description',
|
||||||
@ -35,9 +23,10 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const history = {
|
const history = {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
const wrapper = mountWithContexts(
|
||||||
context: { router: { history } }
|
<OrganizationAdd />,
|
||||||
});
|
{ context: { router: { history } } }
|
||||||
|
);
|
||||||
expect(history.push).not.toHaveBeenCalled();
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
|
||||||
expect(history.push).toHaveBeenCalledWith('/organizations');
|
expect(history.push).toHaveBeenCalledWith('/organizations');
|
||||||
@ -47,15 +36,16 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const history = {
|
const history = {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
const wrapper = mountWithContexts(
|
||||||
context: { router: { history } }
|
<OrganizationAdd />,
|
||||||
});
|
{ context: { router: { history } } }
|
||||||
|
);
|
||||||
expect(history.push).not.toHaveBeenCalled();
|
expect(history.push).not.toHaveBeenCalled();
|
||||||
wrapper.find('button[aria-label="Close"]').prop('onClick')();
|
wrapper.find('button[aria-label="Close"]').prop('onClick')();
|
||||||
expect(history.push).toHaveBeenCalledWith('/organizations');
|
expect(history.push).toHaveBeenCalledWith('/organizations');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('successful form submission should trigger redirect', async () => {
|
test('successful form submission should trigger redirect', async (done) => {
|
||||||
const history = {
|
const history = {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
};
|
};
|
||||||
@ -64,7 +54,7 @@ describe('<OrganizationAdd />', () => {
|
|||||||
description: 'new description',
|
description: 'new description',
|
||||||
custom_virtualenv: 'Buzz',
|
custom_virtualenv: 'Buzz',
|
||||||
};
|
};
|
||||||
OrganizationsAPI.create.mockReturnValueOnce({
|
OrganizationsAPI.create.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
id: 5,
|
id: 5,
|
||||||
related: {
|
related: {
|
||||||
@ -73,24 +63,23 @@ describe('<OrganizationAdd />', () => {
|
|||||||
...orgData,
|
...orgData,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
const wrapper = mountWithContexts(
|
||||||
context: { router: { history }, network: networkProviderValue }
|
<OrganizationAdd />,
|
||||||
});
|
{ context: { router: { history } } }
|
||||||
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [], []);
|
);
|
||||||
await sleep(0);
|
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
||||||
|
await wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
|
||||||
expect(history.push).toHaveBeenCalledWith('/organizations/5');
|
expect(history.push).toHaveBeenCalledWith('/organizations/5');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSubmit should post instance groups', async () => {
|
test('handleSubmit should post instance groups', async (done) => {
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
|
||||||
context: { network: networkProviderValue }
|
|
||||||
});
|
|
||||||
const orgData = {
|
const orgData = {
|
||||||
name: 'new name',
|
name: 'new name',
|
||||||
description: 'new description',
|
description: 'new description',
|
||||||
custom_virtualenv: 'Buzz',
|
custom_virtualenv: 'Buzz',
|
||||||
};
|
};
|
||||||
OrganizationsAPI.create.mockReturnValueOnce({
|
OrganizationsAPI.create.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
id: 5,
|
id: 5,
|
||||||
related: {
|
related: {
|
||||||
@ -99,19 +88,22 @@ describe('<OrganizationAdd />', () => {
|
|||||||
...orgData,
|
...orgData,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
|
const wrapper = mountWithContexts(<OrganizationAdd />);
|
||||||
await sleep(0);
|
await waitForElement(wrapper, 'button[aria-label="Save"]');
|
||||||
|
await wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
|
||||||
expect(OrganizationsAPI.associateInstanceGroup)
|
expect(OrganizationsAPI.associateInstanceGroup)
|
||||||
.toHaveBeenCalledWith(5, 3);
|
.toHaveBeenCalledWith(5, 3);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('AnsibleSelect component renders if there are virtual environments', () => {
|
test('AnsibleSelect component renders if there are virtual environments', () => {
|
||||||
const config = {
|
const config = {
|
||||||
custom_virtualenvs: ['foo', 'bar'],
|
custom_virtualenvs: ['foo', 'bar'],
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
const wrapper = mountWithContexts(
|
||||||
context: { network: networkProviderValue, config }
|
<OrganizationAdd />,
|
||||||
}).find('AnsibleSelect');
|
{ context: { config } }
|
||||||
|
).find('AnsibleSelect');
|
||||||
expect(wrapper.find('FormSelect')).toHaveLength(1);
|
expect(wrapper.find('FormSelect')).toHaveLength(1);
|
||||||
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
|
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
@ -120,9 +112,10 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const config = {
|
const config = {
|
||||||
custom_virtualenvs: [],
|
custom_virtualenvs: [],
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />, {
|
const wrapper = mountWithContexts(
|
||||||
context: { network: networkProviderValue, config }
|
<OrganizationAdd />,
|
||||||
}).find('AnsibleSelect');
|
{ context: { config } }
|
||||||
|
).find('AnsibleSelect');
|
||||||
expect(wrapper.find('FormSelect')).toHaveLength(0);
|
expect(wrapper.find('FormSelect')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -59,25 +59,13 @@ const mockAPIOrgsList = {
|
|||||||
|
|
||||||
describe('<OrganizationsList />', () => {
|
describe('<OrganizationsList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let api;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
api = {
|
|
||||||
getOrganizations: () => {},
|
|
||||||
destroyOrganization: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(<OrganizationsList />);
|
||||||
<OrganizationsList />
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Puts 1 selected Org in state when handleSelect is called.', () => {
|
test('Puts 1 selected Org in state when handleSelect is called.', () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<OrganizationsList />).find('OrganizationsList');
|
||||||
<OrganizationsList />
|
|
||||||
).find('OrganizationsList');
|
|
||||||
|
|
||||||
wrapper.setState({
|
wrapper.setState({
|
||||||
organizations: mockAPIOrgsList.data.results,
|
organizations: mockAPIOrgsList.data.results,
|
||||||
@ -91,9 +79,7 @@ describe('<OrganizationsList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Puts all Orgs in state when handleSelectAll is called.', () => {
|
test('Puts all Orgs in state when handleSelectAll is called.', () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<OrganizationsList />);
|
||||||
<OrganizationsList />
|
|
||||||
);
|
|
||||||
const list = wrapper.find('OrganizationsList');
|
const list = wrapper.find('OrganizationsList');
|
||||||
list.setState({
|
list.setState({
|
||||||
organizations: mockAPIOrgsList.data.results,
|
organizations: mockAPIOrgsList.data.results,
|
||||||
@ -108,16 +94,7 @@ describe('<OrganizationsList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('api is called to delete Orgs for each org in selected.', () => {
|
test('api is called to delete Orgs for each org in selected.', () => {
|
||||||
const fetchOrganizations = jest.fn(() => wrapper.find('OrganizationsList').setState({
|
wrapper = mountWithContexts(<OrganizationsList />);
|
||||||
organizations: []
|
|
||||||
}));
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<OrganizationsList
|
|
||||||
fetchOrganizations={fetchOrganizations}
|
|
||||||
/>, {
|
|
||||||
context: { network: { api } }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const component = wrapper.find('OrganizationsList');
|
const component = wrapper.find('OrganizationsList');
|
||||||
wrapper.find('OrganizationsList').setState({
|
wrapper.find('OrganizationsList').setState({
|
||||||
organizations: mockAPIOrgsList.data.results,
|
organizations: mockAPIOrgsList.data.results,
|
||||||
@ -130,14 +107,10 @@ describe('<OrganizationsList />', () => {
|
|||||||
expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(component.state('selected').length);
|
expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(component.state('selected').length);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('call fetchOrganizations after org(s) have been deleted', () => {
|
test('call loadOrganizations after org(s) have been deleted', () => {
|
||||||
const fetchOrgs = jest.spyOn(_OrganizationsList.prototype, 'fetchOrganizations');
|
const fetchOrgs = jest.spyOn(_OrganizationsList.prototype, 'loadOrganizations');
|
||||||
const event = { preventDefault: () => { } };
|
const event = { preventDefault: () => { } };
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<OrganizationsList />);
|
||||||
<OrganizationsList />, {
|
|
||||||
context: { network: { api } }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
wrapper.find('OrganizationsList').setState({
|
wrapper.find('OrganizationsList').setState({
|
||||||
organizations: mockAPIOrgsList.data.results,
|
organizations: mockAPIOrgsList.data.results,
|
||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
@ -153,13 +126,9 @@ describe('<OrganizationsList />', () => {
|
|||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['organizations?order_by=name&page=1&page_size=5'],
|
initialEntries: ['organizations?order_by=name&page=1&page_size=5'],
|
||||||
});
|
});
|
||||||
const handleError = jest.fn();
|
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<OrganizationsList />, {
|
<OrganizationsList />,
|
||||||
context: {
|
{ context: { router: { history } } }
|
||||||
router: { history }, network: { api, handleHttpError: handleError }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await wrapper.setState({
|
await wrapper.setState({
|
||||||
organizations: mockAPIOrgsList.data.results,
|
organizations: mockAPIOrgsList.data.results,
|
||||||
@ -173,6 +142,5 @@ describe('<OrganizationsList />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
const component = wrapper.find('OrganizationsList');
|
const component = wrapper.find('OrganizationsList');
|
||||||
component.instance().handleOrgDelete();
|
component.instance().handleOrgDelete();
|
||||||
expect(handleError).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '../../enzymeHelpers';
|
||||||
import TemplatesList, { _TemplatesList } from '../../../src/pages/Templates/TemplatesList';
|
import TemplatesList, { _TemplatesList } from '../../../src/pages/Templates/TemplatesList';
|
||||||
|
import { UnifiedJobTemplatesAPI } from '../../../src/api';
|
||||||
|
|
||||||
jest.mock('../../../src/api');
|
jest.mock('../../../src/api');
|
||||||
|
|
||||||
const setDefaultState = (templatesList) => {
|
const mockTemplates = [{
|
||||||
templatesList.setState({
|
|
||||||
itemCount: mockUnifiedJobTemplatesFromAPI.length,
|
|
||||||
isLoading: false,
|
|
||||||
isInitialized: true,
|
|
||||||
selected: [],
|
|
||||||
templates: mockUnifiedJobTemplatesFromAPI,
|
|
||||||
});
|
|
||||||
templatesList.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockUnifiedJobTemplatesFromAPI = [{
|
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Template 1',
|
name: 'Template 1',
|
||||||
url: '/templates/job_template/1',
|
url: '/templates/job_template/1',
|
||||||
@ -47,6 +37,19 @@ const mockUnifiedJobTemplatesFromAPI = [{
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
describe('<TemplatesList />', () => {
|
describe('<TemplatesList />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
UnifiedJobTemplatesAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: mockTemplates.length,
|
||||||
|
results: mockTemplates
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
<TemplatesList
|
<TemplatesList
|
||||||
@ -55,46 +58,33 @@ describe('<TemplatesList />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Templates are retrieved from the api and the components finishes loading', async (done) => {
|
test('Templates are retrieved from the api and the components finishes loading', async (done) => {
|
||||||
const readTemplates = jest.spyOn(_TemplatesList.prototype, 'readUnifiedJobTemplates');
|
const loadUnifiedJobTemplates = jest.spyOn(_TemplatesList.prototype, 'loadUnifiedJobTemplates');
|
||||||
|
const wrapper = mountWithContexts(<TemplatesList />);
|
||||||
const wrapper = mountWithContexts(<TemplatesList />).find('TemplatesList');
|
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === true);
|
||||||
|
expect(loadUnifiedJobTemplates).toHaveBeenCalled();
|
||||||
expect(wrapper.state('isLoading')).toBe(true);
|
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
|
||||||
await expect(readTemplates).toHaveBeenCalled();
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.state('isLoading')).toBe(false);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSelect is called when a template list item is selected', async () => {
|
test('handleSelect is called when a template list item is selected', async (done) => {
|
||||||
const handleSelect = jest.spyOn(_TemplatesList.prototype, 'handleSelect');
|
const handleSelect = jest.spyOn(_TemplatesList.prototype, 'handleSelect');
|
||||||
|
|
||||||
const wrapper = mountWithContexts(<TemplatesList />);
|
const wrapper = mountWithContexts(<TemplatesList />);
|
||||||
|
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
|
||||||
const templatesList = wrapper.find('TemplatesList');
|
|
||||||
setDefaultState(templatesList);
|
|
||||||
|
|
||||||
expect(templatesList.state('isLoading')).toBe(false);
|
|
||||||
wrapper.find('DataListCheck#select-jobTemplate-1').props().onChange();
|
wrapper.find('DataListCheck#select-jobTemplate-1').props().onChange();
|
||||||
expect(handleSelect).toBeCalled();
|
expect(handleSelect).toBeCalled();
|
||||||
templatesList.update();
|
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('selected').length === 1);
|
||||||
expect(templatesList.state('selected').length).toBe(1);
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSelectAll is called when a template list item is selected', async () => {
|
test('handleSelectAll is called when a template list item is selected', async (done) => {
|
||||||
const handleSelectAll = jest.spyOn(_TemplatesList.prototype, 'handleSelectAll');
|
const handleSelectAll = jest.spyOn(_TemplatesList.prototype, 'handleSelectAll');
|
||||||
|
|
||||||
const wrapper = mountWithContexts(<TemplatesList />);
|
const wrapper = mountWithContexts(<TemplatesList />);
|
||||||
|
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
|
||||||
const templatesList = wrapper.find('TemplatesList');
|
|
||||||
setDefaultState(templatesList);
|
|
||||||
|
|
||||||
expect(templatesList.state('isLoading')).toBe(false);
|
|
||||||
wrapper.find('Checkbox#select-all').props().onChange(true);
|
wrapper.find('Checkbox#select-all').props().onChange(true);
|
||||||
expect(handleSelectAll).toBeCalled();
|
expect(handleSelectAll).toBeCalled();
|
||||||
wrapper.update();
|
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('selected').length === 3);
|
||||||
expect(templatesList.state('selected').length).toEqual(templatesList.state('templates')
|
done();
|
||||||
.length);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
155
src/App.jsx
155
src/App.jsx
@ -6,19 +6,16 @@ import {
|
|||||||
Page,
|
Page,
|
||||||
PageHeader as PFPageHeader,
|
PageHeader as PFPageHeader,
|
||||||
PageSidebar,
|
PageSidebar,
|
||||||
Button
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
|
||||||
import { RootDialog } from './contexts/RootDialog';
|
import { ConfigAPI, MeAPI, RootAPI } from './api';
|
||||||
import { withNetwork } from './contexts/Network';
|
import { ConfigProvider } from './contexts/Config';
|
||||||
import { Config } from './contexts/Config';
|
|
||||||
import { RootAPI } from './api';
|
|
||||||
|
|
||||||
import AlertModal from './components/AlertModal';
|
|
||||||
import About from './components/About';
|
import About from './components/About';
|
||||||
|
import AlertModal from './components/AlertModal';
|
||||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||||
import BrandLogo from './components/BrandLogo';
|
import BrandLogo from './components/BrandLogo';
|
||||||
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||||
@ -46,92 +43,95 @@ class App extends Component {
|
|||||||
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
ansible_version: null,
|
||||||
|
custom_virtualenvs: null,
|
||||||
|
me: null,
|
||||||
|
version: null,
|
||||||
isAboutModalOpen: false,
|
isAboutModalOpen: false,
|
||||||
isNavOpen
|
isNavOpen,
|
||||||
|
configError: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onLogout = this.onLogout.bind(this);
|
this.handleLogout = this.handleLogout.bind(this);
|
||||||
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
this.handleAboutClose = this.handleAboutClose.bind(this);
|
||||||
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
this.handleAboutOpen = this.handleAboutOpen.bind(this);
|
||||||
this.onNavToggle = this.onNavToggle.bind(this);
|
this.handleNavToggle = this.handleNavToggle.bind(this);
|
||||||
|
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onLogout () {
|
async componentDidMount () {
|
||||||
const { handleHttpError } = this.props;
|
await this.loadConfig();
|
||||||
try {
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
async handleLogout () {
|
||||||
await RootAPI.logout();
|
await RootAPI.logout();
|
||||||
window.location.replace('/#/login');
|
window.location.replace('/#/login');
|
||||||
} catch (err) {
|
|
||||||
handleHttpError(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAboutModalOpen () {
|
handleAboutOpen () {
|
||||||
this.setState({ isAboutModalOpen: true });
|
this.setState({ isAboutModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onAboutModalClose () {
|
handleAboutClose () {
|
||||||
this.setState({ isAboutModalOpen: false });
|
this.setState({ isAboutModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onNavToggle () {
|
handleNavToggle () {
|
||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleConfigErrorClose () {
|
||||||
|
this.setState({ configError: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfig () {
|
||||||
|
try {
|
||||||
|
const [configRes, meRes] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
||||||
|
const { data: { ansible_version, custom_virtualenvs, version } } = configRes;
|
||||||
|
const { data: { results: [me] } } = meRes;
|
||||||
|
|
||||||
|
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ configError: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isAboutModalOpen, isNavOpen } = this.state;
|
const {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
isAboutModalOpen,
|
||||||
|
isNavOpen,
|
||||||
|
me,
|
||||||
|
version,
|
||||||
|
configError,
|
||||||
|
} = this.state;
|
||||||
|
const {
|
||||||
|
i18n,
|
||||||
|
render = () => {},
|
||||||
|
routeGroups = [],
|
||||||
|
navLabel = '',
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const { render, routeGroups = [], navLabel = '', i18n } = this.props;
|
const header = (
|
||||||
|
|
||||||
return (
|
|
||||||
<Config>
|
|
||||||
{({ ansible_version, version, me }) => (
|
|
||||||
<RootDialog>
|
|
||||||
{({
|
|
||||||
title,
|
|
||||||
bodyText,
|
|
||||||
variant = 'info',
|
|
||||||
clearRootDialogMessage
|
|
||||||
}) => (
|
|
||||||
<Fragment>
|
|
||||||
{(title || bodyText) && (
|
|
||||||
<AlertModal
|
|
||||||
variant={variant}
|
|
||||||
isOpen={!!(title || bodyText)}
|
|
||||||
onClose={clearRootDialogMessage}
|
|
||||||
title={title}
|
|
||||||
actions={[
|
|
||||||
<Button
|
|
||||||
key="close"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={clearRootDialogMessage}
|
|
||||||
>
|
|
||||||
{i18n._(t`Close`)}
|
|
||||||
</Button>
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{bodyText}
|
|
||||||
</AlertModal>
|
|
||||||
)}
|
|
||||||
<Page
|
|
||||||
usecondensed="True"
|
|
||||||
header={(
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
showNavToggle
|
showNavToggle
|
||||||
onNavToggle={this.onNavToggle}
|
onNavToggle={this.handleNavToggle}
|
||||||
logo={<BrandLogo />}
|
logo={<BrandLogo />}
|
||||||
logoProps={{ href: '/' }}
|
logoProps={{ href: '/' }}
|
||||||
toolbar={(
|
toolbar={(
|
||||||
<PageHeaderToolbar
|
<PageHeaderToolbar
|
||||||
loggedInUser={me}
|
loggedInUser={me}
|
||||||
isAboutDisabled={!version}
|
isAboutDisabled={!version}
|
||||||
onAboutClick={this.onAboutModalOpen}
|
onAboutClick={this.handleAboutOpen}
|
||||||
onLogoutClick={this.onLogout}
|
onLogoutClick={this.handleLogout}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
sidebar={(
|
|
||||||
|
const sidebar = (
|
||||||
<PageSidebar
|
<PageSidebar
|
||||||
isNavOpen={isNavOpen}
|
isNavOpen={isNavOpen}
|
||||||
nav={(
|
nav={(
|
||||||
@ -151,24 +151,37 @@ class App extends Component {
|
|||||||
</Nav>
|
</Nav>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Page
|
||||||
|
usecondensed="True"
|
||||||
|
header={header}
|
||||||
|
sidebar={sidebar}
|
||||||
>
|
>
|
||||||
{render && render({ routeGroups })}
|
<ConfigProvider value={{ ansible_version, custom_virtualenvs, me, version }}>
|
||||||
|
{render({ routeGroups })}
|
||||||
|
</ConfigProvider>
|
||||||
</Page>
|
</Page>
|
||||||
<About
|
<About
|
||||||
ansible_version={ansible_version}
|
ansible_version={ansible_version}
|
||||||
version={version}
|
version={version}
|
||||||
isOpen={isAboutModalOpen}
|
isOpen={isAboutModalOpen}
|
||||||
onClose={this.onAboutModalClose}
|
onClose={this.handleAboutClose}
|
||||||
/>
|
/>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={configError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={this.handleConfigErrorClose}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to retrieve configuration.`)}
|
||||||
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
|
||||||
</RootDialog>
|
|
||||||
)}
|
|
||||||
</Config>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { App as _App };
|
export { App as _App };
|
||||||
export default withI18n()(withNetwork(App));
|
export default withI18n()(App);
|
||||||
|
@ -7,10 +7,6 @@ import {
|
|||||||
HashRouter
|
HashRouter
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { NetworkProvider } from './contexts/Network';
|
|
||||||
import { RootDialogProvider } from './contexts/RootDialog';
|
|
||||||
import { ConfigProvider } from './contexts/Config';
|
|
||||||
|
|
||||||
import ja from '../build/locales/ja/messages';
|
import ja from '../build/locales/ja/messages';
|
||||||
import en from '../build/locales/en/messages';
|
import en from '../build/locales/en/messages';
|
||||||
|
|
||||||
@ -34,13 +30,7 @@ class RootProvider extends Component {
|
|||||||
language={language}
|
language={language}
|
||||||
catalogs={catalogs}
|
catalogs={catalogs}
|
||||||
>
|
>
|
||||||
<RootDialogProvider>
|
|
||||||
<NetworkProvider>
|
|
||||||
<ConfigProvider>
|
|
||||||
{children}
|
{children}
|
||||||
</ConfigProvider>
|
|
||||||
</NetworkProvider>
|
|
||||||
</RootDialogProvider>
|
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
);
|
);
|
||||||
|
@ -26,6 +26,36 @@ const NotificationsMixin = (parent) => class extends parent {
|
|||||||
disassociateNotificationTemplatesError (resourceId, notificationId) {
|
disassociateNotificationTemplatesError (resourceId, notificationId) {
|
||||||
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true });
|
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a helper method meant to simplify setting the "on" or "off" status of
|
||||||
|
* a related notification.
|
||||||
|
*
|
||||||
|
* @param[resourceId] - id of the base resource
|
||||||
|
* @param[notificationId] - id of the notification
|
||||||
|
* @param[notificationType] - the type of notification, options are "success" and "error"
|
||||||
|
* @param[associationState] - Boolean for associating or disassociating, options are true or false
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
updateNotificationTemplateAssociation (resourceId, notificationId, notificationType, associationState) {
|
||||||
|
if (notificationType === 'success' && associationState === true) {
|
||||||
|
return this.associateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationType === 'success' && associationState === false) {
|
||||||
|
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationType === 'error' && associationState === true) {
|
||||||
|
return this.associateNotificationTemplatesError(resourceId, notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationType === 'error' && associationState === false) {
|
||||||
|
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotificationsMixin;
|
export default NotificationsMixin;
|
||||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Wizard } from '@patternfly/react-core';
|
import { Wizard } from '@patternfly/react-core';
|
||||||
import { withNetwork } from '../../contexts/Network';
|
|
||||||
import SelectResourceStep from './SelectResourceStep';
|
import SelectResourceStep from './SelectResourceStep';
|
||||||
import SelectRoleStep from './SelectRoleStep';
|
import SelectRoleStep from './SelectRoleStep';
|
||||||
import SelectableCard from './SelectableCard';
|
import SelectableCard from './SelectableCard';
|
||||||
@ -245,4 +244,4 @@ AddResourceRole.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { AddResourceRole as _AddResourceRole };
|
export { AddResourceRole as _AddResourceRole };
|
||||||
export default withI18n()(withNetwork(AddResourceRole));
|
export default withI18n()(AddResourceRole);
|
||||||
|
25
src/components/ContentEmpty.jsx
Normal file
25
src/components/ContentEmpty.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateIcon,
|
||||||
|
EmptyStateBody
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
|
||||||
|
<EmptyState>
|
||||||
|
<EmptyStateIcon icon={CubesIcon} />
|
||||||
|
<Title size="lg">
|
||||||
|
{title || i18n._(t`No items found.`)}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{message}
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { ContentEmpty as _ContentEmpty };
|
||||||
|
export default withI18n()(ContentEmpty);
|
26
src/components/ContentError.jsx
Normal file
26
src/components/ContentError.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import {
|
||||||
|
Title,
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateIcon,
|
||||||
|
EmptyStateBody
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
// TODO: Pass actual error as prop and display expandable details for network errors.
|
||||||
|
const ContentError = ({ i18n }) => (
|
||||||
|
<EmptyState>
|
||||||
|
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
||||||
|
<Title size="lg">
|
||||||
|
{i18n._(t`Something went wrong...`)}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{i18n._(t`There was an error loading this content. Please reload the page.`)}
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { ContentError as _ContentError };
|
||||||
|
export default withI18n()(ContentError);
|
19
src/components/ContentLoading.jsx
Normal file
19
src/components/ContentLoading.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateBody
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
// TODO: Better loading state - skeleton lines / spinner, etc.
|
||||||
|
const ContentLoading = ({ i18n }) => (
|
||||||
|
<EmptyState>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{i18n._(t`Loading...`)}
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { ContentLoading as _ContentLoading };
|
||||||
|
export default withI18n()(ContentLoading);
|
@ -11,7 +11,6 @@ import {
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { withNetwork } from '../../contexts/Network';
|
|
||||||
import PaginatedDataList from '../PaginatedDataList';
|
import PaginatedDataList from '../PaginatedDataList';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
import CheckboxListItem from '../ListItem';
|
import CheckboxListItem from '../ListItem';
|
||||||
@ -53,8 +52,8 @@ class Lookup extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getData () {
|
async getData () {
|
||||||
const { getItems, handleHttpError, location } = this.props;
|
const { getItems, location: { search } } = this.props;
|
||||||
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
|
const queryParams = parseNamespacedQueryString(this.qsConfig, search);
|
||||||
|
|
||||||
this.setState({ error: false });
|
this.setState({ error: false });
|
||||||
try {
|
try {
|
||||||
@ -66,7 +65,7 @@ class Lookup extends React.Component {
|
|||||||
count
|
count
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,4 +213,4 @@ Lookup.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { Lookup as _Lookup };
|
export { Lookup as _Lookup };
|
||||||
export default withI18n()(withNetwork(withRouter(Lookup)));
|
export default withI18n()(withRouter(Lookup));
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
|
|
||||||
import { Redirect, withRouter } from 'react-router-dom';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
|
|
||||||
import { withRootDialog } from '../contexts/RootDialog';
|
|
||||||
|
|
||||||
const NotifyAndRedirect = ({
|
|
||||||
to,
|
|
||||||
push,
|
|
||||||
from,
|
|
||||||
exact,
|
|
||||||
strict,
|
|
||||||
sensitive,
|
|
||||||
setRootDialogMessage,
|
|
||||||
location,
|
|
||||||
i18n
|
|
||||||
}) => {
|
|
||||||
setRootDialogMessage({
|
|
||||||
title: '404',
|
|
||||||
bodyText: (
|
|
||||||
<Fragment>{i18n._(t`Cannot find route ${(<strong>{location.pathname}</strong>)}.`)}</Fragment>
|
|
||||||
),
|
|
||||||
variant: 'warning'
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={to}
|
|
||||||
push={push}
|
|
||||||
from={from}
|
|
||||||
exact={exact}
|
|
||||||
strict={strict}
|
|
||||||
sensitive={sensitive}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { NotifyAndRedirect as _NotifyAndRedirect };
|
|
||||||
export default withI18n()(withRootDialog(withRouter(NotifyAndRedirect)));
|
|
@ -1,18 +1,14 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes, { arrayOf, shape, string, bool } from 'prop-types';
|
import PropTypes, { arrayOf, shape, string, bool } from 'prop-types';
|
||||||
import {
|
import { DataList } from '@patternfly/react-core';
|
||||||
DataList,
|
|
||||||
Title,
|
|
||||||
EmptyState,
|
|
||||||
EmptyStateIcon,
|
|
||||||
EmptyStateBody
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { CubesIcon } from '@patternfly/react-icons';
|
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import ContentEmpty from '../ContentEmpty';
|
||||||
|
import ContentError from '../ContentError';
|
||||||
|
import ContentLoading from '../ContentLoading';
|
||||||
import Pagination from '../Pagination';
|
import Pagination from '../Pagination';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
import PaginatedDataListItem from './PaginatedDataListItem';
|
import PaginatedDataListItem from './PaginatedDataListItem';
|
||||||
@ -37,11 +33,6 @@ const EmptyStateControlsWrapper = styled.div`
|
|||||||
class PaginatedDataList extends React.Component {
|
class PaginatedDataList extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleSetPage = this.handleSetPage.bind(this);
|
this.handleSetPage = this.handleSetPage.bind(this);
|
||||||
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
||||||
this.handleSort = this.handleSort.bind(this);
|
this.handleSort = this.handleSort.bind(this);
|
||||||
@ -79,7 +70,10 @@ class PaginatedDataList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const [orderBy, sortOrder] = this.getSortOrder();
|
||||||
const {
|
const {
|
||||||
|
contentError,
|
||||||
|
contentLoading,
|
||||||
emptyStateControls,
|
emptyStateControls,
|
||||||
items,
|
items,
|
||||||
itemCount,
|
itemCount,
|
||||||
@ -93,37 +87,44 @@ class PaginatedDataList extends React.Component {
|
|||||||
i18n,
|
i18n,
|
||||||
renderToolbar,
|
renderToolbar,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { error } = this.state;
|
|
||||||
const [orderBy, sortOrder] = this.getSortOrder();
|
|
||||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
|
||||||
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true }];
|
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true }];
|
||||||
|
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||||
|
|
||||||
|
const itemDisplayName = ucFirst(pluralize(itemName));
|
||||||
|
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
|
||||||
|
|
||||||
|
const dataListLabel = i18n._(t`${itemDisplayName} List`);
|
||||||
|
const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `);
|
||||||
|
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
|
||||||
|
|
||||||
|
let Content;
|
||||||
|
if (contentLoading && items.length <= 0) {
|
||||||
|
Content = (<ContentLoading />);
|
||||||
|
} else if (contentError) {
|
||||||
|
Content = (<ContentError />);
|
||||||
|
} else if (items.length <= 0) {
|
||||||
|
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
||||||
|
} else {
|
||||||
|
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length <= 0) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{error && (
|
{emptyStateControls && (
|
||||||
<Fragment>
|
|
||||||
<div>{error.message}</div>
|
|
||||||
{error.response && (
|
|
||||||
<div>{error.response.data.detail}</div>
|
|
||||||
)}
|
|
||||||
</Fragment> // TODO: replace with proper error handling
|
|
||||||
)}
|
|
||||||
{items.length === 0 ? (
|
|
||||||
<Fragment>
|
|
||||||
<EmptyStateControlsWrapper>
|
<EmptyStateControlsWrapper>
|
||||||
{emptyStateControls}
|
{emptyStateControls}
|
||||||
</EmptyStateControlsWrapper>
|
</EmptyStateControlsWrapper>
|
||||||
|
)}
|
||||||
|
{emptyStateControls && (
|
||||||
<div css="border-bottom: 1px solid #d2d2d2" />
|
<div css="border-bottom: 1px solid #d2d2d2" />
|
||||||
<EmptyState>
|
)}
|
||||||
<EmptyStateIcon icon={CubesIcon} />
|
{Content}
|
||||||
<Title size="lg">
|
|
||||||
{i18n._(t`No ${ucFirst(itemNamePlural || pluralize(itemName))} Found `)}
|
|
||||||
</Title>
|
|
||||||
<EmptyStateBody>
|
|
||||||
{i18n._(t`Please add ${ucFirst(itemNamePlural || pluralize(itemName))} to populate this list `)}
|
|
||||||
</EmptyStateBody>
|
|
||||||
</EmptyState>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{renderToolbar({
|
{renderToolbar({
|
||||||
sortedColumnKey: orderBy,
|
sortedColumnKey: orderBy,
|
||||||
@ -132,11 +133,7 @@ class PaginatedDataList extends React.Component {
|
|||||||
onSearch: () => { },
|
onSearch: () => { },
|
||||||
onSort: this.handleSort,
|
onSort: this.handleSort,
|
||||||
})}
|
})}
|
||||||
<DataList aria-label={i18n._(t`${ucFirst(pluralize(itemName))} List`)}>
|
{Content}
|
||||||
{items.map(item => (renderItem ? renderItem(item) : (
|
|
||||||
<PaginatedDataListItem key={item.id} item={item} />
|
|
||||||
)))}
|
|
||||||
</DataList>
|
|
||||||
<Pagination
|
<Pagination
|
||||||
variant="bottom"
|
variant="bottom"
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@ -152,8 +149,6 @@ class PaginatedDataList extends React.Component {
|
|||||||
onPerPageSelect={this.handleSetPageSize}
|
onPerPageSelect={this.handleSetPageSize}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,14 +173,18 @@ PaginatedDataList.propTypes = {
|
|||||||
})),
|
})),
|
||||||
showPageSizeOptions: PropTypes.bool,
|
showPageSizeOptions: PropTypes.bool,
|
||||||
renderToolbar: PropTypes.func,
|
renderToolbar: PropTypes.func,
|
||||||
|
contentLoading: PropTypes.bool,
|
||||||
|
contentError: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
PaginatedDataList.defaultProps = {
|
PaginatedDataList.defaultProps = {
|
||||||
renderItem: null,
|
contentLoading: false,
|
||||||
|
contentError: false,
|
||||||
toolbarColumns: [],
|
toolbarColumns: [],
|
||||||
itemName: 'item',
|
itemName: 'item',
|
||||||
itemNamePlural: '',
|
itemNamePlural: '',
|
||||||
showPageSizeOptions: true,
|
showPageSizeOptions: true,
|
||||||
|
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
|
||||||
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,136 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { withNetwork } from './Network';
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const ConfigContext = React.createContext({});
|
||||||
|
|
||||||
import { ConfigAPI, MeAPI, RootAPI } from '../api';
|
export const ConfigProvider = ConfigContext.Provider;
|
||||||
|
export const Config = ConfigContext.Consumer;
|
||||||
const ConfigContext = React.createContext({});
|
|
||||||
|
|
||||||
class Provider extends Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: {
|
|
||||||
ansible_version: null,
|
|
||||||
custom_virtualenvs: null,
|
|
||||||
version: null,
|
|
||||||
custom_logo: null,
|
|
||||||
custom_login_info: null,
|
|
||||||
me: {},
|
|
||||||
...props.value
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fetchConfig = this.fetchConfig.bind(this);
|
|
||||||
this.fetchMe = this.fetchMe.bind(this);
|
|
||||||
this.updateConfig = this.updateConfig.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { value } = this.props;
|
|
||||||
if (!value) {
|
|
||||||
this.fetchConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig = config => {
|
|
||||||
const { ansible_version, custom_virtualenvs, version } = config;
|
|
||||||
|
|
||||||
this.setState(prevState => ({
|
|
||||||
value: {
|
|
||||||
...prevState.value,
|
|
||||||
ansible_version,
|
|
||||||
custom_virtualenvs,
|
|
||||||
version
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
async fetchMe () {
|
|
||||||
const { handleHttpError } = this.props;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: {
|
|
||||||
results: [me]
|
|
||||||
}
|
|
||||||
} = await MeAPI.read();
|
|
||||||
this.setState(prevState => ({
|
|
||||||
value: {
|
|
||||||
...prevState.value,
|
|
||||||
me
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
handleHttpError(err)
|
|
||||||
|| this.setState({
|
|
||||||
value: {
|
|
||||||
ansible_version: null,
|
|
||||||
custom_virtualenvs: null,
|
|
||||||
version: null,
|
|
||||||
custom_logo: null,
|
|
||||||
custom_login_info: null,
|
|
||||||
me: {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchConfig () {
|
|
||||||
const { handleHttpError } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [configRes, rootRes, meRes] = await Promise.all([
|
|
||||||
ConfigAPI.read(),
|
|
||||||
RootAPI.read(),
|
|
||||||
MeAPI.read()
|
|
||||||
]);
|
|
||||||
this.setState({
|
|
||||||
value: {
|
|
||||||
ansible_version: configRes.data.ansible_version,
|
|
||||||
custom_virtualenvs: configRes.data.custom_virtualenvs,
|
|
||||||
version: configRes.data.version,
|
|
||||||
custom_logo: rootRes.data.custom_logo,
|
|
||||||
custom_login_info: rootRes.data.custom_login_info,
|
|
||||||
me: meRes.data.results[0]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
handleHttpError(err)
|
|
||||||
|| this.setState({
|
|
||||||
value: {
|
|
||||||
ansible_version: null,
|
|
||||||
custom_virtualenvs: null,
|
|
||||||
version: null,
|
|
||||||
custom_logo: null,
|
|
||||||
custom_login_info: null,
|
|
||||||
me: {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value } = this.state;
|
|
||||||
|
|
||||||
const { children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigContext.Provider
|
|
||||||
value={{
|
|
||||||
...value,
|
|
||||||
fetchMe: this.fetchMe,
|
|
||||||
updateConfig: this.updateConfig
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ConfigContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfigProvider = withNetwork(Provider);
|
|
||||||
|
|
||||||
export const Config = ({ children }) => (
|
|
||||||
<ConfigContext.Consumer>{value => children(value)}</ConfigContext.Consumer>
|
|
||||||
);
|
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
|
|
||||||
import { withRootDialog } from './RootDialog';
|
|
||||||
|
|
||||||
const NetworkContext = React.createContext({});
|
|
||||||
|
|
||||||
class Provider extends Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: {
|
|
||||||
handleHttpError: err => {
|
|
||||||
if (err.response.status === 401) {
|
|
||||||
this.handle401();
|
|
||||||
} else if (err.response.status === 404) {
|
|
||||||
this.handle404();
|
|
||||||
}
|
|
||||||
return (err.response.status === 401 || err.response.status === 404);
|
|
||||||
},
|
|
||||||
...props.value
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handle401 () {
|
|
||||||
const { handle401, history, setRootDialogMessage, i18n } = this.props;
|
|
||||||
if (handle401) {
|
|
||||||
handle401();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
history.replace('/login');
|
|
||||||
setRootDialogMessage({
|
|
||||||
bodyText: i18n._(t`You have been logged out.`)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handle404 () {
|
|
||||||
const { handle404, history, setRootDialogMessage, i18n } = this.props;
|
|
||||||
if (handle404) {
|
|
||||||
handle404();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
history.replace('/home');
|
|
||||||
setRootDialogMessage({
|
|
||||||
title: i18n._(t`404`),
|
|
||||||
bodyText: i18n._(t`Cannot find resource.`),
|
|
||||||
variant: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value } = this.state;
|
|
||||||
|
|
||||||
const { children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NetworkContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</NetworkContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Provider as _NetworkProvider };
|
|
||||||
export const NetworkProvider = withI18n()(withRootDialog(withRouter(Provider)));
|
|
||||||
|
|
||||||
export function withNetwork (Child) {
|
|
||||||
return (props) => (
|
|
||||||
<NetworkContext.Consumer>
|
|
||||||
{context => <Child {...props} {...context} />}
|
|
||||||
</NetworkContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
const RootDialogContext = React.createContext({});
|
|
||||||
|
|
||||||
export class RootDialogProvider extends Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
value: {
|
|
||||||
title: null,
|
|
||||||
setRootDialogMessage: ({ title, bodyText, variant }) => {
|
|
||||||
const { value } = this.state;
|
|
||||||
this.setState({ value: { ...value, title, bodyText, variant } });
|
|
||||||
},
|
|
||||||
clearRootDialogMessage: () => {
|
|
||||||
const { value } = this.state;
|
|
||||||
this.setState({ value: { ...value, title: null, bodyText: null, variant: null } });
|
|
||||||
},
|
|
||||||
...props.value
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const {
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
value
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RootDialogContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</RootDialogContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RootDialog = ({ children }) => (
|
|
||||||
<RootDialogContext.Consumer>
|
|
||||||
{value => children(value)}
|
|
||||||
</RootDialogContext.Consumer>
|
|
||||||
);
|
|
||||||
|
|
||||||
export function withRootDialog (Child) {
|
|
||||||
return (props) => (
|
|
||||||
<RootDialogContext.Consumer>
|
|
||||||
{context => <Child {...props} {...context} />}
|
|
||||||
</RootDialogContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
|
@ -13,15 +13,12 @@ import { t } from '@lingui/macro';
|
|||||||
import '@patternfly/react-core/dist/styles/base.css';
|
import '@patternfly/react-core/dist/styles/base.css';
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
|
|
||||||
import { Config } from './contexts/Config';
|
|
||||||
|
|
||||||
import { BrandName } from './variables';
|
|
||||||
|
|
||||||
import Background from './components/Background';
|
import Background from './components/Background';
|
||||||
import NotifyAndRedirect from './components/NotifyAndRedirect';
|
|
||||||
|
|
||||||
import RootProvider from './RootProvider';
|
import RootProvider from './RootProvider';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { BrandName } from './variables';
|
||||||
|
import { isAuthenticated } from './util/auth';
|
||||||
|
|
||||||
import Applications from './pages/Applications';
|
import Applications from './pages/Applications';
|
||||||
import Credentials from './pages/Credentials';
|
import Credentials from './pages/Credentials';
|
||||||
@ -52,12 +49,8 @@ export function main (render) {
|
|||||||
const el = document.getElementById('app');
|
const el = document.getElementById('app');
|
||||||
document.title = `Ansible ${BrandName}`;
|
document.title = `Ansible ${BrandName}`;
|
||||||
|
|
||||||
return render(
|
const defaultRedirect = () => (<Redirect to="/home" />);
|
||||||
<RootProvider>
|
const removeTrailingSlash = (
|
||||||
<I18n>
|
|
||||||
{({ i18n }) => (
|
|
||||||
<Background>
|
|
||||||
<Switch>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
strict
|
strict
|
||||||
@ -66,22 +59,30 @@ export function main (render) {
|
|||||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
const loginRoutes = (
|
||||||
|
<Switch>
|
||||||
|
{removeTrailingSlash}
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
render={() => (
|
render={() => (
|
||||||
<Config>
|
<Login isAuthenticated={isAuthenticated} />
|
||||||
{({ custom_logo, custom_login_info, fetchMe, updateConfig }) => (
|
|
||||||
<Login
|
|
||||||
logo={custom_logo}
|
|
||||||
loginInfo={custom_login_info}
|
|
||||||
fetchMe={fetchMe}
|
|
||||||
updateConfig={updateConfig}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Config>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/" render={() => <Redirect to="/home" />} />
|
<Redirect to="/login" />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<RootProvider>
|
||||||
|
<I18n>
|
||||||
|
{({ i18n }) => (
|
||||||
|
<Background>
|
||||||
|
{!isAuthenticated() ? loginRoutes : (
|
||||||
|
<Switch>
|
||||||
|
{removeTrailingSlash}
|
||||||
|
<Route path="/login" render={defaultRedirect} />
|
||||||
|
<Route exact path="/" render={defaultRedirect} />
|
||||||
<Route
|
<Route
|
||||||
render={() => (
|
render={() => (
|
||||||
<App
|
<App
|
||||||
@ -229,8 +230,7 @@ export function main (render) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
render={({ routeGroups }) => (
|
render={({ routeGroups }) => (
|
||||||
<Switch>
|
routeGroups
|
||||||
{routeGroups
|
|
||||||
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
||||||
.map(({ component: PageComponent, path }) => (
|
.map(({ component: PageComponent, path }) => (
|
||||||
<Route
|
<Route
|
||||||
@ -241,15 +241,12 @@ export function main (render) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.concat([
|
|
||||||
<NotifyAndRedirect key="redirect" to="/" />
|
|
||||||
])}
|
|
||||||
</Switch>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
)}
|
||||||
</Background>
|
</Background>
|
||||||
)}
|
)}
|
||||||
</I18n>
|
</I18n>
|
||||||
|
@ -7,13 +7,10 @@ import {
|
|||||||
LoginForm,
|
LoginForm,
|
||||||
LoginPage as PFLoginPage,
|
LoginPage as PFLoginPage,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { withRootDialog } from '../contexts/RootDialog';
|
|
||||||
import { withNetwork } from '../contexts/Network';
|
|
||||||
import { RootAPI } from '../api';
|
import { RootAPI } from '../api';
|
||||||
import { BrandName } from '../variables';
|
import { BrandName } from '../variables';
|
||||||
|
|
||||||
import logoImg from '../../images/brand-logo.svg';
|
import brandLogo from '../../images/brand-logo.svg';
|
||||||
|
|
||||||
const LoginPage = styled(PFLoginPage)`
|
const LoginPage = styled(PFLoginPage)`
|
||||||
& .pf-c-brand {
|
& .pf-c-brand {
|
||||||
@ -28,80 +25,122 @@ class AWXLogin extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
isInputValid: true,
|
authenticationError: false,
|
||||||
isLoading: false,
|
validationError: false,
|
||||||
isAuthenticated: false
|
isAuthenticating: false,
|
||||||
|
isLoading: true,
|
||||||
|
logo: null,
|
||||||
|
loginInfo: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onChangeUsername = this.onChangeUsername.bind(this);
|
this.handleChangeUsername = this.handleChangeUsername.bind(this);
|
||||||
this.onChangePassword = this.onChangePassword.bind(this);
|
this.handleChangePassword = this.handleChangePassword.bind(this);
|
||||||
this.onLoginButtonClick = this.onLoginButtonClick.bind(this);
|
this.handleLoginButtonClick = this.handleLoginButtonClick.bind(this);
|
||||||
|
this.loadCustomLoginInfo = this.loadCustomLoginInfo.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeUsername (value) {
|
async componentDidMount () {
|
||||||
this.setState({ username: value, isInputValid: true });
|
await this.loadCustomLoginInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangePassword (value) {
|
async loadCustomLoginInfo () {
|
||||||
this.setState({ password: value, isInputValid: true });
|
this.setState({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const { data: { custom_logo, custom_login_info } } = await RootAPI.read();
|
||||||
|
const logo = custom_logo ? `data:image/jpeg;${custom_logo}` : brandLogo;
|
||||||
|
|
||||||
|
this.setState({ logo, loginInfo: custom_login_info });
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ logo: brandLogo });
|
||||||
|
} finally {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onLoginButtonClick (event) {
|
async handleLoginButtonClick (event) {
|
||||||
const { username, password, isLoading } = this.state;
|
const { username, password, isAuthenticating } = this.state;
|
||||||
const { handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isAuthenticating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearRootDialogMessage();
|
this.setState({ authenticationError: false, isAuthenticating: true });
|
||||||
this.setState({ isLoading: true });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await RootAPI.login(username, password);
|
// note: if authentication is successful, the appropriate cookie will be set automatically
|
||||||
updateConfig(data);
|
// and isAuthenticated() (the source of truth) will start returning true.
|
||||||
await fetchMe();
|
await RootAPI.login(username, password);
|
||||||
this.setState({ isAuthenticated: true, isLoading: false });
|
} catch (err) {
|
||||||
} catch (error) {
|
if (err && err.response && err.response.status === 401) {
|
||||||
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
this.setState({ validationError: true });
|
||||||
|
} else {
|
||||||
|
this.setState({ authenticationError: true });
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.setState({ isAuthenticating: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeUsername (value) {
|
||||||
|
this.setState({ username: value, validationError: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangePassword (value) {
|
||||||
|
this.setState({ password: value, validationError: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { username, password, isInputValid, isAuthenticated } = this.state;
|
const {
|
||||||
const { alt, loginInfo, logo, bodyText: errorMessage, i18n } = this.props;
|
authenticationError,
|
||||||
const logoSrc = logo ? `data:image/jpeg;${logo}` : logoImg;
|
validationError,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
isLoading,
|
||||||
|
logo,
|
||||||
|
loginInfo,
|
||||||
|
} = this.state;
|
||||||
|
const { alt, i18n, isAuthenticated } = this.props;
|
||||||
// Setting BrandName to a variable here is necessary to get the jest tests
|
// Setting BrandName to a variable here is necessary to get the jest tests
|
||||||
// passing. Attempting to use BrandName in the template literal results
|
// passing. Attempting to use BrandName in the template literal results
|
||||||
// in failing tests.
|
// in failing tests.
|
||||||
const brandName = BrandName;
|
const brandName = BrandName;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated()) {
|
||||||
return (<Redirect to="/" />);
|
return (<Redirect to="/" />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let helperText;
|
||||||
|
if (validationError) {
|
||||||
|
helperText = i18n._(t`Invalid username or password. Please try again.`);
|
||||||
|
} else {
|
||||||
|
helperText = i18n._(t`There was a problem signing in. Please try again.`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginPage
|
<LoginPage
|
||||||
brandImgSrc={logoSrc}
|
brandImgSrc={logo}
|
||||||
brandImgAlt={alt || brandName}
|
brandImgAlt={alt || brandName}
|
||||||
loginTitle={i18n._(t`Welcome to Ansible ${brandName}! Please Sign In.`)}
|
loginTitle={i18n._(t`Welcome to Ansible ${brandName}! Please Sign In.`)}
|
||||||
textContent={loginInfo}
|
textContent={loginInfo}
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
className={errorMessage && 'pf-m-error'}
|
className={(authenticationError || validationError) ? 'pf-m-error' : ''}
|
||||||
usernameLabel={i18n._(t`Username`)}
|
usernameLabel={i18n._(t`Username`)}
|
||||||
passwordLabel={i18n._(t`Password`)}
|
passwordLabel={i18n._(t`Password`)}
|
||||||
showHelperText={!isInputValid || !!errorMessage}
|
showHelperText={(authenticationError || validationError)}
|
||||||
helperText={errorMessage || i18n._(t`Invalid username or password. Please try again.`)}
|
helperText={helperText}
|
||||||
usernameValue={username}
|
usernameValue={username}
|
||||||
passwordValue={password}
|
passwordValue={password}
|
||||||
isValidUsername={isInputValid}
|
isValidUsername={!validationError}
|
||||||
isValidPassword={isInputValid}
|
isValidPassword={!validationError}
|
||||||
onChangeUsername={this.onChangeUsername}
|
onChangeUsername={this.handleChangeUsername}
|
||||||
onChangePassword={this.onChangePassword}
|
onChangePassword={this.handleChangePassword}
|
||||||
onLoginButtonClick={this.onLoginButtonClick}
|
onLoginButtonClick={this.handleLoginButtonClick}
|
||||||
/>
|
/>
|
||||||
</LoginPage>
|
</LoginPage>
|
||||||
);
|
);
|
||||||
@ -109,4 +148,4 @@ class AWXLogin extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { AWXLogin as _AWXLogin };
|
export { AWXLogin as _AWXLogin };
|
||||||
export default withI18n()(withNetwork(withRootDialog(withRouter(AWXLogin))));
|
export default withI18n()(withRouter(AWXLogin));
|
||||||
|
@ -4,9 +4,6 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import { NetworkProvider } from '../../contexts/Network';
|
|
||||||
import { withRootDialog } from '../../contexts/RootDialog';
|
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||||
|
|
||||||
import OrganizationsList from './screens/OrganizationsList';
|
import OrganizationsList from './screens/OrganizationsList';
|
||||||
@ -49,7 +46,7 @@ class Organizations extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { match, history, location, setRootDialogMessage, i18n } = this.props;
|
const { match, history, location } = this.props;
|
||||||
const { breadcrumbConfig } = this.state;
|
const { breadcrumbConfig } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -66,23 +63,7 @@ class Organizations extends Component {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}/:id`}
|
path={`${match.path}/:id`}
|
||||||
render={({ match: newRouteMatch }) => (
|
render={() => (
|
||||||
<NetworkProvider
|
|
||||||
handle404={() => {
|
|
||||||
history.replace('/organizations');
|
|
||||||
setRootDialogMessage({
|
|
||||||
title: '404',
|
|
||||||
bodyText: (
|
|
||||||
<Fragment>
|
|
||||||
{i18n._(t`Cannot find organization with ID`)}
|
|
||||||
<strong>{` ${newRouteMatch.params.id}`}</strong>
|
|
||||||
.
|
|
||||||
</Fragment>
|
|
||||||
),
|
|
||||||
variant: 'warning'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Config>
|
<Config>
|
||||||
{({ me }) => (
|
{({ me }) => (
|
||||||
<Organization
|
<Organization
|
||||||
@ -93,7 +74,6 @@ class Organizations extends Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
</NetworkProvider>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
@ -109,4 +89,4 @@ class Organizations extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { Organizations as _Organizations };
|
export { Organizations as _Organizations };
|
||||||
export default withI18n()(withRootDialog(withRouter(Organizations)));
|
export default withI18n()(withRouter(Organizations));
|
||||||
|
@ -7,8 +7,6 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
|
|||||||
|
|
||||||
import Lookup from '../../../components/Lookup';
|
import Lookup from '../../../components/Lookup';
|
||||||
|
|
||||||
import { withNetwork } from '../../../contexts/Network';
|
|
||||||
|
|
||||||
import { InstanceGroupsAPI } from '../../../api';
|
import { InstanceGroupsAPI } from '../../../api';
|
||||||
|
|
||||||
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params);
|
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params);
|
||||||
@ -66,4 +64,4 @@ InstanceGroupsLookup.defaultProps = {
|
|||||||
tooltip: '',
|
tooltip: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(withNetwork(InstanceGroupsLookup));
|
export default withI18n()(InstanceGroupsLookup);
|
||||||
|
@ -14,7 +14,6 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { Config } from '../../../contexts/Config';
|
import { Config } from '../../../contexts/Config';
|
||||||
import { withNetwork } from '../../../contexts/Network';
|
|
||||||
import FormRow from '../../../components/FormRow';
|
import FormRow from '../../../components/FormRow';
|
||||||
import FormField from '../../../components/FormField';
|
import FormField from '../../../components/FormField';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
@ -210,4 +209,4 @@ OrganizationForm.contextTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationForm as _OrganizationForm };
|
export { OrganizationForm as _OrganizationForm };
|
||||||
export default withI18n()(withNetwork(withRouter(OrganizationForm)));
|
export default withI18n()(withRouter(OrganizationForm));
|
||||||
|
@ -3,9 +3,8 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect } from 'react-router-dom';
|
import { Switch, Route, withRouter, Redirect } from 'react-router-dom';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
||||||
import { withNetwork } from '../../../../contexts/Network';
|
|
||||||
import NotifyAndRedirect from '../../../../components/NotifyAndRedirect';
|
|
||||||
import CardCloseButton from '../../../../components/CardCloseButton';
|
import CardCloseButton from '../../../../components/CardCloseButton';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
import OrganizationAccess from './OrganizationAccess';
|
import OrganizationAccess from './OrganizationAccess';
|
||||||
import OrganizationDetail from './OrganizationDetail';
|
import OrganizationDetail from './OrganizationDetail';
|
||||||
import OrganizationEdit from './OrganizationEdit';
|
import OrganizationEdit from './OrganizationEdit';
|
||||||
@ -20,77 +19,74 @@ class Organization extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
organization: null,
|
organization: null,
|
||||||
error: false,
|
contentLoading: true,
|
||||||
loading: true,
|
contentError: false,
|
||||||
|
isInitialized: false,
|
||||||
isNotifAdmin: false,
|
isNotifAdmin: false,
|
||||||
isAuditorOfThisOrg: false,
|
isAuditorOfThisOrg: false,
|
||||||
isAdminOfThisOrg: false
|
isAdminOfThisOrg: false,
|
||||||
};
|
};
|
||||||
|
this.loadOrganization = this.loadOrganization.bind(this);
|
||||||
this.fetchOrganization = this.fetchOrganization.bind(this);
|
this.loadOrganizationAndRoles = this.loadOrganizationAndRoles.bind(this);
|
||||||
this.fetchOrganizationAndRoles = this.fetchOrganizationAndRoles.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
async componentDidMount () {
|
||||||
this.fetchOrganizationAndRoles();
|
await this.loadOrganizationAndRoles();
|
||||||
|
this.setState({ isInitialized: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate (prevProps) {
|
async componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
await this.fetchOrganization();
|
await this.loadOrganization();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrganizationAndRoles () {
|
async loadOrganizationAndRoles () {
|
||||||
const {
|
const {
|
||||||
match,
|
match,
|
||||||
setBreadcrumb,
|
setBreadcrumb,
|
||||||
handleHttpError
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const id = parseInt(match.params.id, 10);
|
||||||
|
|
||||||
|
this.setState({ contentError: false, contentLoading: true });
|
||||||
try {
|
try {
|
||||||
const [{ data }, notifAdminRest, auditorRes, adminRes] = await Promise.all([
|
const [{ data }, notifAdminRes, auditorRes, adminRes] = await Promise.all([
|
||||||
OrganizationsAPI.readDetail(parseInt(match.params.id, 10)),
|
OrganizationsAPI.readDetail(id),
|
||||||
OrganizationsAPI.read({
|
OrganizationsAPI.read({ page_size: 1, role_level: 'notification_admin_role' }),
|
||||||
role_level: 'notification_admin_role',
|
OrganizationsAPI.read({ id, role_level: 'auditor_role' }),
|
||||||
page_size: 1
|
OrganizationsAPI.read({ id, role_level: 'admin_role' }),
|
||||||
}),
|
|
||||||
OrganizationsAPI.read({
|
|
||||||
role_level: 'auditor_role',
|
|
||||||
id: parseInt(match.params.id, 10)
|
|
||||||
}),
|
|
||||||
OrganizationsAPI.read({
|
|
||||||
role_level: 'admin_role',
|
|
||||||
id: parseInt(match.params.id, 10)
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
setBreadcrumb(data);
|
setBreadcrumb(data);
|
||||||
this.setState({
|
this.setState({
|
||||||
organization: data,
|
organization: data,
|
||||||
loading: false,
|
isNotifAdmin: notifAdminRes.data.results.length > 0,
|
||||||
isNotifAdmin: notifAdminRest.data.results.length > 0,
|
|
||||||
isAuditorOfThisOrg: auditorRes.data.results.length > 0,
|
isAuditorOfThisOrg: auditorRes.data.results.length > 0,
|
||||||
isAdminOfThisOrg: adminRes.data.results.length > 0
|
isAdminOfThisOrg: adminRes.data.results.length > 0
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
handleHttpError(error) || this.setState({ error: true, loading: false });
|
this.setState(({ contentError: true }));
|
||||||
|
} finally {
|
||||||
|
this.setState({ contentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrganization () {
|
async loadOrganization () {
|
||||||
const {
|
const {
|
||||||
match,
|
match,
|
||||||
setBreadcrumb,
|
setBreadcrumb,
|
||||||
handleHttpError
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const id = parseInt(match.params.id, 10);
|
||||||
|
|
||||||
|
this.setState({ contentError: false, contentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await OrganizationsAPI.readDetail(parseInt(match.params.id, 10));
|
const { data } = await OrganizationsAPI.readDetail(id);
|
||||||
setBreadcrumb(data);
|
setBreadcrumb(data);
|
||||||
this.setState({ organization: data, loading: false });
|
this.setState({ organization: data });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
handleHttpError(error) || this.setState({ error: true, loading: false });
|
this.setState(({ contentError: true }));
|
||||||
|
} finally {
|
||||||
|
this.setState({ contentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,8 +101,9 @@ class Organization extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
organization,
|
organization,
|
||||||
error,
|
contentError,
|
||||||
loading,
|
contentLoading,
|
||||||
|
isInitialized,
|
||||||
isNotifAdmin,
|
isNotifAdmin,
|
||||||
isAuditorOfThisOrg,
|
isAuditorOfThisOrg,
|
||||||
isAdminOfThisOrg
|
isAdminOfThisOrg
|
||||||
@ -134,7 +131,6 @@ class Organization extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
loading ? '' : (
|
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<CardHeader style={{ padding: 0 }}>
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="awx-orgTabs-container">
|
<div className="awx-orgTabs-container">
|
||||||
@ -151,8 +147,12 @@ class Organization extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isInitialized) {
|
||||||
|
cardHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
@ -161,6 +161,16 @@ class Organization extends Component {
|
|||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!contentLoading && contentError) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card className="awx-c-card">
|
||||||
|
<ContentError />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
@ -220,18 +230,12 @@ class Organization extends Component {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization && (
|
|
||||||
<NotifyAndRedirect
|
|
||||||
to={`/organizations/${match.params.id}/details`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Switch>
|
</Switch>
|
||||||
{error ? 'error!' : ''}
|
|
||||||
{loading ? 'loading...' : ''}
|
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default withI18n()(withNetwork(withRouter(Organization)));
|
|
||||||
|
export default withI18n()(withRouter(Organization));
|
||||||
export { Organization as _Organization };
|
export { Organization as _Organization };
|
||||||
|
@ -2,13 +2,17 @@ import React, { Fragment } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import AlertModal from '../../../../components/AlertModal';
|
||||||
import PaginatedDataList, { ToolbarAddButton } from '../../../../components/PaginatedDataList';
|
import PaginatedDataList, { ToolbarAddButton } from '../../../../components/PaginatedDataList';
|
||||||
import DataListToolbar from '../../../../components/DataListToolbar';
|
import DataListToolbar from '../../../../components/DataListToolbar';
|
||||||
import OrganizationAccessItem from '../../components/OrganizationAccessItem';
|
import OrganizationAccessItem from '../../components/OrganizationAccessItem';
|
||||||
import DeleteRoleConfirmationModal from '../../components/DeleteRoleConfirmationModal';
|
import DeleteRoleConfirmationModal from '../../components/DeleteRoleConfirmationModal';
|
||||||
import AddResourceRole from '../../../../components/AddRole/AddResourceRole';
|
import AddResourceRole from '../../../../components/AddRole/AddResourceRole';
|
||||||
import { withNetwork } from '../../../../contexts/Network';
|
import {
|
||||||
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
getQSConfig,
|
||||||
|
encodeQueryString,
|
||||||
|
parseNamespacedQueryString
|
||||||
|
} from '../../../../util/qs';
|
||||||
import { Organization } from '../../../../types';
|
import { Organization } from '../../../../types';
|
||||||
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../api';
|
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../api';
|
||||||
|
|
||||||
@ -25,146 +29,139 @@ class OrganizationAccess extends React.Component {
|
|||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.readOrgAccessList = this.readOrgAccessList.bind(this);
|
|
||||||
this.confirmRemoveRole = this.confirmRemoveRole.bind(this);
|
|
||||||
this.cancelRemoveRole = this.cancelRemoveRole.bind(this);
|
|
||||||
this.removeRole = this.removeRole.bind(this);
|
|
||||||
this.toggleAddModal = this.toggleAddModal.bind(this);
|
|
||||||
this.handleSuccessfulRoleAdd = this.handleSuccessfulRoleAdd.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isLoading: false,
|
|
||||||
isInitialized: false,
|
|
||||||
isAddModalOpen: false,
|
|
||||||
error: null,
|
|
||||||
itemCount: 0,
|
|
||||||
accessRecords: [],
|
accessRecords: [],
|
||||||
roleToDelete: null,
|
contentError: false,
|
||||||
roleToDeleteAccessRecord: null,
|
contentLoading: true,
|
||||||
|
deletionError: false,
|
||||||
|
deletionRecord: null,
|
||||||
|
deletionRole: null,
|
||||||
|
isAddModalOpen: false,
|
||||||
|
itemCount: 0,
|
||||||
};
|
};
|
||||||
|
this.loadAccessList = this.loadAccessList.bind(this);
|
||||||
|
this.handleAddClose = this.handleAddClose.bind(this);
|
||||||
|
this.handleAddOpen = this.handleAddOpen.bind(this);
|
||||||
|
this.handleAddSuccess = this.handleAddSuccess.bind(this);
|
||||||
|
this.handleDeleteCancel = this.handleDeleteCancel.bind(this);
|
||||||
|
this.handleDeleteConfirm = this.handleDeleteConfirm.bind(this);
|
||||||
|
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
|
||||||
|
this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.readOrgAccessList();
|
this.loadAccessList();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
|
||||||
this.readOrgAccessList();
|
const prevParams = parseNamespacedQueryString(QS_CONFIG, prevProps.location.search);
|
||||||
|
const currentParams = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
|
if (encodeQueryString(currentParams) !== encodeQueryString(prevParams)) {
|
||||||
|
this.loadAccessList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readOrgAccessList () {
|
async loadAccessList () {
|
||||||
const { organization, handleHttpError, location } = this.props;
|
const { organization, location } = this.props;
|
||||||
this.setState({ isLoading: true });
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
|
this.setState({ contentError: false, contentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await OrganizationsAPI.readAccessList(
|
const {
|
||||||
organization.id,
|
data: {
|
||||||
parseNamespacedQueryString(QS_CONFIG, location.search)
|
results: accessRecords = [],
|
||||||
);
|
count: itemCount = 0
|
||||||
this.setState({
|
}
|
||||||
itemCount: data.count || 0,
|
} = await OrganizationsAPI.readAccessList(organization.id, params);
|
||||||
accessRecords: data.results || [],
|
this.setState({ itemCount, accessRecords });
|
||||||
isLoading: false,
|
|
||||||
isInitialized: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleHttpError(error) || this.setState({
|
this.setState({ contentError: true });
|
||||||
error,
|
} finally {
|
||||||
isLoading: false,
|
this.setState({ contentLoading: false });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmRemoveRole (role, accessRecord) {
|
handleDeleteOpen (deletionRole, deletionRecord) {
|
||||||
|
this.setState({ deletionRole, deletionRecord });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteCancel () {
|
||||||
|
this.setState({ deletionRole: null, deletionRecord: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteErrorClose () {
|
||||||
this.setState({
|
this.setState({
|
||||||
roleToDelete: role,
|
deletionError: false,
|
||||||
roleToDeleteAccessRecord: accessRecord,
|
deletionRecord: null,
|
||||||
|
deletionRole: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelRemoveRole () {
|
async handleDeleteConfirm () {
|
||||||
this.setState({
|
const { deletionRole, deletionRecord } = this.state;
|
||||||
roleToDelete: null,
|
|
||||||
roleToDeleteAccessRecord: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRole () {
|
if (!deletionRole || !deletionRecord) {
|
||||||
const { handleHttpError } = this.props;
|
|
||||||
const { roleToDelete: role, roleToDeleteAccessRecord: accessRecord } = this.state;
|
|
||||||
if (!role || !accessRecord) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const type = typeof role.team_id === 'undefined' ? 'users' : 'teams';
|
|
||||||
this.setState({ isLoading: true });
|
let promise;
|
||||||
try {
|
if (typeof deletionRole.team_id !== 'undefined') {
|
||||||
if (type === 'teams') {
|
promise = TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
|
||||||
await TeamsAPI.disassociateRole(role.team_id, role.id);
|
|
||||||
} else {
|
} else {
|
||||||
await UsersAPI.disassociateRole(accessRecord.id, role.id);
|
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({ contentLoading: true });
|
||||||
|
try {
|
||||||
|
await promise.then(this.loadAccessList);
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
deletionRole: null,
|
||||||
roleToDelete: null,
|
deletionRecord: null
|
||||||
roleToDeleteAccessRecord: null,
|
|
||||||
});
|
});
|
||||||
this.readOrgAccessList();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleHttpError(error) || this.setState({
|
|
||||||
error,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleAddModal () {
|
|
||||||
const { isAddModalOpen } = this.state;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isAddModalOpen: !isAddModalOpen,
|
contentLoading: false,
|
||||||
|
deletionError: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleSuccessfulRoleAdd () {
|
handleAddClose () {
|
||||||
this.toggleAddModal();
|
this.setState({ isAddModalOpen: false });
|
||||||
this.readOrgAccessList();
|
}
|
||||||
|
|
||||||
|
handleAddOpen () {
|
||||||
|
this.setState({ isAddModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddSuccess () {
|
||||||
|
this.setState({ isAddModalOpen: false });
|
||||||
|
this.loadAccessList();
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { organization, i18n } = this.props;
|
const { organization, i18n } = this.props;
|
||||||
const {
|
const {
|
||||||
isLoading,
|
accessRecords,
|
||||||
isInitialized,
|
contentError,
|
||||||
|
contentLoading,
|
||||||
|
deletionRole,
|
||||||
|
deletionRecord,
|
||||||
|
deletionError,
|
||||||
itemCount,
|
itemCount,
|
||||||
isAddModalOpen,
|
isAddModalOpen,
|
||||||
accessRecords,
|
|
||||||
roleToDelete,
|
|
||||||
roleToDeleteAccessRecord,
|
|
||||||
error,
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const canEdit = organization.summary_fields.user_capabilities.edit;
|
const canEdit = organization.summary_fields.user_capabilities.edit;
|
||||||
|
const isDeleteModalOpen = !contentLoading && !deletionError && deletionRole;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
// TODO: better error state
|
|
||||||
return <div>{error.message}</div>;
|
|
||||||
}
|
|
||||||
// TODO: better loading state
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{isLoading && (<div>Loading...</div>)}
|
|
||||||
{roleToDelete && (
|
|
||||||
<DeleteRoleConfirmationModal
|
|
||||||
role={roleToDelete}
|
|
||||||
username={roleToDeleteAccessRecord.username}
|
|
||||||
onCancel={this.cancelRemoveRole}
|
|
||||||
onConfirm={this.removeRole}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isInitialized && (
|
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
contentLoading={contentLoading}
|
||||||
items={accessRecords}
|
items={accessRecords}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
itemName="role"
|
itemName="role"
|
||||||
@ -178,7 +175,7 @@ class OrganizationAccess extends React.Component {
|
|||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
additionalControls={canEdit ? [
|
additionalControls={canEdit ? [
|
||||||
<ToolbarAddButton key="add" onClick={this.toggleAddModal} />
|
<ToolbarAddButton key="add" onClick={this.handleAddOpen} />
|
||||||
] : null}
|
] : null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -186,22 +183,37 @@ class OrganizationAccess extends React.Component {
|
|||||||
<OrganizationAccessItem
|
<OrganizationAccessItem
|
||||||
key={accessRecord.id}
|
key={accessRecord.id}
|
||||||
accessRecord={accessRecord}
|
accessRecord={accessRecord}
|
||||||
onRoleDelete={this.confirmRemoveRole}
|
onRoleDelete={this.handleDeleteOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isAddModalOpen && (
|
{isAddModalOpen && (
|
||||||
<AddResourceRole
|
<AddResourceRole
|
||||||
onClose={this.toggleAddModal}
|
onClose={this.handleAddClose}
|
||||||
onSave={this.handleSuccessfulRoleAdd}
|
onSave={this.handleAddSuccess}
|
||||||
roles={organization.summary_fields.object_roles}
|
roles={organization.summary_fields.object_roles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isDeleteModalOpen && (
|
||||||
|
<DeleteRoleConfirmationModal
|
||||||
|
role={deletionRole}
|
||||||
|
username={deletionRecord.username}
|
||||||
|
onCancel={this.handleDeleteCancel}
|
||||||
|
onConfirm={this.handleDeleteConfirm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={this.handleDeleteErrorClose}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete role`)}
|
||||||
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OrganizationAccess as _OrganizationAccess };
|
export { OrganizationAccess as _OrganizationAccess };
|
||||||
export default withI18n()(withNetwork(withRouter(OrganizationAccess)));
|
export default withI18n()(withRouter(OrganizationAccess));
|
||||||
|
@ -4,9 +4,11 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
import { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { DetailList, Detail } from '../../../../components/DetailList';
|
import { DetailList, Detail } from '../../../../components/DetailList';
|
||||||
import { withNetwork } from '../../../../contexts/Network';
|
|
||||||
import { ChipGroup, Chip } from '../../../../components/Chip';
|
import { ChipGroup, Chip } from '../../../../components/Chip';
|
||||||
|
import ContentError from '../../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../../components/ContentLoading';
|
||||||
import { OrganizationsAPI } from '../../../../api';
|
import { OrganizationsAPI } from '../../../../api';
|
||||||
|
|
||||||
const CardBody = styled(PFCardBody)`
|
const CardBody = styled(PFCardBody)`
|
||||||
@ -18,8 +20,9 @@ class OrganizationDetail extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
contentError: false,
|
||||||
|
contentLoading: true,
|
||||||
instanceGroups: [],
|
instanceGroups: [],
|
||||||
error: false
|
|
||||||
};
|
};
|
||||||
this.loadInstanceGroups = this.loadInstanceGroups.bind(this);
|
this.loadInstanceGroups = this.loadInstanceGroups.bind(this);
|
||||||
}
|
}
|
||||||
@ -29,25 +32,23 @@ class OrganizationDetail extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadInstanceGroups () {
|
async loadInstanceGroups () {
|
||||||
const {
|
const { match: { params: { id } } } = this.props;
|
||||||
handleHttpError,
|
|
||||||
match
|
this.setState({ contentLoading: true });
|
||||||
} = this.props;
|
|
||||||
try {
|
try {
|
||||||
const {
|
const { data: { results = [] } } = await OrganizationsAPI.readInstanceGroups(id);
|
||||||
data
|
this.setState({ instanceGroups: [...results] });
|
||||||
} = await OrganizationsAPI.readInstanceGroups(match.params.id);
|
|
||||||
this.setState({
|
|
||||||
instanceGroups: [...data.results]
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: true });
|
this.setState({ contentError: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ contentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
error,
|
contentLoading,
|
||||||
|
contentError,
|
||||||
instanceGroups,
|
instanceGroups,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
@ -65,6 +66,14 @@ class OrganizationDetail extends Component {
|
|||||||
i18n
|
i18n
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (contentLoading) {
|
||||||
|
return (<ContentLoading />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentError) {
|
||||||
|
return (<ContentError />);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList>
|
<DetailList>
|
||||||
@ -116,10 +125,9 @@ class OrganizationDetail extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error ? 'error!' : ''}
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(withRouter(withNetwork(OrganizationDetail)));
|
export default withI18n()(withRouter(OrganizationDetail));
|
||||||
|
@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom';
|
|||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@patternfly/react-core';
|
||||||
import OrganizationForm from '../../components/OrganizationForm';
|
import OrganizationForm from '../../components/OrganizationForm';
|
||||||
import { Config } from '../../../../contexts/Config';
|
import { Config } from '../../../../contexts/Config';
|
||||||
import { withNetwork } from '../../../../contexts/Network';
|
|
||||||
import { OrganizationsAPI } from '../../../../api';
|
import { OrganizationsAPI } from '../../../../api';
|
||||||
|
|
||||||
class OrganizationEdit extends Component {
|
class OrganizationEdit extends Component {
|
||||||
@ -22,13 +22,13 @@ class OrganizationEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
|
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
|
||||||
const { organization, handleHttpError } = this.props;
|
const { organization } = this.props;
|
||||||
try {
|
try {
|
||||||
await OrganizationsAPI.update(organization.id, values);
|
await OrganizationsAPI.update(organization.id, values);
|
||||||
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
|
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
|
||||||
this.handleSuccess();
|
this.handleSuccess();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,8 +43,7 @@ class OrganizationEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
|
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
|
||||||
const { organization, handleHttpError } = this.props;
|
const { organization } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupsToAssociate.map(id => OrganizationsAPI.associateInstanceGroup(organization.id, id))
|
groupsToAssociate.map(id => OrganizationsAPI.associateInstanceGroup(organization.id, id))
|
||||||
@ -55,7 +54,7 @@ class OrganizationEdit extends Component {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,4 +89,4 @@ OrganizationEdit.contextTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationEdit as _OrganizationEdit };
|
export { OrganizationEdit as _OrganizationEdit };
|
||||||
export default withNetwork(withRouter(OrganizationEdit));
|
export default withRouter(OrganizationEdit);
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { number, shape, func, string, bool } from 'prop-types';
|
import { number, shape, string, bool } from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withNetwork } from '../../../../contexts/Network';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import AlertModal from '../../../../components/AlertModal';
|
||||||
import PaginatedDataList from '../../../../components/PaginatedDataList';
|
import PaginatedDataList from '../../../../components/PaginatedDataList';
|
||||||
import NotificationListItem from '../../../../components/NotificationsList/NotificationListItem';
|
import NotificationListItem from '../../../../components/NotificationsList/NotificationListItem';
|
||||||
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
||||||
@ -22,176 +25,134 @@ const COLUMNS = [
|
|||||||
class OrganizationNotifications extends Component {
|
class OrganizationNotifications extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.readNotifications = this.readNotifications.bind(this);
|
|
||||||
this.readSuccessesAndErrors = this.readSuccessesAndErrors.bind(this);
|
|
||||||
this.toggleNotification = this.toggleNotification.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isInitialized: false,
|
contentError: false,
|
||||||
isLoading: false,
|
contentLoading: true,
|
||||||
error: null,
|
toggleError: false,
|
||||||
|
toggleLoading: false,
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
successTemplateIds: [],
|
successTemplateIds: [],
|
||||||
errorTemplateIds: [],
|
errorTemplateIds: [],
|
||||||
};
|
};
|
||||||
|
this.handleNotificationToggle = this.handleNotificationToggle.bind(this);
|
||||||
|
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(this);
|
||||||
|
this.loadNotifications = this.loadNotifications.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.readNotifications();
|
this.loadNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
this.readNotifications();
|
this.loadNotifications();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readNotifications () {
|
async loadNotifications () {
|
||||||
const { id, handleHttpError, location } = this.props;
|
const { id, location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
this.setState({ isLoading: true });
|
|
||||||
|
this.setState({ contentError: false, contentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await OrganizationsAPI.readNotificationTemplates(id, params);
|
const {
|
||||||
this.setState(
|
data: {
|
||||||
{
|
count: itemCount = 0,
|
||||||
itemCount: data.count || 0,
|
results: notifications = [],
|
||||||
notifications: data.results || [],
|
|
||||||
isLoading: false,
|
|
||||||
isInitialized: true,
|
|
||||||
},
|
|
||||||
this.readSuccessesAndErrors
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
handleHttpError(error) || this.setState({
|
|
||||||
error,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} = await OrganizationsAPI.readNotificationTemplates(id, params);
|
||||||
|
|
||||||
|
let idMatchParams;
|
||||||
|
if (notifications.length > 0) {
|
||||||
|
idMatchParams = { id__in: notifications.map(n => n.id).join(',') };
|
||||||
|
} else {
|
||||||
|
idMatchParams = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async readSuccessesAndErrors () {
|
const [
|
||||||
const { handleHttpError, id } = this.props;
|
{ data: successTemplates },
|
||||||
const { notifications } = this.state;
|
{ data: errorTemplates },
|
||||||
if (!notifications.length) {
|
] = await Promise.all([
|
||||||
return;
|
OrganizationsAPI.readNotificationTemplatesSuccess(id, idMatchParams),
|
||||||
}
|
OrganizationsAPI.readNotificationTemplatesError(id, idMatchParams),
|
||||||
const ids = notifications.map(n => n.id).join(',');
|
]);
|
||||||
try {
|
|
||||||
const successTemplatesPromise = OrganizationsAPI.readNotificationTemplatesSuccess(
|
|
||||||
id,
|
|
||||||
{ id__in: ids }
|
|
||||||
);
|
|
||||||
const errorTemplatesPromise = OrganizationsAPI.readNotificationTemplatesError(
|
|
||||||
id,
|
|
||||||
{ id__in: ids }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: successTemplates } = await successTemplatesPromise;
|
|
||||||
const { data: errorTemplates } = await errorTemplatesPromise;
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
itemCount,
|
||||||
|
notifications,
|
||||||
successTemplateIds: successTemplates.results.map(s => s.id),
|
successTemplateIds: successTemplates.results.map(s => s.id),
|
||||||
errorTemplateIds: errorTemplates.results.map(e => e.id),
|
errorTemplateIds: errorTemplates.results.map(e => e.id),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch {
|
||||||
handleHttpError(error) || this.setState({
|
this.setState({ contentError: true });
|
||||||
error,
|
} finally {
|
||||||
isLoading: false,
|
this.setState({ contentLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleNotificationToggle (notificationId, isCurrentlyOn, status) {
|
||||||
|
const { id } = this.props;
|
||||||
|
|
||||||
|
let stateArrayName;
|
||||||
|
if (status === 'success') {
|
||||||
|
stateArrayName = 'successTemplateIds';
|
||||||
|
} else {
|
||||||
|
stateArrayName = 'errorTemplateIds';
|
||||||
|
}
|
||||||
|
|
||||||
|
let stateUpdateFunction;
|
||||||
|
if (isCurrentlyOn) {
|
||||||
|
// when switching off, remove the toggled notification id from the array
|
||||||
|
stateUpdateFunction = (prevState) => ({
|
||||||
|
[stateArrayName]: prevState[stateArrayName].filter(i => i !== notificationId)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// when switching on, add the toggled notification id to the array
|
||||||
|
stateUpdateFunction = (prevState) => ({
|
||||||
|
[stateArrayName]: prevState[stateArrayName].concat(notificationId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toggleNotification = (notificationId, isCurrentlyOn, status) => {
|
this.setState({ toggleLoading: true });
|
||||||
if (status === 'success') {
|
|
||||||
if (isCurrentlyOn) {
|
|
||||||
this.disassociateSuccess(notificationId);
|
|
||||||
} else {
|
|
||||||
this.associateSuccess(notificationId);
|
|
||||||
}
|
|
||||||
} else if (status === 'error') {
|
|
||||||
if (isCurrentlyOn) {
|
|
||||||
this.disassociateError(notificationId);
|
|
||||||
} else {
|
|
||||||
this.associateError(notificationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async associateSuccess (notificationId) {
|
|
||||||
const { id, handleHttpError } = this.props;
|
|
||||||
try {
|
try {
|
||||||
await OrganizationsAPI.associateNotificationTemplatesSuccess(id, notificationId);
|
await OrganizationsAPI.updateNotificationTemplateAssociation(
|
||||||
this.setState(prevState => ({
|
id,
|
||||||
successTemplateIds: [...prevState.successTemplateIds, notificationId]
|
notificationId,
|
||||||
}));
|
status,
|
||||||
|
!isCurrentlyOn
|
||||||
|
);
|
||||||
|
this.setState(stateUpdateFunction);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: true });
|
this.setState({ toggleError: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ toggleLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async disassociateSuccess (notificationId) {
|
handleNotificationErrorClose () {
|
||||||
const { id, handleHttpError } = this.props;
|
this.setState({ toggleError: false });
|
||||||
try {
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { canToggleNotifications } = this.props;
|
const { canToggleNotifications, i18n } = this.props;
|
||||||
const {
|
const {
|
||||||
notifications,
|
contentError,
|
||||||
|
contentLoading,
|
||||||
|
toggleError,
|
||||||
|
toggleLoading,
|
||||||
itemCount,
|
itemCount,
|
||||||
isLoading,
|
notifications,
|
||||||
isInitialized,
|
|
||||||
error,
|
|
||||||
successTemplateIds,
|
successTemplateIds,
|
||||||
errorTemplateIds,
|
errorTemplateIds,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
// TODO: better error state
|
|
||||||
return <div>{error.message}</div>;
|
|
||||||
}
|
|
||||||
// TODO: better loading state
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{isLoading && (<div>Loading...</div>)}
|
|
||||||
{isInitialized && (
|
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
contentLoading={contentLoading}
|
||||||
items={notifications}
|
items={notifications}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
itemName="notification"
|
itemName="notification"
|
||||||
@ -202,14 +163,21 @@ class OrganizationNotifications extends Component {
|
|||||||
key={notification.id}
|
key={notification.id}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
detailUrl={`/notifications/${notification.id}`}
|
detailUrl={`/notifications/${notification.id}`}
|
||||||
canToggleNotifications={canToggleNotifications}
|
canToggleNotifications={canToggleNotifications && !toggleLoading}
|
||||||
toggleNotification={this.toggleNotification}
|
toggleNotification={this.handleNotificationToggle}
|
||||||
errorTurnedOn={errorTemplateIds.includes(notification.id)}
|
errorTurnedOn={errorTemplateIds.includes(notification.id)}
|
||||||
successTurnedOn={successTemplateIds.includes(notification.id)}
|
successTurnedOn={successTemplateIds.includes(notification.id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
<AlertModal
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
isOpen={toggleError && !toggleLoading}
|
||||||
|
onClose={this.handleNotificationErrorClose}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to toggle notification.`)}
|
||||||
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -218,11 +186,10 @@ class OrganizationNotifications extends Component {
|
|||||||
OrganizationNotifications.propTypes = {
|
OrganizationNotifications.propTypes = {
|
||||||
id: number.isRequired,
|
id: number.isRequired,
|
||||||
canToggleNotifications: bool.isRequired,
|
canToggleNotifications: bool.isRequired,
|
||||||
handleHttpError: func.isRequired,
|
|
||||||
location: shape({
|
location: shape({
|
||||||
search: string.isRequired,
|
search: string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationNotifications as _OrganizationNotifications };
|
export { OrganizationNotifications as _OrganizationNotifications };
|
||||||
export default withNetwork(withRouter(OrganizationNotifications));
|
export default withI18n()(withRouter(OrganizationNotifications));
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import PaginatedDataList from '../../../../components/PaginatedDataList';
|
import PaginatedDataList from '../../../../components/PaginatedDataList';
|
||||||
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
|
||||||
import { withNetwork } from '../../../../contexts/Network';
|
|
||||||
import { OrganizationsAPI } from '../../../../api';
|
import { OrganizationsAPI } from '../../../../api';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('team', {
|
const QS_CONFIG = getQSConfig('team', {
|
||||||
@ -16,32 +15,32 @@ class OrganizationTeams extends React.Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.readOrganizationTeamsList = this.readOrganizationTeamsList.bind(this);
|
this.loadOrganizationTeamsList = this.loadOrganizationTeamsList.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isInitialized: false,
|
contentError: false,
|
||||||
isLoading: false,
|
contentLoading: true,
|
||||||
error: null,
|
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
teams: [],
|
teams: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.readOrganizationTeamsList();
|
this.loadOrganizationTeamsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
this.readOrganizationTeamsList();
|
this.loadOrganizationTeamsList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readOrganizationTeamsList () {
|
async loadOrganizationTeamsList () {
|
||||||
const { id, handleHttpError, location } = this.props;
|
const { id, location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
this.setState({ isLoading: true, error: null });
|
|
||||||
|
this.setState({ contentLoading: true, contentError: false });
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { count = 0, results = [] },
|
data: { count = 0, results = [] },
|
||||||
@ -49,38 +48,25 @@ class OrganizationTeams extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
teams: results,
|
teams: results,
|
||||||
isLoading: false,
|
|
||||||
isInitialized: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleHttpError(error) || this.setState({
|
|
||||||
error,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
this.setState({ contentError: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ contentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { teams, itemCount, isLoading, isInitialized, error } = this.state;
|
const { contentError, contentLoading, teams, itemCount } = this.state;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
// TODO: better error state
|
|
||||||
return <div>{error.message}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: better loading state
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
{isLoading && (<div>Loading...</div>)}
|
|
||||||
{isInitialized && (
|
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
contentLoading={contentLoading}
|
||||||
items={teams}
|
items={teams}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
itemName="team"
|
itemName="team"
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,4 +76,4 @@ OrganizationTeams.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationTeams as _OrganizationTeams };
|
export { OrganizationTeams as _OrganizationTeams };
|
||||||
export default withNetwork(withRouter(OrganizationTeams));
|
export default withRouter(OrganizationTeams);
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { Config } from '../../../contexts/Config';
|
import { Config } from '../../../contexts/Config';
|
||||||
import { withNetwork } from '../../../contexts/Network';
|
|
||||||
import CardCloseButton from '../../../components/CardCloseButton';
|
import CardCloseButton from '../../../components/CardCloseButton';
|
||||||
import OrganizationForm from '../components/OrganizationForm';
|
import OrganizationForm from '../components/OrganizationForm';
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI } from '../../../api';
|
||||||
@ -20,29 +19,20 @@ import { OrganizationsAPI } from '../../../api';
|
|||||||
class OrganizationAdd extends React.Component {
|
class OrganizationAdd extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleCancel = this.handleCancel.bind(this);
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
this.handleSuccess = this.handleSuccess.bind(this);
|
this.state = { error: '' };
|
||||||
|
|
||||||
this.state = {
|
|
||||||
error: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit (values, groupsToAssociate) {
|
async handleSubmit (values, groupsToAssociate) {
|
||||||
const { handleHttpError } = this.props;
|
const { history } = this.props;
|
||||||
try {
|
try {
|
||||||
const { data: response } = await OrganizationsAPI.create(values);
|
const { data: response } = await OrganizationsAPI.create(values);
|
||||||
try {
|
|
||||||
await Promise.all(groupsToAssociate.map(id => OrganizationsAPI
|
await Promise.all(groupsToAssociate.map(id => OrganizationsAPI
|
||||||
.associateInstanceGroup(response.id, id)));
|
.associateInstanceGroup(response.id, id)));
|
||||||
this.handleSuccess(response.id);
|
history.push(`/organizations/${response.id}`);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
handleHttpError(err) || this.setState({ error: err });
|
this.setState({ error });
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ error: err });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,11 +41,6 @@ class OrganizationAdd extends React.Component {
|
|||||||
history.push('/organizations');
|
history.push('/organizations');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSuccess (id) {
|
|
||||||
const { history } = this.props;
|
|
||||||
history.push(`/organizations/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { error } = this.state;
|
const { error } = this.state;
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
@ -94,4 +79,4 @@ OrganizationAdd.contextTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationAdd as _OrganizationAdd };
|
export { OrganizationAdd as _OrganizationAdd };
|
||||||
export default withI18n()(withNetwork(withRouter(OrganizationAdd)));
|
export default withI18n()(withRouter(OrganizationAdd));
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@ -8,13 +8,13 @@ import {
|
|||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { withNetwork } from '../../../contexts/Network';
|
|
||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
ToolbarAddButton
|
ToolbarAddButton
|
||||||
} from '../../../components/PaginatedDataList';
|
} from '../../../components/PaginatedDataList';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import OrganizationListItem from '../components/OrganizationListItem';
|
import OrganizationListItem from '../components/OrganizationListItem';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import { getQSConfig, parseNamespacedQueryString } from '../../../util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '../../../util/qs';
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI } from '../../../api';
|
||||||
|
|
||||||
@ -29,29 +29,30 @@ class OrganizationsList extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
error: null,
|
contentLoading: true,
|
||||||
isLoading: true,
|
contentError: false,
|
||||||
isInitialized: false,
|
deletionError: false,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
selected: []
|
selected: [],
|
||||||
|
itemCount: 0,
|
||||||
|
actions: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||||
this.handleSelect = this.handleSelect.bind(this);
|
this.handleSelect = this.handleSelect.bind(this);
|
||||||
this.fetchOptionsOrganizations = this.fetchOptionsOrganizations.bind(this);
|
|
||||||
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
|
||||||
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
||||||
|
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
|
||||||
|
this.loadOrganizations = this.loadOrganizations.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.fetchOptionsOrganizations();
|
this.loadOrganizations();
|
||||||
this.fetchOrganizations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
this.fetchOrganizations();
|
this.loadOrganizations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,63 +73,54 @@ class OrganizationsList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDeleteErrorClose () {
|
||||||
|
this.setState({ deletionError: false });
|
||||||
|
}
|
||||||
|
|
||||||
async handleOrgDelete () {
|
async handleOrgDelete () {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
const { handleHttpError } = this.props;
|
|
||||||
let errorHandled;
|
|
||||||
|
|
||||||
|
this.setState({ contentLoading: true, deletionError: false });
|
||||||
try {
|
try {
|
||||||
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
|
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
|
||||||
this.setState({
|
this.setState({ selected: [] });
|
||||||
selected: []
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorHandled = handleHttpError(err);
|
this.setState({ deletionError: true });
|
||||||
} finally {
|
} finally {
|
||||||
if (!errorHandled) {
|
await this.loadOrganizations();
|
||||||
this.fetchOrganizations();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrganizations () {
|
async loadOrganizations () {
|
||||||
const { handleHttpError, location } = this.props;
|
const { location } = this.props;
|
||||||
|
const { actions: cachedActions } = this.state;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
this.setState({ error: false, isLoading: true });
|
let optionsPromise;
|
||||||
|
if (cachedActions) {
|
||||||
|
optionsPromise = Promise.resolve({ data: { actions: cachedActions } });
|
||||||
|
} else {
|
||||||
|
optionsPromise = OrganizationsAPI.readOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = Promise.all([
|
||||||
|
OrganizationsAPI.read(params),
|
||||||
|
optionsPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.setState({ contentError: false, contentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await OrganizationsAPI.read(params);
|
const [{ data: { count, results } }, { data: { actions } }] = await promises;
|
||||||
const { count, results } = data;
|
this.setState({
|
||||||
|
actions,
|
||||||
const stateToUpdate = {
|
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
organizations: results,
|
organizations: results,
|
||||||
selected: [],
|
selected: [],
|
||||||
isLoading: false,
|
});
|
||||||
isInitialized: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(stateToUpdate);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: true, isLoading: false });
|
this.setState(({ contentError: true }));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchOptionsOrganizations () {
|
|
||||||
try {
|
|
||||||
const { data } = await OrganizationsAPI.readOptions();
|
|
||||||
const { actions } = data;
|
|
||||||
|
|
||||||
const stateToUpdate = {
|
|
||||||
canAdd: Object.prototype.hasOwnProperty.call(actions, 'POST')
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(stateToUpdate);
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ error: true });
|
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ isLoading: false });
|
this.setState({ contentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,23 +129,26 @@ class OrganizationsList extends Component {
|
|||||||
medium,
|
medium,
|
||||||
} = PageSectionVariants;
|
} = PageSectionVariants;
|
||||||
const {
|
const {
|
||||||
canAdd,
|
actions,
|
||||||
itemCount,
|
itemCount,
|
||||||
error,
|
contentError,
|
||||||
isLoading,
|
contentLoading,
|
||||||
isInitialized,
|
deletionError,
|
||||||
selected,
|
selected,
|
||||||
organizations
|
organizations,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { match, i18n } = this.props;
|
const { match, i18n } = this.props;
|
||||||
|
|
||||||
|
const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
const isAllSelected = selected.length === organizations.length;
|
const isAllSelected = selected.length === organizations.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Card>
|
<Card>
|
||||||
{isInitialized && (
|
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
contentLoading={contentLoading}
|
||||||
items={organizations}
|
items={organizations}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
itemName="organization"
|
itemName="organization"
|
||||||
@ -196,14 +191,20 @@ class OrganizationsList extends Component {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{ isLoading ? <div>loading...</div> : '' }
|
|
||||||
{ error ? <div>error</div> : '' }
|
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={this.handleDeleteErrorClose}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more organizations.`)}
|
||||||
|
</AlertModal>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OrganizationsList as _OrganizationsList };
|
export { OrganizationsList as _OrganizationsList };
|
||||||
export default withI18n()(withNetwork(withRouter(OrganizationsList)));
|
export default withI18n()(withRouter(OrganizationsList));
|
||||||
|
@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Route, withRouter, Switch } from 'react-router-dom';
|
import { Route, withRouter, Switch } from 'react-router-dom';
|
||||||
import { NetworkProvider } from '../../contexts/Network';
|
|
||||||
import { withRootDialog } from '../../contexts/RootDialog';
|
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||||
import TemplatesList from './TemplatesList';
|
import TemplatesList from './TemplatesList';
|
||||||
@ -21,32 +19,12 @@ class Templates extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { match, history, setRootDialogMessage, i18n } = this.props;
|
const { match } = this.props;
|
||||||
const { breadcrumbConfig } = this.state;
|
const { breadcrumbConfig } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
|
||||||
path={`${match.path}/:templateType/:id`}
|
|
||||||
render={({ match: newRouteMatch }) => (
|
|
||||||
<NetworkProvider
|
|
||||||
handle404={() => {
|
|
||||||
history.replace('/templates');
|
|
||||||
setRootDialogMessage({
|
|
||||||
title: '404',
|
|
||||||
bodyText: (
|
|
||||||
<Fragment>
|
|
||||||
{i18n._(t`Cannot find template with ID`)}
|
|
||||||
<strong>{` ${newRouteMatch.params.id}`}</strong>
|
|
||||||
</Fragment>
|
|
||||||
),
|
|
||||||
variant: 'warning'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}`}
|
path={`${match.path}`}
|
||||||
render={() => (
|
render={() => (
|
||||||
@ -60,4 +38,4 @@ class Templates extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { Templates as _Templates };
|
export { Templates as _Templates };
|
||||||
export default withI18n()(withRootDialog(withRouter(Templates)));
|
export default withI18n()(withRouter(Templates));
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
PageSection,
|
PageSection,
|
||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { withNetwork } from '../../contexts/Network';
|
|
||||||
import { UnifiedJobTemplatesAPI } from '../../api';
|
import { UnifiedJobTemplatesAPI } from '../../api';
|
||||||
|
|
||||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||||
@ -29,25 +28,25 @@ class TemplatesList extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
error: null,
|
contentError: false,
|
||||||
isLoading: true,
|
contentLoading: true,
|
||||||
isInitialized: false,
|
|
||||||
selected: [],
|
selected: [],
|
||||||
templates: [],
|
templates: [],
|
||||||
|
itemCount: 0,
|
||||||
};
|
};
|
||||||
this.readUnifiedJobTemplates = this.readUnifiedJobTemplates.bind(this);
|
this.loadUnifiedJobTemplates = this.loadUnifiedJobTemplates.bind(this);
|
||||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||||
this.handleSelect = this.handleSelect.bind(this);
|
this.handleSelect = this.handleSelect.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.readUnifiedJobTemplates();
|
this.loadUnifiedJobTemplates();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
this.readUnifiedJobTemplates();
|
this.loadUnifiedJobTemplates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,33 +65,29 @@ class TemplatesList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readUnifiedJobTemplates () {
|
async loadUnifiedJobTemplates () {
|
||||||
const { handleHttpError, location } = this.props;
|
const { location } = this.props;
|
||||||
this.setState({ error: false, isLoading: true });
|
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
|
this.setState({ contentError: false, contentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await UnifiedJobTemplatesAPI.read(params);
|
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params);
|
||||||
const { count, results } = data;
|
this.setState({
|
||||||
|
|
||||||
const stateToUpdate = {
|
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
templates: results,
|
templates: results,
|
||||||
selected: [],
|
selected: [],
|
||||||
isInitialized: true,
|
});
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
this.setState(stateToUpdate);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleHttpError(err) || this.setState({ error: true, isLoading: false });
|
this.setState({ contentError: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ contentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
error,
|
contentError,
|
||||||
isInitialized,
|
contentLoading,
|
||||||
isLoading,
|
|
||||||
templates,
|
templates,
|
||||||
itemCount,
|
itemCount,
|
||||||
selected,
|
selected,
|
||||||
@ -106,8 +101,9 @@ class TemplatesList extends Component {
|
|||||||
return (
|
return (
|
||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Card>
|
<Card>
|
||||||
{isInitialized && (
|
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
contentLoading={contentLoading}
|
||||||
items={templates}
|
items={templates}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
itemName={i18n._(t`Template`)}
|
itemName={i18n._(t`Template`)}
|
||||||
@ -137,13 +133,11 @@ class TemplatesList extends Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isLoading ? <div>loading....</div> : ''}
|
|
||||||
{error ? <div>error</div> : '' }
|
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TemplatesList as _TemplatesList };
|
export { TemplatesList as _TemplatesList };
|
||||||
export default withI18n()(withNetwork(withRouter(TemplatesList)));
|
export default withI18n()(withRouter(TemplatesList));
|
||||||
|
8
src/util/auth.js
Normal file
8
src/util/auth.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export function isAuthenticated () {
|
||||||
|
const parsed = (`; ${document.cookie}`).split('; userLoggedIn=');
|
||||||
|
if (parsed.length === 2) {
|
||||||
|
return parsed.pop().split(';').shift() === 'true';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user