mirror of
https://github.com/ansible/awx.git
synced 2026-05-23 16:47:45 -02:30
Merge pull request #156 from jlmitch5/handleNetworkErrors
Handle network errors
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"object-curly-newline": "off",
|
"object-curly-newline": "off",
|
||||||
"space-before-function-paren": ["error", "always"],
|
"space-before-function-paren": ["error", "always"],
|
||||||
"no-trailing-spaces": ["error"],
|
"no-trailing-spaces": ["error"],
|
||||||
|
"no-unused-expressions": ["error", { "allowShortCircuit": true }],
|
||||||
"react/prefer-stateless-function": "off",
|
"react/prefer-stateless-function": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"jsx-a11y/label-has-for": "off",
|
"jsx-a11y/label-has-for": "off",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ 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)
|
||||||
* [Working with React](#working-with-react)
|
* [Working with React](#working-with-react)
|
||||||
|
* [App structure](#app-structure)
|
||||||
|
* [Naming files](#naming-files)
|
||||||
* [Class constructors vs Class properties](#class-constructors-vs-class-properties)
|
* [Class constructors vs Class properties](#class-constructors-vs-class-properties)
|
||||||
* [Binding](#binding)
|
* [Binding](#binding)
|
||||||
* [Typechecking with PropTypes](#typechecking-with-proptypes)
|
* [Typechecking with PropTypes](#typechecking-with-proptypes)
|
||||||
@@ -56,6 +58,50 @@ Run the following to build the AWX UI:
|
|||||||
You can now log into the AWX web interface at [https://127.0.0.1:3001](https://127.0.0.1:3001).
|
You can now log into the AWX web interface at [https://127.0.0.1:3001](https://127.0.0.1:3001).
|
||||||
|
|
||||||
## Working with React
|
## Working with React
|
||||||
|
|
||||||
|
### App structure
|
||||||
|
|
||||||
|
All source code lives in the `/src` directory and all tests live in the `/__tests__` directory (mimicing the internal structure of `/src`).
|
||||||
|
|
||||||
|
Inside these folders, the internal structure is:
|
||||||
|
- **/components** - All generic components that are meant to be used in multiple contexts throughout awx. Things like buttons, tabs go here.
|
||||||
|
- **/contexts** - Components which utilize react's context api.
|
||||||
|
- **/pages** - Based on the various routes of awx.
|
||||||
|
- **/components** - Components that are meant to be used specifically by a particular route, but might be sharable across pages of that route. For example, a form component which is used on both add and edit screens.
|
||||||
|
- **/screens** - Individual pages of the route, such as add, edit, list, related lists, etc.
|
||||||
|
- **/util** - Stateless helper functions that aren't tied to react.
|
||||||
|
|
||||||
|
#### Bootstrapping the application (root src/ files)
|
||||||
|
|
||||||
|
In the root of `/src`, there are a few files which are used to initialize the react app. These are
|
||||||
|
|
||||||
|
- **index.jsx**
|
||||||
|
- Connects react app to root dom node.
|
||||||
|
- Sets up root route structure, navigation grouping and login modal
|
||||||
|
- Calls base context providers
|
||||||
|
- Imports .scss styles.
|
||||||
|
- **app.jsx**
|
||||||
|
- Sets standard page layout, about modal, and root dialog modal.
|
||||||
|
- **RootProvider.jsx**
|
||||||
|
- Sets up all context providers.
|
||||||
|
- Initializes i18n and router
|
||||||
|
|
||||||
|
### Naming files
|
||||||
|
|
||||||
|
Ideally, files should be named the same as the component they export, and tests with `.test` appended. In other words, `<FooBar>` would be defined in `FooBar.jsx`, and its tests would be defined in `FooBar.test.jsx`.
|
||||||
|
|
||||||
|
#### Naming components that use the context api
|
||||||
|
|
||||||
|
**File naming** - Since contexts export both consumer and provider (and potentially in withContext function form), the file can be simplified to be named after the consumer export. In other words, the file containing the `Network` context components would be named `Network.jsx`.
|
||||||
|
|
||||||
|
**Component naming and conventions** - In order to provide a consistent interface with react-router and lingui, as well as make their usage easier and less verbose, context components follow these conventions:
|
||||||
|
- Providers are wrapped in a component in the `FooProvider` format.
|
||||||
|
- The value prop of the provider should be pulled from state. This is recommended by the react docs, [here](https://reactjs.org/docs/context.html#caveats).
|
||||||
|
- The provider should also be able to accept its value by prop for testing.
|
||||||
|
- Any sort of code related to grabbing data to put on the context should be done in this component.
|
||||||
|
- Consumers are wrapped in a component in the `Foo` format.
|
||||||
|
- If it makes sense, consumers can be exported as a function in the `withFoo()` format. If a component is wrapped in this function, its context values are available on the component as props.
|
||||||
|
|
||||||
### Class constructors vs Class properties
|
### Class constructors vs Class properties
|
||||||
It is good practice to use constructor-bound instance methods rather than methods as class properties. Methods as arrow functions provide lexical scope and are bound to the Component class instance instead of the class itself. This makes it so we cannot easily test a Component's methods without invoking an instance of the Component and calling the method directly within our tests.
|
It is good practice to use constructor-bound instance methods rather than methods as class properties. Methods as arrow functions provide lexical scope and are bound to the Component class instance instead of the class itself. This makes it so we cannot easily test a Component's methods without invoking an instance of the Component and calling the method directly within our tests.
|
||||||
|
|
||||||
|
|||||||
@@ -2,38 +2,47 @@ import React from 'react';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { asyncFlush } from '../jest.setup';
|
import { asyncFlush } from '../jest.setup';
|
||||||
|
|
||||||
import App from '../src/App';
|
import { ConfigProvider } from '../src/contexts/Config';
|
||||||
|
import { NetworkProvider } from '../src/contexts/Network';
|
||||||
|
|
||||||
|
import App, { _App } from '../src/App';
|
||||||
|
|
||||||
|
const networkProviderValue = { api: {}, handleHttpError: () => {} };
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
test('expected content is rendered', () => {
|
test('expected content is rendered', () => {
|
||||||
const appWrapper = mount(
|
const appWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<App
|
<NetworkProvider value={networkProviderValue}>
|
||||||
routeGroups={[
|
<ConfigProvider>
|
||||||
{
|
<App
|
||||||
groupTitle: 'Group One',
|
routeGroups={[
|
||||||
groupId: 'group_one',
|
{
|
||||||
routes: [
|
groupTitle: 'Group One',
|
||||||
{ title: 'Foo', path: '/foo' },
|
groupId: 'group_one',
|
||||||
{ title: 'Bar', path: '/bar' },
|
routes: [
|
||||||
],
|
{ title: 'Foo', path: '/foo' },
|
||||||
},
|
{ title: 'Bar', path: '/bar' },
|
||||||
{
|
],
|
||||||
groupTitle: 'Group Two',
|
},
|
||||||
groupId: 'group_two',
|
{
|
||||||
routes: [
|
groupTitle: 'Group Two',
|
||||||
{ title: 'Fiz', path: '/fiz' },
|
groupId: 'group_two',
|
||||||
]
|
routes: [
|
||||||
}
|
{ title: 'Fiz', path: '/fiz' },
|
||||||
]}
|
]
|
||||||
render={({ routeGroups }) => (
|
}
|
||||||
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
|
]}
|
||||||
)}
|
render={({ routeGroups }) => (
|
||||||
/>
|
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -58,20 +67,20 @@ describe('<App />', () => {
|
|||||||
const ansible_version = '111';
|
const ansible_version = '111';
|
||||||
const version = '222';
|
const version = '222';
|
||||||
|
|
||||||
const getConfig = jest.fn(() => Promise.resolve({ data: { ansible_version, version } }));
|
const config = { ansible_version, version };
|
||||||
const api = { getConfig };
|
|
||||||
|
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<App api={api} />
|
<NetworkProvider value={networkProviderValue}>
|
||||||
|
<ConfigProvider value={config}>
|
||||||
|
<App />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
await asyncFlush();
|
|
||||||
expect(getConfig).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
// open about modal
|
// open about modal
|
||||||
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
const aboutDropdown = 'Dropdown QuestionCircleIcon';
|
||||||
const aboutButton = 'DropdownItem li button';
|
const aboutButton = 'DropdownItem li button';
|
||||||
@@ -97,7 +106,17 @@ describe('<App />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
test('onNavToggle sets state.isNavOpen to opposite', () => {
|
||||||
const appWrapper = shallow(<App />);
|
const appWrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<NetworkProvider value={networkProviderValue}>
|
||||||
|
<ConfigProvider>
|
||||||
|
<App />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
).find('App');
|
||||||
const { onNavToggle } = appWrapper.instance();
|
const { onNavToggle } = appWrapper.instance();
|
||||||
|
|
||||||
[true, false, true, false, true].forEach(expected => {
|
[true, false, true, false, true].forEach(expected => {
|
||||||
@@ -108,13 +127,22 @@ describe('<App />', () => {
|
|||||||
|
|
||||||
test('onLogout makes expected call to api client', async (done) => {
|
test('onLogout makes expected call to api client', async (done) => {
|
||||||
const logout = jest.fn(() => Promise.resolve());
|
const logout = jest.fn(() => Promise.resolve());
|
||||||
const api = { logout };
|
|
||||||
|
|
||||||
const appWrapper = shallow(<App api={api} />);
|
const appWrapper = mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<NetworkProvider value={networkProviderValue}>
|
||||||
|
<ConfigProvider>
|
||||||
|
<_App api={{ logout }} handleHttpError={() => {}} />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
).find('App');
|
||||||
|
|
||||||
appWrapper.instance().onLogout();
|
appWrapper.instance().onLogout();
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
expect(api.logout).toHaveBeenCalledTimes(1);
|
expect(logout).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
10
__tests__/RootProvider.test.jsx
Normal file
10
__tests__/RootProvider.test.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { getLanguage } from '../src/RootProvider';
|
||||||
|
|
||||||
|
describe('RootProvider.jsx', () => {
|
||||||
|
test('getLanguage returns the expected language code', () => {
|
||||||
|
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
|
||||||
|
expect(getLanguage({ languages: ['es-US'], language: 'fr-FR', userLanguage: 'en-US' })).toEqual('es');
|
||||||
|
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual('fr');
|
||||||
|
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import Lookup from '../../src/components/Lookup';
|
import Lookup from '../../src/components/Lookup';
|
||||||
|
import { _Lookup } from '../../src/components/Lookup/Lookup';
|
||||||
|
|
||||||
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
|
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
|
||||||
const mockColumns = [
|
const mockColumns = [
|
||||||
@@ -11,7 +12,7 @@ describe('<Lookup />', () => {
|
|||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mount(
|
mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockData}
|
value={mockData}
|
||||||
@@ -19,14 +20,16 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('API response is formatted properly', (done) => {
|
test('API response is formatted properly', (done) => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockData}
|
value={mockData}
|
||||||
@@ -34,6 +37,7 @@ 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={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
@@ -43,12 +47,13 @@ describe('<Lookup />', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Opens modal when search icon is clicked', () => {
|
test('Opens modal when search icon is clicked', () => {
|
||||||
const spy = jest.spyOn(Lookup.prototype, 'handleModalToggle');
|
const spy = jest.spyOn(_Lookup.prototype, 'handleModalToggle');
|
||||||
const mockSelected = [{ name: 'foo', id: 1 }];
|
const mockSelected = [{ name: 'foo', id: 1 }];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
id="search"
|
id="search"
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
@@ -57,6 +62,7 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
@@ -71,12 +77,13 @@ describe('<Lookup />', () => {
|
|||||||
}]);
|
}]);
|
||||||
expect(wrapper.state('isModalOpen')).toEqual(true);
|
expect(wrapper.state('isModalOpen')).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls "toggleSelected" when a user changes a checkbox', (done) => {
|
test('calls "toggleSelected" when a user changes a checkbox', (done) => {
|
||||||
const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
|
const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
|
||||||
const mockSelected = [{ name: 'foo', id: 1 }];
|
const mockSelected = [{ name: 'foo', id: 1 }];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
id="search"
|
id="search"
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
@@ -85,6 +92,7 @@ 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={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
@@ -96,12 +104,13 @@ describe('<Lookup />', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls "toggleSelected" when remove icon is clicked', () => {
|
test('calls "toggleSelected" when remove icon is clicked', () => {
|
||||||
const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
|
const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
|
||||||
mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
|
mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
id="search"
|
id="search"
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
@@ -110,6 +119,7 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
@@ -117,6 +127,7 @@ describe('<Lookup />', () => {
|
|||||||
removeIcon.simulate('click');
|
removeIcon.simulate('click');
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders chips from prop value', () => {
|
test('renders chips from prop value', () => {
|
||||||
mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }];
|
mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
@@ -129,12 +140,14 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
const chip = wrapper.find('li.pf-c-chip');
|
const chip = wrapper.find('li.pf-c-chip');
|
||||||
expect(chip).toHaveLength(2);
|
expect(chip).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => {
|
test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => {
|
||||||
mockData = [];
|
mockData = [];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
@@ -146,6 +159,7 @@ describe('<Lookup />', () => {
|
|||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
@@ -163,6 +177,7 @@ describe('<Lookup />', () => {
|
|||||||
});
|
});
|
||||||
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
|
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('saveModal calls callback with selected items', () => {
|
test('saveModal calls callback with selected items', () => {
|
||||||
mockData = [];
|
mockData = [];
|
||||||
const onLookupSaveFn = jest.fn();
|
const onLookupSaveFn = jest.fn();
|
||||||
@@ -174,6 +189,7 @@ describe('<Lookup />', () => {
|
|||||||
value={mockData}
|
value={mockData}
|
||||||
onLookupSave={onLookupSaveFn}
|
onLookupSave={onLookupSaveFn}
|
||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
@@ -191,11 +207,12 @@ describe('<Lookup />', () => {
|
|||||||
name: 'foo'
|
name: 'foo'
|
||||||
}], 'fooBar');
|
}], 'fooBar');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('onSort sets state and calls getData ', () => {
|
test('onSort sets state and calls getData ', () => {
|
||||||
const spy = jest.spyOn(Lookup.prototype, 'getData');
|
const spy = jest.spyOn(_Lookup.prototype, 'getData');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => { }}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
@@ -203,6 +220,7 @@ describe('<Lookup />', () => {
|
|||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
@@ -211,11 +229,12 @@ describe('<Lookup />', () => {
|
|||||||
expect(wrapper.state('sortOrder')).toEqual('descending');
|
expect(wrapper.state('sortOrder')).toEqual('descending');
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
test('onSetPage sets state and calls getData ', () => {
|
|
||||||
const spy = jest.spyOn(Lookup.prototype, 'getData');
|
test('onSearch calls getData (through calling onSort)', () => {
|
||||||
|
const spy = jest.spyOn(_Lookup.prototype, 'getData');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<Lookup
|
<_Lookup
|
||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => { }}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
@@ -223,6 +242,27 @@ describe('<Lookup />', () => {
|
|||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
|
handleHttpError={() => {}}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
).find('Lookup');
|
||||||
|
wrapper.instance().onSearch();
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onSetPage sets state and calls getData ', () => {
|
||||||
|
const spy = jest.spyOn(_Lookup.prototype, 'getData');
|
||||||
|
const wrapper = mount(
|
||||||
|
<I18nProvider>
|
||||||
|
<_Lookup
|
||||||
|
lookup_header="Foo Bar"
|
||||||
|
onLookupSave={() => { }}
|
||||||
|
value={mockData}
|
||||||
|
selected={[]}
|
||||||
|
columns={mockColumns}
|
||||||
|
sortedColumnKey="name"
|
||||||
|
getItems={() => { }}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import Notifications from '../../src/components/NotificationsList/Notifications.list';
|
import Notifications, { _Notifications } from '../../src/components/NotificationsList/Notifications.list';
|
||||||
|
|
||||||
describe('<Notifications />', () => {
|
describe('<Notifications />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
@@ -17,50 +17,52 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={jest.fn()}
|
onReadSuccess={jest.fn()}
|
||||||
onCreateError={jest.fn()}
|
onCreateError={jest.fn()}
|
||||||
onCreateSuccess={jest.fn()}
|
onCreateSuccess={jest.fn()}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetches notifications on mount', () => {
|
test('fetches notifications on mount', () => {
|
||||||
const spy = jest.spyOn(Notifications.prototype, 'readNotifications');
|
const spy = jest.spyOn(_Notifications.prototype, 'readNotifications');
|
||||||
mount(
|
mount(
|
||||||
<MemoryRouter>
|
<I18nProvider>
|
||||||
<I18nProvider>
|
<_Notifications
|
||||||
<Notifications
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
onReadError={jest.fn()}
|
||||||
onReadError={jest.fn()}
|
onReadNotifications={jest.fn()}
|
||||||
onReadNotifications={jest.fn()}
|
onReadSuccess={jest.fn()}
|
||||||
onReadSuccess={jest.fn()}
|
onCreateError={jest.fn()}
|
||||||
onCreateError={jest.fn()}
|
onCreateSuccess={jest.fn()}
|
||||||
onCreateSuccess={jest.fn()}
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toggle success calls post', () => {
|
test('toggle success calls post', () => {
|
||||||
const spy = jest.spyOn(Notifications.prototype, 'createSuccess');
|
const spy = jest.spyOn(_Notifications.prototype, 'createSuccess');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<I18nProvider>
|
||||||
<I18nProvider>
|
<_Notifications
|
||||||
<Notifications
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
onReadError={jest.fn()}
|
||||||
onReadError={jest.fn()}
|
onReadNotifications={jest.fn()}
|
||||||
onReadNotifications={jest.fn()}
|
onReadSuccess={jest.fn()}
|
||||||
onReadSuccess={jest.fn()}
|
onCreateError={jest.fn()}
|
||||||
onCreateError={jest.fn()}
|
onCreateSuccess={jest.fn()}
|
||||||
onCreateSuccess={jest.fn()}
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.instance().toggleNotification(1, true, 'success');
|
wrapper.instance().toggleNotification(1, true, 'success');
|
||||||
expect(spy).toHaveBeenCalledWith(1, true);
|
expect(spy).toHaveBeenCalledWith(1, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('post success makes request and updates state properly', async () => {
|
test('post success makes request and updates state properly', async () => {
|
||||||
const createSuccess = jest.fn();
|
const createSuccess = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
@@ -74,6 +76,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={jest.fn()}
|
onReadSuccess={jest.fn()}
|
||||||
onCreateError={jest.fn()}
|
onCreateError={jest.fn()}
|
||||||
onCreateSuccess={createSuccess}
|
onCreateSuccess={createSuccess}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -86,26 +89,27 @@ describe('<Notifications />', () => {
|
|||||||
expect(createSuccess).toHaveBeenCalledWith(1, { id: 44 });
|
expect(createSuccess).toHaveBeenCalledWith(1, { id: 44 });
|
||||||
expect(wrapper.state('successTemplateIds')).toContain(44);
|
expect(wrapper.state('successTemplateIds')).toContain(44);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toggle error calls post', () => {
|
test('toggle error calls post', () => {
|
||||||
const spy = jest.spyOn(Notifications.prototype, 'createError');
|
const spy = jest.spyOn(_Notifications.prototype, 'createError');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<I18nProvider>
|
||||||
<I18nProvider>
|
<_Notifications
|
||||||
<Notifications
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
onReadError={jest.fn()}
|
||||||
onReadError={jest.fn()}
|
onReadNotifications={jest.fn()}
|
||||||
onReadNotifications={jest.fn()}
|
onReadSuccess={jest.fn()}
|
||||||
onReadSuccess={jest.fn()}
|
onCreateError={jest.fn()}
|
||||||
onCreateError={jest.fn()}
|
onCreateSuccess={jest.fn()}
|
||||||
onCreateSuccess={jest.fn()}
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.instance().toggleNotification(1, true, 'error');
|
wrapper.instance().toggleNotification(1, true, 'error');
|
||||||
expect(spy).toHaveBeenCalledWith(1, true);
|
expect(spy).toHaveBeenCalledWith(1, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('post error makes request and updates state properly', async () => {
|
test('post error makes request and updates state properly', async () => {
|
||||||
const createError = jest.fn();
|
const createError = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
@@ -119,6 +123,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={jest.fn()}
|
onReadSuccess={jest.fn()}
|
||||||
onCreateError={createError}
|
onCreateError={createError}
|
||||||
onCreateSuccess={jest.fn()}
|
onCreateSuccess={jest.fn()}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -131,6 +136,7 @@ describe('<Notifications />', () => {
|
|||||||
expect(createError).toHaveBeenCalledWith(1, { id: 44 });
|
expect(createError).toHaveBeenCalledWith(1, { id: 44 });
|
||||||
expect(wrapper.state('errorTemplateIds')).toContain(44);
|
expect(wrapper.state('errorTemplateIds')).toContain(44);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetchNotifications', async () => {
|
test('fetchNotifications', async () => {
|
||||||
const mockQueryParams = {
|
const mockQueryParams = {
|
||||||
page: 44,
|
page: 44,
|
||||||
@@ -171,6 +177,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadError={readError}
|
onReadError={readError}
|
||||||
onCreateError={jest.fn()}
|
onCreateError={jest.fn()}
|
||||||
onCreateSuccess={jest.fn()}
|
onCreateSuccess={jest.fn()}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
|||||||
25
__tests__/components/NotifyAndRedirect.test.jsx
Normal file
25
__tests__/components/NotifyAndRedirect.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
|
||||||
|
import { _NotifyAndRedirect } from '../../src/components/NotifyAndRedirect';
|
||||||
|
|
||||||
|
describe('<NotifyAndRedirect />', () => {
|
||||||
|
test('initially renders succesfully and calls setRootDialogMessage', () => {
|
||||||
|
const setRootDialogMessage = jest.fn();
|
||||||
|
mount(
|
||||||
|
<MemoryRouter>
|
||||||
|
<I18nProvider>
|
||||||
|
<_NotifyAndRedirect
|
||||||
|
to="foo"
|
||||||
|
setRootDialogMessage={setRootDialogMessage}
|
||||||
|
location={{ pathname: 'foo' }}
|
||||||
|
/>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(setRootDialogMessage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,51 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { main, getLanguage } from '../src/index';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { main } from '../src/index';
|
||||||
|
|
||||||
const render = template => mount(template);
|
const render = template => mount(
|
||||||
const data = { custom_logo: 'foo', custom_login_info: '' };
|
<MemoryRouter>
|
||||||
|
{template}
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
describe('index.jsx', () => {
|
describe('index.jsx', () => {
|
||||||
test('login loads when unauthenticated', async (done) => {
|
test('index.jsx loads without issue', () => {
|
||||||
const isAuthenticated = () => false;
|
const wrapper = main(render);
|
||||||
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
expect(wrapper.find('RootProvider')).toHaveLength(1);
|
||||||
|
|
||||||
const api = { getRoot, isAuthenticated };
|
|
||||||
const wrapper = await main(render, api);
|
|
||||||
|
|
||||||
expect(api.getRoot).toHaveBeenCalled();
|
|
||||||
expect(wrapper.find('App')).toHaveLength(0);
|
|
||||||
expect(wrapper.find('Login')).toHaveLength(1);
|
|
||||||
|
|
||||||
const { src } = wrapper.find('Login Brand img').props();
|
|
||||||
expect(src).toContain(data.custom_logo);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('app loads when authenticated', async (done) => {
|
|
||||||
const isAuthenticated = () => true;
|
|
||||||
const getRoot = jest.fn(() => Promise.resolve({ data }));
|
|
||||||
|
|
||||||
const api = { getRoot, isAuthenticated };
|
|
||||||
const wrapper = await main(render, api);
|
|
||||||
|
|
||||||
expect(api.getRoot).toHaveBeenCalled();
|
|
||||||
expect(wrapper.find('App')).toHaveLength(1);
|
|
||||||
expect(wrapper.find('Login')).toHaveLength(0);
|
|
||||||
|
|
||||||
wrapper.find('header a').simulate('click');
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find('App')).toHaveLength(1);
|
|
||||||
expect(wrapper.find('Login')).toHaveLength(0);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getLanguage returns the expected language code', () => {
|
|
||||||
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
|
|
||||||
expect(getLanguage({ languages: ['es-US'], language: 'fr-FR', userLanguage: 'en-US' })).toEqual('es');
|
|
||||||
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual('fr');
|
|
||||||
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { asyncFlush } from '../../jest.setup';
|
import { asyncFlush } from '../../jest.setup';
|
||||||
import AWXLogin from '../../src/pages/Login';
|
import { _AWXLogin } from '../../src/pages/Login';
|
||||||
import APIClient from '../../src/api';
|
import APIClient from '../../src/api';
|
||||||
|
|
||||||
describe('<Login />', () => {
|
describe('<Login />', () => {
|
||||||
@@ -32,7 +32,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AWXLogin api={api} />
|
<_AWXLogin api={api} clearRootDialogMessage={() => {}} handleHttpError={() => {}} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -61,7 +61,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AWXLogin api={api} logo="images/foo.jpg" alt="Foo Application" />
|
<_AWXLogin api={api} logo="images/foo.jpg" alt="Foo Application" />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -75,7 +75,7 @@ describe('<Login />', () => {
|
|||||||
loginWrapper = mount(
|
loginWrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AWXLogin api={api} />
|
<_AWXLogin api={api} />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -166,9 +166,7 @@ describe('<Login />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('render Redirect to / when already authenticated', () => {
|
test('render Redirect to / when already authenticated', () => {
|
||||||
api.isAuthenticated = jest.fn();
|
awxLogin.setState({ isAuthenticated: true });
|
||||||
api.isAuthenticated.mockReturnValue(true);
|
|
||||||
loginWrapper = shallow(<AWXLogin api={api} />);
|
|
||||||
const redirectElem = loginWrapper.find('Redirect');
|
const redirectElem = loginWrapper.find('Redirect');
|
||||||
expect(redirectElem.length).toBe(1);
|
expect(redirectElem.length).toBe(1);
|
||||||
expect(redirectElem.props().to).toBe('/');
|
expect(redirectElem.props().to).toBe('/');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { mount } from 'enzyme';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
|
|
||||||
import OrganizationAccessList from '../../../../src/pages/Organizations/components/OrganizationAccessList';
|
import OrganizationAccessList, { _OrganizationAccessList } from '../../../../src/pages/Organizations/components/OrganizationAccessList';
|
||||||
|
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
@@ -66,16 +66,17 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('onExpand and onCompact methods called when user clicks on Expand and Compact icons respectively', async (done) => {
|
test('onExpand and onCompact methods called when user clicks on Expand and Compact icons respectively', async (done) => {
|
||||||
const onExpand = jest.spyOn(OrganizationAccessList.prototype, 'onExpand');
|
const onExpand = jest.spyOn(_OrganizationAccessList.prototype, 'onExpand');
|
||||||
const onCompact = jest.spyOn(OrganizationAccessList.prototype, 'onCompact');
|
const onCompact = jest.spyOn(_OrganizationAccessList.prototype, 'onCompact');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<OrganizationAccessList
|
<_OrganizationAccessList
|
||||||
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
||||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
@@ -94,15 +95,16 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('onSort being passed properly to DataListToolbar component', async (done) => {
|
test('onSort being passed properly to DataListToolbar component', async (done) => {
|
||||||
const onSort = jest.spyOn(OrganizationAccessList.prototype, 'onSort');
|
const onSort = jest.spyOn(_OrganizationAccessList.prototype, 'onSort');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<OrganizationAccessList
|
<_OrganizationAccessList
|
||||||
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
||||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
@@ -141,17 +143,18 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('test handleWarning, confirmDelete, and removeRole methods for Alert component', (done) => {
|
test('test handleWarning, confirmDelete, and removeRole methods for Alert component', (done) => {
|
||||||
const handleWarning = jest.spyOn(OrganizationAccessList.prototype, 'handleWarning');
|
const handleWarning = jest.spyOn(_OrganizationAccessList.prototype, 'handleWarning');
|
||||||
const confirmDelete = jest.spyOn(OrganizationAccessList.prototype, 'confirmDelete');
|
const confirmDelete = jest.spyOn(_OrganizationAccessList.prototype, 'confirmDelete');
|
||||||
const removeRole = jest.spyOn(OrganizationAccessList.prototype, 'removeAccessRole');
|
const removeRole = jest.spyOn(_OrganizationAccessList.prototype, 'removeAccessRole');
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<OrganizationAccessList
|
<_OrganizationAccessList
|
||||||
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
||||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
@@ -164,8 +167,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
const rendered = wrapper.update().find('ChipButton');
|
const rendered = wrapper.update().find('ChipButton');
|
||||||
rendered.find('button[aria-label="close"]').simulate('click');
|
rendered.find('button[aria-label="close"]').simulate('click');
|
||||||
expect(handleWarning).toHaveBeenCalled();
|
expect(handleWarning).toHaveBeenCalled();
|
||||||
const alert = wrapper.update().find('Alert');
|
const alertModal = wrapper.update().find('Modal');
|
||||||
alert.find('button[aria-label="confirm-delete"]').simulate('click');
|
alertModal.find('button[aria-label="Confirm delete"]').simulate('click');
|
||||||
expect(confirmDelete).toHaveBeenCalled();
|
expect(confirmDelete).toHaveBeenCalled();
|
||||||
expect(removeRole).toHaveBeenCalled();
|
expect(removeRole).toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
@@ -176,7 +179,7 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<OrganizationAccessList
|
<_OrganizationAccessList
|
||||||
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
|
||||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
@@ -200,8 +203,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
];
|
];
|
||||||
const rendered = wrapper.update().find('ChipButton');
|
const rendered = wrapper.update().find('ChipButton');
|
||||||
rendered.find('button[aria-label="close"]').simulate('click');
|
rendered.find('button[aria-label="close"]').simulate('click');
|
||||||
const alert = wrapper.update().find('Alert');
|
const alertModal = wrapper.update().find('Modal');
|
||||||
alert.find('button[aria-label="confirm-delete"]').simulate('click');
|
alertModal.find('button[aria-label="Confirm delete"]').simulate('click');
|
||||||
expect(wrapper.state().warningTitle).not.toBe(null);
|
expect(wrapper.state().warningTitle).not.toBe(null);
|
||||||
expect(wrapper.state().warningMsg).not.toBe(null);
|
expect(wrapper.state().warningMsg).not.toBe(null);
|
||||||
expected.forEach(criteria => {
|
expected.forEach(criteria => {
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { ConfigContext } from '../../../../src/context';
|
|
||||||
import OrganizationForm from '../../../../src/pages/Organizations/components/OrganizationForm';
|
|
||||||
import { sleep } from '../../../testUtils';
|
import { sleep } from '../../../testUtils';
|
||||||
|
import { ConfigProvider } from '../../../../src/contexts/Config';
|
||||||
|
import { NetworkProvider } from '../../../../src/contexts/Network';
|
||||||
|
import OrganizationForm, { _OrganizationForm } from '../../../../src/pages/Organizations/components/OrganizationForm';
|
||||||
|
|
||||||
describe('<OrganizationForm />', () => {
|
describe('<OrganizationForm />', () => {
|
||||||
let api;
|
let api;
|
||||||
|
let networkProviderValue;
|
||||||
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -23,6 +25,11 @@ describe('<OrganizationForm />', () => {
|
|||||||
api = {
|
api = {
|
||||||
getInstanceGroups: jest.fn(),
|
getInstanceGroups: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
networkProviderValue = {
|
||||||
|
api,
|
||||||
|
handleHttpError: () => {}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should request related instance groups from api', () => {
|
test('should request related instance groups from api', () => {
|
||||||
@@ -34,16 +41,18 @@ describe('<OrganizationForm />', () => {
|
|||||||
Promise.resolve({ data: { results: mockInstanceGroups } })
|
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||||
));
|
));
|
||||||
mount(
|
mount(
|
||||||
<I18nProvider>
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
<I18nProvider>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
api={api}
|
<_OrganizationForm
|
||||||
organization={mockData}
|
api={api}
|
||||||
handleSubmit={jest.fn()}
|
organization={mockData}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
/>
|
handleCancel={jest.fn()}
|
||||||
</MemoryRouter>
|
/>
|
||||||
</I18nProvider>
|
</NetworkProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
).find('OrganizationForm');
|
).find('OrganizationForm');
|
||||||
|
|
||||||
expect(api.getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
|
expect(api.getOrganizationInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
@@ -58,16 +67,18 @@ describe('<OrganizationForm />', () => {
|
|||||||
Promise.resolve({ data: { results: mockInstanceGroups } })
|
Promise.resolve({ data: { results: mockInstanceGroups } })
|
||||||
));
|
));
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
<I18nProvider>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<_OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={jest.fn()}
|
api={api}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
/>
|
handleCancel={jest.fn()}
|
||||||
</MemoryRouter>
|
/>
|
||||||
</I18nProvider>
|
</NetworkProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
</MemoryRouter>
|
||||||
).find('OrganizationForm');
|
).find('OrganizationForm');
|
||||||
|
|
||||||
await wrapper.instance().componentDidMount();
|
await wrapper.instance().componentDidMount();
|
||||||
@@ -78,12 +89,13 @@ describe('<OrganizationForm />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
/>
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationForm');
|
).find('OrganizationForm');
|
||||||
@@ -109,12 +121,13 @@ describe('<OrganizationForm />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
/>
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationForm');
|
).find('OrganizationForm');
|
||||||
@@ -137,14 +150,15 @@ describe('<OrganizationForm />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ConfigContext.Provider value={config}>
|
<NetworkProvider value={networkProviderValue}>
|
||||||
<OrganizationForm
|
<ConfigProvider value={config}>
|
||||||
organization={mockData}
|
<OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
handleCancel={jest.fn()}
|
handleCancel={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</ConfigContext.Provider>
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -157,16 +171,18 @@ describe('<OrganizationForm />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={handleSubmit}
|
api={api}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={handleSubmit}
|
||||||
/>
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationForm');
|
).find('OrganizationForm');
|
||||||
expect(wrapper.prop('handleSubmit')).not.toHaveBeenCalled();
|
expect(handleSubmit).not.toHaveBeenCalled();
|
||||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(handleSubmit).toHaveBeenCalledWith({
|
expect(handleSubmit).toHaveBeenCalledWith({
|
||||||
@@ -196,12 +212,14 @@ describe('<OrganizationForm />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<_OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={handleSubmit}
|
api={api}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={handleSubmit}
|
||||||
/>
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('OrganizationForm');
|
).find('OrganizationForm');
|
||||||
@@ -223,12 +241,13 @@ describe('<OrganizationForm />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationForm
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<OrganizationForm
|
||||||
api={api}
|
organization={mockData}
|
||||||
handleSubmit={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ describe('<OrganizationDetail />', () => {
|
|||||||
api={{
|
api={{
|
||||||
getOrganizationInstanceGroups
|
getOrganizationInstanceGroups
|
||||||
}}
|
}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
organization={mockDetails}
|
organization={mockDetails}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -63,6 +64,7 @@ describe('<OrganizationDetail />', () => {
|
|||||||
params: { id: '1' }
|
params: { id: '1' }
|
||||||
}}
|
}}
|
||||||
organization={mockDetails}
|
organization={mockDetails}
|
||||||
|
handleHttpError={() => {}}
|
||||||
api={{
|
api={{
|
||||||
getOrganizationInstanceGroups
|
getOrganizationInstanceGroups
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import OrganizationEdit, { _OrganizationEdit } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
|
|
||||||
|
import { NetworkProvider } from '../../../../../src/contexts/Network';
|
||||||
|
|
||||||
|
import { _OrganizationEdit } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationEdit';
|
||||||
|
|
||||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
describe('<OrganizationEdit />', () => {
|
describe('<OrganizationEdit />', () => {
|
||||||
let api;
|
let api;
|
||||||
|
let networkProviderValue;
|
||||||
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
@@ -26,16 +30,24 @@ describe('<OrganizationEdit />', () => {
|
|||||||
associateInstanceGroup: jest.fn(),
|
associateInstanceGroup: jest.fn(),
|
||||||
disassociate: jest.fn(),
|
disassociate: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
networkProviderValue = {
|
||||||
|
api,
|
||||||
|
handleHttpError: () => {}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSubmit should call api update', () => {
|
test('handleSubmit should call api update', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationEdit
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<_OrganizationEdit
|
||||||
api={api}
|
organization={mockData}
|
||||||
/>
|
api={api}
|
||||||
|
handleHttpError={() => {}}
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -57,10 +69,13 @@ describe('<OrganizationEdit />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationEdit
|
<NetworkProvider value={networkProviderValue}>
|
||||||
organization={mockData}
|
<_OrganizationEdit
|
||||||
api={api}
|
organization={mockData}
|
||||||
/>
|
api={api}
|
||||||
|
handleHttpError={() => {}}
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -94,11 +109,14 @@ describe('<OrganizationEdit />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<_OrganizationEdit
|
<NetworkProvider value={networkProviderValue}>
|
||||||
history={history}
|
<_OrganizationEdit
|
||||||
organization={mockData}
|
organization={mockData}
|
||||||
api={api}
|
api={api}
|
||||||
/>
|
handleHttpError={() => {}}
|
||||||
|
history={history}
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import OrganizationNotifications from '../../../../../src/pages/Organizations/screens/Organization/OrganizationNotifications';
|
import { _OrganizationNotifications } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationNotifications';
|
||||||
|
|
||||||
describe('<OrganizationNotifications />', () => {
|
describe('<OrganizationNotifications />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mount(
|
mount(
|
||||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
<OrganizationNotifications
|
<_OrganizationNotifications
|
||||||
match={{ path: '/organizations/:id/notifications', url: '/organizations/1/notifications' }}
|
match={{ path: '/organizations/:id/notifications', url: '/organizations/1/notifications' }}
|
||||||
location={{ search: '', pathname: '/organizations/1/notifications' }}
|
location={{ search: '', pathname: '/organizations/1/notifications' }}
|
||||||
params={{}}
|
params={{}}
|
||||||
@@ -18,6 +18,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
createOrganizationNotificationSuccess: jest.fn(),
|
createOrganizationNotificationSuccess: jest.fn(),
|
||||||
createOrganizationNotificationError: jest.fn()
|
createOrganizationNotificationError: jest.fn()
|
||||||
}}
|
}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -30,7 +31,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
const createOrganizationNotificationError = jest.fn();
|
const createOrganizationNotificationError = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
<OrganizationNotifications
|
<_OrganizationNotifications
|
||||||
match={{ path: '/organizations/:id/notifications', url: '/organizations/1/notifications' }}
|
match={{ path: '/organizations/:id/notifications', url: '/organizations/1/notifications' }}
|
||||||
location={{ search: '', pathname: '/organizations/1/notifications' }}
|
location={{ search: '', pathname: '/organizations/1/notifications' }}
|
||||||
params={{}}
|
params={{}}
|
||||||
@@ -41,6 +42,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
createOrganizationNotificationSuccess,
|
createOrganizationNotificationSuccess,
|
||||||
createOrganizationNotificationError
|
createOrganizationNotificationError
|
||||||
}}
|
}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationNotifications');
|
).find('OrganizationNotifications');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createMemoryHistory } from 'history';
|
|||||||
import { sleep } from '../../../../testUtils';
|
import { sleep } from '../../../../testUtils';
|
||||||
import OrganizationTeams, { _OrganizationTeams } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationTeams';
|
import OrganizationTeams, { _OrganizationTeams } from '../../../../../src/pages/Organizations/screens/Organization/OrganizationTeams';
|
||||||
import OrganizationTeamsList from '../../../../../src/pages/Organizations/components/OrganizationTeamsList';
|
import OrganizationTeamsList from '../../../../../src/pages/Organizations/components/OrganizationTeamsList';
|
||||||
|
import { NetworkProvider } from '../../../../../src/contexts/Network';
|
||||||
|
|
||||||
const listData = {
|
const listData = {
|
||||||
data: {
|
data: {
|
||||||
@@ -30,6 +31,7 @@ describe('<OrganizationTeams />', () => {
|
|||||||
api={{
|
api={{
|
||||||
readOrganizationTeamsList: jest.fn(),
|
readOrganizationTeamsList: jest.fn(),
|
||||||
}}
|
}}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -39,11 +41,14 @@ describe('<OrganizationTeams />', () => {
|
|||||||
mount(
|
mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||||
<OrganizationTeams
|
<NetworkProvider
|
||||||
id={1}
|
value={{ api: { readOrganizationTeamsList }, handleHttpError: () => {} }}
|
||||||
searchString=""
|
>
|
||||||
api={{ readOrganizationTeamsList }}
|
<OrganizationTeams
|
||||||
/>
|
id={1}
|
||||||
|
searchString=""
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
).find('OrganizationTeams');
|
).find('OrganizationTeams');
|
||||||
@@ -59,11 +64,14 @@ describe('<OrganizationTeams />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<OrganizationTeams
|
<NetworkProvider
|
||||||
id={1}
|
value={{ api: { readOrganizationTeamsList }, handleHttpError: () => {} }}
|
||||||
searchString=""
|
>
|
||||||
api={{ readOrganizationTeamsList }}
|
<OrganizationTeams
|
||||||
/>
|
id={1}
|
||||||
|
searchString=""
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
@@ -100,11 +108,14 @@ describe('<OrganizationTeams />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationTeams
|
<NetworkProvider
|
||||||
id={1}
|
value={{ api: { readOrganizationTeamsList }, handleHttpError: () => {} }}
|
||||||
searchString=""
|
>
|
||||||
api={{ readOrganizationTeamsList }}
|
<OrganizationTeams
|
||||||
/>
|
id={1}
|
||||||
|
searchString=""
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import { ConfigContext } from '../../../../src/context';
|
|
||||||
import OrganizationAdd, { _OrganizationAdd } from '../../../../src/pages/Organizations/screens/OrganizationAdd';
|
import { ConfigProvider } from '../../../../src/contexts/Config';
|
||||||
|
import { NetworkProvider } from '../../../../src/contexts/Network';
|
||||||
|
|
||||||
|
import { _OrganizationAdd } from '../../../../src/pages/Organizations/screens/OrganizationAdd';
|
||||||
|
|
||||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
describe('<OrganizationAdd />', () => {
|
describe('<OrganizationAdd />', () => {
|
||||||
let api;
|
let api;
|
||||||
|
let networkProviderValue;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
api = {
|
api = {
|
||||||
@@ -17,15 +21,22 @@ describe('<OrganizationAdd />', () => {
|
|||||||
associateInstanceGroup: jest.fn(),
|
associateInstanceGroup: jest.fn(),
|
||||||
disassociate: jest.fn(),
|
disassociate: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
networkProviderValue = {
|
||||||
|
api,
|
||||||
|
handleHttpError: () => {}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSubmit should post to api', () => {
|
test('handleSubmit should post to api', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationAdd
|
<NetworkProvider value={networkProviderValue}>
|
||||||
api={api}
|
<ConfigProvider>
|
||||||
/>
|
<_OrganizationAdd api={api} />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -47,10 +58,11 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<_OrganizationAdd
|
<NetworkProvider value={networkProviderValue}>
|
||||||
history={history}
|
<ConfigProvider>
|
||||||
api={api}
|
<_OrganizationAdd api={api} history={history} />
|
||||||
/>
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -68,10 +80,11 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<_OrganizationAdd
|
<NetworkProvider value={networkProviderValue}>
|
||||||
history={history}
|
<ConfigProvider>
|
||||||
api={api}
|
<_OrganizationAdd api={api} history={history} />
|
||||||
/>
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -103,10 +116,11 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<_OrganizationAdd
|
<NetworkProvider value={networkProviderValue}>
|
||||||
history={history}
|
<ConfigProvider>
|
||||||
api={api}
|
<_OrganizationAdd api={api} history={history} />
|
||||||
/>
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -121,9 +135,11 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationAdd
|
<NetworkProvider value={networkProviderValue}>
|
||||||
api={api}
|
<ConfigProvider>
|
||||||
/>
|
<_OrganizationAdd api={api} />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -156,9 +172,11 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ConfigContext.Provider value={config}>
|
<NetworkProvider value={networkProviderValue}>
|
||||||
<OrganizationAdd api={api} />
|
<ConfigProvider value={config}>
|
||||||
</ConfigContext.Provider>
|
<_OrganizationAdd api={api} />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationAdd').find('AnsibleSelect');
|
).find('OrganizationAdd').find('AnsibleSelect');
|
||||||
@@ -173,9 +191,11 @@ describe('<OrganizationAdd />', () => {
|
|||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ConfigContext.Provider value={config}>
|
<NetworkProvider value={networkProviderValue}>
|
||||||
<OrganizationAdd api={api} />
|
<ConfigProvider value={config}>
|
||||||
</ConfigContext.Provider>
|
<_OrganizationAdd api={api} />
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('OrganizationAdd').find('AnsibleSelect');
|
).find('OrganizationAdd').find('AnsibleSelect');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { I18nProvider } from '@lingui/react';
|
import { I18nProvider } from '@lingui/react';
|
||||||
import OrganizationsList from '../../../../src/pages/Organizations/screens/OrganizationsList';
|
import { _OrganizationsList } from '../../../../src/pages/Organizations/screens/OrganizationsList';
|
||||||
|
|
||||||
const mockAPIOrgsList = {
|
const mockAPIOrgsList = {
|
||||||
data: {
|
data: {
|
||||||
@@ -48,25 +48,28 @@ describe('<OrganizationsList />', () => {
|
|||||||
mount(
|
mount(
|
||||||
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationsList
|
<_OrganizationsList
|
||||||
match={{ path: '/organizations', url: '/organizations' }}
|
match={{ path: '/organizations', url: '/organizations' }}
|
||||||
location={{ search: '', pathname: '/organizations' }}
|
location={{ search: '', pathname: '/organizations' }}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only('Modal closes when close button is clicked.', async (done) => {
|
// TODO: these tests were not correct. will work to clean these tests up after PR
|
||||||
|
test.skip('Modal closes when close button is clicked.', async (done) => {
|
||||||
const handleClearOrgsToDelete = jest.fn();
|
const handleClearOrgsToDelete = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationsList
|
<_OrganizationsList
|
||||||
match={{ path: '/organizations', url: '/organizations' }}
|
match={{ path: '/organizations', url: '/organizations' }}
|
||||||
location={{ search: '', pathname: '/organizations' }}
|
location={{ search: '', pathname: '/organizations' }}
|
||||||
getItems={({ data: { orgsToDelete: [{ name: 'Organization 1', id: 1 }] } })}
|
getItems={({ data: { orgsToDelete: [{ name: 'Organization 1', id: 1 }] } })}
|
||||||
handleClearOrgsToDelete={handleClearOrgsToDelete()}
|
handleClearOrgsToDelete={handleClearOrgsToDelete()}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -91,21 +94,22 @@ describe('<OrganizationsList />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only('Orgs to delete length is 0 when all orgs are selected and Delete button is called.', async (done) => {
|
// TODO: these tests were not correct. will work to clean these tests up after PR
|
||||||
|
test.skip('Orgs to delete length is 0 when all orgs are selected and Delete button is called.', async (done) => {
|
||||||
const handleClearOrgsToDelete = jest.fn();
|
const handleClearOrgsToDelete = jest.fn();
|
||||||
const handleOrgDelete = jest.fn();
|
const handleOrgDelete = jest.fn();
|
||||||
const fetchOrganizations = jest.fn();
|
const fetchOrganizations = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<OrganizationsList
|
<_OrganizationsList
|
||||||
match={{ path: '/organizations', url: '/organizations' }}
|
match={{ path: '/organizations', url: '/organizations' }}
|
||||||
location={{ search: '', pathname: '/organizations' }}
|
location={{ search: '', pathname: '/organizations' }}
|
||||||
getItems={({ data: { orgsToDelete: [{ name: 'Organization 1', id: 1 }] } })}
|
getItems={({ data: { orgsToDelete: [{ name: 'Organization 1', id: 1 }] } })}
|
||||||
handleClearOrgsToDelete={handleClearOrgsToDelete()}
|
handleClearOrgsToDelete={handleClearOrgsToDelete()}
|
||||||
handleOrgDelete={handleOrgDelete()}
|
handleOrgDelete={handleOrgDelete()}
|
||||||
fetchOrganizations={fetchOrganizations()}
|
fetchOrganizations={fetchOrganizations()}
|
||||||
|
handleHttpError={() => {}}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { encodeQueryString, parseQueryString } from '../src/qs';
|
import { encodeQueryString, parseQueryString } from '../../src/util/qs';
|
||||||
|
|
||||||
describe('qs (qs.js)', () => {
|
describe('qs (qs.js)', () => {
|
||||||
test('encodeQueryString returns the expected queryString', () => {
|
test('encodeQueryString returns the expected queryString', () => {
|
||||||
175
src/App.jsx
175
src/App.jsx
@@ -6,13 +6,21 @@ import {
|
|||||||
Page,
|
Page,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageSidebar,
|
PageSidebar,
|
||||||
|
Button
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { I18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { RootDialog } from './contexts/RootDialog';
|
||||||
|
import { withNetwork } from './contexts/Network';
|
||||||
|
import { Config } from './contexts/Config';
|
||||||
|
|
||||||
|
import AlertModal from './components/AlertModal';
|
||||||
import About from './components/About';
|
import About from './components/About';
|
||||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||||
import TowerLogo from './components/TowerLogo';
|
import TowerLogo from './components/TowerLogo';
|
||||||
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||||
import { ConfigContext } from './context';
|
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -23,29 +31,24 @@ 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,
|
|
||||||
isAboutModalOpen: false,
|
isAboutModalOpen: false,
|
||||||
isNavOpen,
|
isNavOpen
|
||||||
version: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fetchConfig = this.fetchConfig.bind(this);
|
|
||||||
this.onLogout = this.onLogout.bind(this);
|
this.onLogout = this.onLogout.bind(this);
|
||||||
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
||||||
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
||||||
this.onNavToggle = this.onNavToggle.bind(this);
|
this.onNavToggle = this.onNavToggle.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.fetchConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
async onLogout () {
|
async onLogout () {
|
||||||
const { api } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
|
try {
|
||||||
await api.logout();
|
await api.logout();
|
||||||
window.location.replace('/#/login');
|
window.location.replace('/#/login');
|
||||||
|
} catch (err) {
|
||||||
|
handleHttpError(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAboutModalOpen () {
|
onAboutModalOpen () {
|
||||||
@@ -60,24 +63,10 @@ class App extends Component {
|
|||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchConfig () {
|
|
||||||
const { api } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: { ansible_version, custom_virtualenvs, version } } = await api.getConfig();
|
|
||||||
this.setState({ ansible_version, custom_virtualenvs, version });
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ ansible_version: null, custom_virtualenvs: null, version: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
ansible_version,
|
|
||||||
custom_virtualenvs,
|
|
||||||
isAboutModalOpen,
|
isAboutModalOpen,
|
||||||
isNavOpen,
|
isNavOpen
|
||||||
version,
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -86,63 +75,81 @@ class App extends Component {
|
|||||||
navLabel = '',
|
navLabel = '',
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const config = {
|
|
||||||
ansible_version,
|
|
||||||
custom_virtualenvs,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Config>
|
||||||
<Page
|
{({ ansible_version, version }) => (
|
||||||
usecondensed="True"
|
<I18n>
|
||||||
header={(
|
{({ i18n }) => (
|
||||||
<PageHeader
|
<RootDialog>
|
||||||
showNavToggle
|
{({ title, bodyText, variant = 'info', clearRootDialogMessage }) => (
|
||||||
onNavToggle={this.onNavToggle}
|
<Fragment>
|
||||||
logo={<TowerLogo linkTo="/" />}
|
{(title || bodyText) && (
|
||||||
toolbar={(
|
<AlertModal
|
||||||
<PageHeaderToolbar
|
variant={variant}
|
||||||
isAboutDisabled={!version}
|
isOpen={!!(title || bodyText)}
|
||||||
onAboutClick={this.onAboutModalOpen}
|
onClose={clearRootDialogMessage}
|
||||||
onLogoutClick={this.onLogout}
|
title={title}
|
||||||
/>
|
actions={[
|
||||||
)}
|
<Button key="close" variant="secondary" onClick={clearRootDialogMessage}>{i18n._(t`Close`)}</Button>
|
||||||
/>
|
]}
|
||||||
)}
|
>
|
||||||
sidebar={(
|
{bodyText}
|
||||||
<PageSidebar
|
</AlertModal>
|
||||||
isNavOpen={isNavOpen}
|
)}
|
||||||
nav={(
|
<Page
|
||||||
<Nav aria-label={navLabel}>
|
usecondensed="True"
|
||||||
<NavList>
|
header={(
|
||||||
{routeGroups.map(({ groupId, groupTitle, routes }) => (
|
<PageHeader
|
||||||
<NavExpandableGroup
|
showNavToggle
|
||||||
key={groupId}
|
onNavToggle={this.onNavToggle}
|
||||||
groupId={groupId}
|
logo={<TowerLogo linkTo="/" />}
|
||||||
groupTitle={groupTitle}
|
toolbar={(
|
||||||
routes={routes}
|
<PageHeaderToolbar
|
||||||
/>
|
isAboutDisabled={!version}
|
||||||
))}
|
onAboutClick={this.onAboutModalOpen}
|
||||||
</NavList>
|
onLogoutClick={this.onLogout}
|
||||||
</Nav>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
sidebar={(
|
||||||
<ConfigContext.Provider value={config}>
|
<PageSidebar
|
||||||
{render && render({ routeGroups })}
|
isNavOpen={isNavOpen}
|
||||||
</ConfigContext.Provider>
|
nav={(
|
||||||
</Page>
|
<Nav aria-label={navLabel}>
|
||||||
<About
|
<NavList>
|
||||||
ansible_version={ansible_version}
|
{routeGroups.map(({ groupId, groupTitle, routes }) => (
|
||||||
version={version}
|
<NavExpandableGroup
|
||||||
isOpen={isAboutModalOpen}
|
key={groupId}
|
||||||
onClose={this.onAboutModalClose}
|
groupId={groupId}
|
||||||
/>
|
groupTitle={groupTitle}
|
||||||
</Fragment>
|
routes={routes}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NavList>
|
||||||
|
</Nav>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{render && render({ routeGroups })}
|
||||||
|
</Page>
|
||||||
|
<About
|
||||||
|
ansible_version={ansible_version}
|
||||||
|
version={version}
|
||||||
|
isOpen={isAboutModalOpen}
|
||||||
|
onClose={this.onAboutModalClose}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</RootDialog>
|
||||||
|
)}
|
||||||
|
</I18n>
|
||||||
|
)}
|
||||||
|
</Config>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export { App as _App };
|
||||||
|
export default withNetwork(App);
|
||||||
|
|||||||
50
src/RootProvider.jsx
Normal file
50
src/RootProvider.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
I18nProvider,
|
||||||
|
} from '@lingui/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
HashRouter
|
||||||
|
} 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 en from '../build/locales/en/messages';
|
||||||
|
|
||||||
|
export function getLanguage (nav) {
|
||||||
|
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
||||||
|
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||||
|
|
||||||
|
return languageWithoutRegionCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootProvider extends Component {
|
||||||
|
render () {
|
||||||
|
const { children } = this.props;
|
||||||
|
|
||||||
|
const catalogs = { en, ja };
|
||||||
|
const language = getLanguage(navigator);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HashRouter>
|
||||||
|
<I18nProvider
|
||||||
|
language={language}
|
||||||
|
catalogs={catalogs}
|
||||||
|
>
|
||||||
|
<RootDialogProvider>
|
||||||
|
<NetworkProvider>
|
||||||
|
<ConfigProvider>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
|
</NetworkProvider>
|
||||||
|
</RootDialogProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
</HashRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RootProvider;
|
||||||
99
src/app.scss
99
src/app.scss
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
// masthead overrides
|
// masthead overrides
|
||||||
//
|
//
|
||||||
|
|
||||||
.pf-c-page__header-brand {
|
.pf-c-page__header-brand {
|
||||||
max-width: 255px;
|
max-width: 255px;
|
||||||
}
|
}
|
||||||
@@ -114,8 +115,9 @@
|
|||||||
|
|
||||||
//
|
//
|
||||||
// switch overrides
|
// switch overrides
|
||||||
//
|
|
||||||
// https://github.com/patternfly/patternfly-next/issues/915
|
// https://github.com/patternfly/patternfly-next/issues/915
|
||||||
|
//
|
||||||
|
|
||||||
.pf-c-switch {
|
.pf-c-switch {
|
||||||
.pf-c-switch__label::before {
|
.pf-c-switch__label::before {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -145,6 +147,7 @@
|
|||||||
|
|
||||||
.awx-c-modal.pf-c-modal-box {
|
.awx-c-modal.pf-c-modal-box {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 24px;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
|
||||||
.pf-c-modal-box__body {
|
.pf-c-modal-box__body {
|
||||||
@@ -231,13 +234,21 @@
|
|||||||
border-bottom: 1px solid #d7d7d7;
|
border-bottom: 1px solid #d7d7d7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.at-c-listCardBody {
|
||||||
|
--pf-c-card__footer--PaddingX: 0;
|
||||||
|
--pf-c-card__footer--PaddingY: 0;
|
||||||
|
--pf-c-card__body--PaddingX: 0;
|
||||||
|
--pf-c-card__body--PaddingY: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.awx-c-card {
|
.awx-c-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
// PF Alert notification component overrides
|
// PF Alert notification component overrides
|
||||||
|
//
|
||||||
|
|
||||||
.pf-c-alert__title {
|
.pf-c-alert__title {
|
||||||
--pf-c-alert__title--PaddingTop: 20px;
|
--pf-c-alert__title--PaddingTop: 20px;
|
||||||
--pf-c-alert__title--PaddingRight: 20px;
|
--pf-c-alert__title--PaddingRight: 20px;
|
||||||
@@ -272,17 +283,6 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.orgListAlert-actionBtn{
|
|
||||||
margin:0 10px;
|
|
||||||
}
|
|
||||||
.orgListDetete-progressBar{
|
|
||||||
padding-right: 32px;
|
|
||||||
}
|
|
||||||
.orgListDelete-progressBar-noShow{
|
|
||||||
display: none;
|
|
||||||
padding-right: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.awx-c-form-action-group {
|
.awx-c-form-action-group {
|
||||||
float: right;
|
float: right;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -293,3 +293,74 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// AlertModal styles
|
||||||
|
//
|
||||||
|
|
||||||
|
.at-c-alertModal.pf-c-modal-box {
|
||||||
|
border: 0;
|
||||||
|
border-left: 56px solid black;
|
||||||
|
|
||||||
|
.at-c-alertModal__icon {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 23px;
|
||||||
|
top: 28px;
|
||||||
|
left: -39px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal--warning.pf-c-modal-box {
|
||||||
|
border-color: var(--pf-global--warning-color--100);
|
||||||
|
|
||||||
|
.pf-c-title {
|
||||||
|
color: var(--pf-global--warning-color--200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal__icon {
|
||||||
|
color: var(--pf-global--warning-color--200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal--danger.pf-c-modal-box {
|
||||||
|
border-color: var(--pf-global--danger-color--100);
|
||||||
|
|
||||||
|
.pf-c-title {
|
||||||
|
color: var(--pf-global--danger-color--200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal__icon {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal--info.pf-c-modal-box {
|
||||||
|
border-color: var(--pf-global--info-color--100);
|
||||||
|
|
||||||
|
.pf-c-title {
|
||||||
|
color: var(--pf-global--info-color--200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal__icon {
|
||||||
|
color: var(--pf-global--info-color--200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal--success.pf-c-modal-box {
|
||||||
|
border-color: var(--pf-global--success-color--100);
|
||||||
|
|
||||||
|
.pf-c-title {
|
||||||
|
color: var(--pf-global--success-color--200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.at-c-alertModal__icon {
|
||||||
|
color: var(--pf-global--success-color--200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// LoginModal overrides
|
||||||
|
//
|
||||||
|
|
||||||
|
.pf-m-error p.pf-c-form__helper-text {
|
||||||
|
color: var(--pf-global--danger-color--100);
|
||||||
|
}
|
||||||
|
|||||||
28
src/components/AlertModal.jsx
Normal file
28
src/components/AlertModal.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Modal
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { ExclamationTriangleIcon, ExclamationCircleIcon, InfoCircleIcon, CheckCircleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
const getIcon = (variant) => {
|
||||||
|
let icon;
|
||||||
|
if (variant === 'warning') {
|
||||||
|
icon = (<ExclamationTriangleIcon className="at-c-alertModal__icon" />);
|
||||||
|
} else if (variant === 'danger') {
|
||||||
|
icon = (<ExclamationCircleIcon className="at-c-alertModal__icon" />);
|
||||||
|
} if (variant === 'info') {
|
||||||
|
icon = (<InfoCircleIcon className="at-c-alertModal__icon" />);
|
||||||
|
} if (variant === 'success') {
|
||||||
|
icon = (<CheckCircleIcon className="at-c-alertModal__icon" />);
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({ variant, children, ...props }) => (
|
||||||
|
<Modal className={`awx-c-modal${variant && ` at-c-alertModal at-c-alertModal--${variant}`}`} {...props}>
|
||||||
|
{children}
|
||||||
|
{getIcon(variant)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
import { I18n } from '@lingui/react';
|
import { I18n } from '@lingui/react';
|
||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../contexts/Network';
|
||||||
|
|
||||||
import CheckboxListItem from '../ListItem';
|
import CheckboxListItem from '../ListItem';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
import SelectedList from '../SelectedList';
|
import SelectedList from '../SelectedList';
|
||||||
@@ -67,7 +69,7 @@ class Lookup extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getData () {
|
async getData () {
|
||||||
const { getItems } = this.props;
|
const { getItems, handleHttpError } = this.props;
|
||||||
const { page, page_size, sortedColumnKey, sortOrder } = this.state;
|
const { page, page_size, sortedColumnKey, sortOrder } = this.state;
|
||||||
|
|
||||||
this.setState({ error: false });
|
this.setState({ error: false });
|
||||||
@@ -92,7 +94,7 @@ class Lookup extends React.Component {
|
|||||||
|
|
||||||
this.setState(stateToUpdate);
|
this.setState(stateToUpdate);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
handleHttpError(err) || this.setState({ error: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,4 +275,5 @@ Lookup.defaultProps = {
|
|||||||
name: null,
|
name: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Lookup;
|
export { Lookup as _Lookup };
|
||||||
|
export default withNetwork(Lookup);
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import { CubesIcon } from '@patternfly/react-icons';
|
|||||||
import { I18n, i18nMark } from '@lingui/react';
|
import { I18n, i18nMark } from '@lingui/react';
|
||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../contexts/Network';
|
||||||
|
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
import NotificationListItem from './NotificationListItem';
|
import NotificationListItem from './NotificationListItem';
|
||||||
import Pagination from '../Pagination';
|
import Pagination from '../Pagination';
|
||||||
|
|
||||||
import { parseQueryString } from '../../qs';
|
import { parseQueryString } from '../../util/qs';
|
||||||
|
|
||||||
class Notifications extends Component {
|
class Notifications extends Component {
|
||||||
columns = [
|
columns = [
|
||||||
@@ -117,58 +119,71 @@ class Notifications extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createError (id, isCurrentlyOn) {
|
async createError (id, isCurrentlyOn) {
|
||||||
const { onCreateError, match } = this.props;
|
const { onCreateError, match, handleHttpError } = this.props;
|
||||||
const postParams = { id };
|
const postParams = { id };
|
||||||
|
let errorHandled;
|
||||||
if (isCurrentlyOn) {
|
if (isCurrentlyOn) {
|
||||||
postParams.disassociate = true;
|
postParams.disassociate = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await onCreateError(match.params.id, postParams);
|
await onCreateError(match.params.id, postParams);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
errorHandled = handleHttpError(err);
|
||||||
|
if (!errorHandled) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isCurrentlyOn) {
|
if (!errorHandled) {
|
||||||
// Remove it from state
|
if (isCurrentlyOn) {
|
||||||
this.setState((prevState) => ({
|
// Remove it from state
|
||||||
errorTemplateIds: prevState.errorTemplateIds.filter((templateId) => templateId !== id)
|
this.setState((prevState) => ({
|
||||||
}));
|
errorTemplateIds: prevState.errorTemplateIds.filter((templateId) => templateId !== id)
|
||||||
} else {
|
}));
|
||||||
// Add it to state
|
} else {
|
||||||
this.setState(prevState => ({
|
// Add it to state
|
||||||
errorTemplateIds: [...prevState.errorTemplateIds, id]
|
this.setState(prevState => ({
|
||||||
}));
|
errorTemplateIds: [...prevState.errorTemplateIds, id]
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSuccess (id, isCurrentlyOn) {
|
async createSuccess (id, isCurrentlyOn) {
|
||||||
const { onCreateSuccess, match } = this.props;
|
const { onCreateSuccess, match, handleHttpError } = this.props;
|
||||||
const postParams = { id };
|
const postParams = { id };
|
||||||
|
let errorHandled;
|
||||||
if (isCurrentlyOn) {
|
if (isCurrentlyOn) {
|
||||||
postParams.disassociate = true;
|
postParams.disassociate = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await onCreateSuccess(match.params.id, postParams);
|
await onCreateSuccess(match.params.id, postParams);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
errorHandled = handleHttpError(err);
|
||||||
|
if (!errorHandled) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isCurrentlyOn) {
|
if (!errorHandled) {
|
||||||
// Remove it from state
|
if (isCurrentlyOn) {
|
||||||
this.setState((prevState) => ({
|
// Remove it from state
|
||||||
successTemplateIds: prevState.successTemplateIds.filter((templateId) => templateId !== id)
|
this.setState((prevState) => ({
|
||||||
}));
|
successTemplateIds: prevState.successTemplateIds
|
||||||
} else {
|
.filter((templateId) => templateId !== id)
|
||||||
// Add it to state
|
}));
|
||||||
this.setState(prevState => ({
|
} else {
|
||||||
successTemplateIds: [...prevState.successTemplateIds, id]
|
// Add it to state
|
||||||
}));
|
this.setState(prevState => ({
|
||||||
|
successTemplateIds: [...prevState.successTemplateIds, id]
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readNotifications (queryParams) {
|
async readNotifications (queryParams) {
|
||||||
const { noInitialResults } = this.state;
|
const { noInitialResults } = this.state;
|
||||||
const { onReadNotifications, onReadSuccess, onReadError, match } = this.props;
|
const { onReadNotifications, onReadSuccess, onReadError, match, handleHttpError } = this.props;
|
||||||
const { page, page_size, order_by } = queryParams;
|
const { page, page_size, order_by } = queryParams;
|
||||||
|
|
||||||
let sortOrder = 'ascending';
|
let sortOrder = 'ascending';
|
||||||
@@ -233,12 +248,11 @@ class Notifications extends Component {
|
|||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
successTemplateIds,
|
successTemplateIds,
|
||||||
errorTemplateIds
|
errorTemplateIds,
|
||||||
|
loading: false
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
handleHttpError(err) || this.setState({ error: true, loading: false });
|
||||||
} finally {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,4 +343,5 @@ Notifications.propTypes = {
|
|||||||
onCreateSuccess: PropTypes.func.isRequired,
|
onCreateSuccess: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Notifications;
|
export { Notifications as _Notifications };
|
||||||
|
export default withNetwork(Notifications);
|
||||||
|
|||||||
44
src/components/NotifyAndRedirect.jsx
Normal file
44
src/components/NotifyAndRedirect.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Redirect, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { withRootDialog } from '../contexts/RootDialog';
|
||||||
|
|
||||||
|
const NotifyAndRedirect = ({
|
||||||
|
to,
|
||||||
|
push,
|
||||||
|
from,
|
||||||
|
exact,
|
||||||
|
strict,
|
||||||
|
sensitive,
|
||||||
|
setRootDialogMessage,
|
||||||
|
location
|
||||||
|
}) => {
|
||||||
|
setRootDialogMessage({
|
||||||
|
title: '404',
|
||||||
|
bodyText: (
|
||||||
|
<Trans>
|
||||||
|
Cannot find route
|
||||||
|
<strong>{` ${location.pathname}`}</strong>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
|
variant: 'warning'
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Redirect
|
||||||
|
to={to}
|
||||||
|
push={push}
|
||||||
|
from={from}
|
||||||
|
exact={exact}
|
||||||
|
strict={strict}
|
||||||
|
sensitive={sensitive}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotifyAndRedirect as _NotifyAndRedirect };
|
||||||
|
export default withRootDialog(withRouter(NotifyAndRedirect));
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export const ConfigContext = React.createContext({});
|
|
||||||
89
src/contexts/Config.jsx
Normal file
89
src/contexts/Config.jsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { withNetwork } from './Network';
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.fetchConfig = this.fetchConfig.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.fetchConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchConfig () {
|
||||||
|
const { api, handleHttpError } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version
|
||||||
|
}
|
||||||
|
} = await api.getConfig();
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
custom_logo,
|
||||||
|
custom_login_info
|
||||||
|
}
|
||||||
|
} = await api.getRoot();
|
||||||
|
this.setState({
|
||||||
|
value: {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version,
|
||||||
|
custom_logo,
|
||||||
|
custom_login_info
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
handleHttpError(err) || this.setState({
|
||||||
|
value: {
|
||||||
|
ansible_version: null,
|
||||||
|
custom_virtualenvs: null,
|
||||||
|
version: null,
|
||||||
|
custom_logo: null,
|
||||||
|
custom_login_info: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
value: stateValue
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const { value: propsValue, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigContext.Provider value={propsValue || stateValue}>
|
||||||
|
{children}
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfigProvider = withNetwork(Provider);
|
||||||
|
|
||||||
|
export const Config = ({ children }) => (
|
||||||
|
<ConfigContext.Consumer>
|
||||||
|
{value => children(value)}
|
||||||
|
</ConfigContext.Consumer>
|
||||||
|
);
|
||||||
85
src/contexts/Network.jsx
Normal file
85
src/contexts/Network.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { i18nMark } from '@lingui/react';
|
||||||
|
|
||||||
|
import { withRootDialog } from './RootDialog';
|
||||||
|
|
||||||
|
import APIClient from '../api';
|
||||||
|
|
||||||
|
const NetworkContext = React.createContext({});
|
||||||
|
|
||||||
|
class Provider extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
value: {
|
||||||
|
api: new APIClient(axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' })),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handle401 () {
|
||||||
|
const { handle401, history, setRootDialogMessage } = this.props;
|
||||||
|
if (handle401) {
|
||||||
|
handle401();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
history.replace('/login');
|
||||||
|
setRootDialogMessage({
|
||||||
|
bodyText: i18nMark('You have been logged out.')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handle404 () {
|
||||||
|
const { handle404, history, setRootDialogMessage } = this.props;
|
||||||
|
if (handle404) {
|
||||||
|
handle404();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
history.replace('/home');
|
||||||
|
setRootDialogMessage({
|
||||||
|
title: i18nMark('404'),
|
||||||
|
bodyText: i18nMark('Cannot find resource.'),
|
||||||
|
variant: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
value: stateValue
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const { value: propsValue, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NetworkContext.Provider value={propsValue || stateValue}>
|
||||||
|
{children}
|
||||||
|
</NetworkContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Provider as _NetworkProvider };
|
||||||
|
export const NetworkProvider = withRootDialog(withRouter(Provider));
|
||||||
|
|
||||||
|
export function withNetwork (Child) {
|
||||||
|
return (props) => (
|
||||||
|
<NetworkContext.Consumer>
|
||||||
|
{context => <Child {...props} {...context} />}
|
||||||
|
</NetworkContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
53
src/contexts/RootDialog.jsx
Normal file
53
src/contexts/RootDialog.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
435
src/index.jsx
435
src/index.jsx
@@ -1,15 +1,12 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import {
|
import {
|
||||||
HashRouter,
|
|
||||||
Redirect,
|
|
||||||
Route,
|
Route,
|
||||||
Switch,
|
Switch,
|
||||||
|
Redirect
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
I18n,
|
I18n
|
||||||
I18nProvider,
|
|
||||||
} from '@lingui/react';
|
} from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
@@ -19,10 +16,14 @@ import './components/Pagination/styles.scss';
|
|||||||
import './components/DataListToolbar/styles.scss';
|
import './components/DataListToolbar/styles.scss';
|
||||||
import './components/SelectedList/styles.scss';
|
import './components/SelectedList/styles.scss';
|
||||||
|
|
||||||
import APIClient from './api';
|
import { Config } from './contexts/Config';
|
||||||
|
|
||||||
import App from './App';
|
|
||||||
import Background from './components/Background';
|
import Background from './components/Background';
|
||||||
|
import NotifyAndRedirect from './components/NotifyAndRedirect';
|
||||||
|
|
||||||
|
import RootProvider from './RootProvider';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
import Applications from './pages/Applications';
|
import Applications from './pages/Applications';
|
||||||
import Credentials from './pages/Credentials';
|
import Credentials from './pages/Credentials';
|
||||||
import CredentialTypes from './pages/CredentialTypes';
|
import CredentialTypes from './pages/CredentialTypes';
|
||||||
@@ -46,242 +47,204 @@ import License from './pages/License';
|
|||||||
import Teams from './pages/Teams';
|
import Teams from './pages/Teams';
|
||||||
import Templates from './pages/Templates';
|
import Templates from './pages/Templates';
|
||||||
import Users from './pages/Users';
|
import Users from './pages/Users';
|
||||||
import ja from '../build/locales/ja/messages';
|
|
||||||
import en from '../build/locales/en/messages';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Initialize http
|
|
||||||
//
|
|
||||||
|
|
||||||
const http = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken' });
|
|
||||||
|
|
||||||
//
|
|
||||||
// Derive the language and region from global user agent data. Example: es-US
|
|
||||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/Navigator
|
|
||||||
//
|
|
||||||
|
|
||||||
export function getLanguage (nav) {
|
|
||||||
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
|
||||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
|
||||||
|
|
||||||
return languageWithoutRegionCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Function Main
|
|
||||||
//
|
|
||||||
|
|
||||||
export async function main (render, api) {
|
|
||||||
const catalogs = { en, ja };
|
|
||||||
const language = getLanguage(navigator);
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export function main (render) {
|
||||||
const el = document.getElementById('app');
|
const el = document.getElementById('app');
|
||||||
const { data: { custom_logo, custom_login_info } } = await api.getRoot();
|
|
||||||
|
|
||||||
const defaultRedirect = () => (<Redirect to="/home" />);
|
|
||||||
const loginRoutes = (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
path="/login"
|
|
||||||
render={() => (
|
|
||||||
<Login
|
|
||||||
api={api}
|
|
||||||
logo={custom_logo}
|
|
||||||
loginInfo={custom_login_info}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Redirect to="/login" />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
<HashRouter>
|
<RootProvider>
|
||||||
<I18nProvider
|
<I18n>
|
||||||
language={language}
|
{({ i18n }) => (
|
||||||
catalogs={catalogs}
|
<Background>
|
||||||
>
|
<Switch>
|
||||||
<I18n>
|
<Route
|
||||||
{({ i18n }) => (
|
path="/login"
|
||||||
<Background>
|
render={() => (
|
||||||
{!api.isAuthenticated() ? loginRoutes : (
|
<Config>
|
||||||
<Switch>
|
{({ custom_logo, custom_login_info }) => (
|
||||||
<Route path="/login" render={defaultRedirect} />
|
<Login
|
||||||
<Route exact path="/" render={defaultRedirect} />
|
logo={custom_logo}
|
||||||
<Route
|
loginInfo={custom_login_info}
|
||||||
render={() => (
|
|
||||||
<App
|
|
||||||
api={api}
|
|
||||||
navLabel={i18n._(t`Primary Navigation`)}
|
|
||||||
routeGroups={[
|
|
||||||
{
|
|
||||||
groupTitle: i18n._(t`Views`),
|
|
||||||
groupId: 'views_group',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
title: i18n._(t`Dashboard`),
|
|
||||||
path: '/home',
|
|
||||||
component: Dashboard
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Jobs`),
|
|
||||||
path: '/jobs',
|
|
||||||
component: Jobs
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Schedules`),
|
|
||||||
path: '/schedules',
|
|
||||||
component: Schedules
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`My View`),
|
|
||||||
path: '/portal',
|
|
||||||
component: Portal
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
groupTitle: i18n._(t`Resources`),
|
|
||||||
groupId: 'resources_group',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
title: i18n._(t`Templates`),
|
|
||||||
path: '/templates',
|
|
||||||
component: Templates
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Credentials`),
|
|
||||||
path: '/credentials',
|
|
||||||
component: Credentials
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Projects`),
|
|
||||||
path: '/projects',
|
|
||||||
component: Projects
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Inventories`),
|
|
||||||
path: '/inventories',
|
|
||||||
component: Inventories
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Inventory Scripts`),
|
|
||||||
path: '/inventory_scripts',
|
|
||||||
component: InventoryScripts
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
groupTitle: i18n._(t`Access`),
|
|
||||||
groupId: 'access_group',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
title: i18n._(t`Organizations`),
|
|
||||||
path: '/organizations',
|
|
||||||
component: Organizations
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Users`),
|
|
||||||
path: '/users',
|
|
||||||
component: Users
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Teams`),
|
|
||||||
path: '/teams',
|
|
||||||
component: Teams
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
groupTitle: i18n._(t`Administration`),
|
|
||||||
groupId: 'administration_group',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
title: i18n._(t`Credential Types`),
|
|
||||||
path: '/credential_types',
|
|
||||||
component: CredentialTypes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Notifications`),
|
|
||||||
path: '/notification_templates',
|
|
||||||
component: NotificationTemplates
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Management Jobs`),
|
|
||||||
path: '/management_jobs',
|
|
||||||
component: ManagementJobs
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Instance Groups`),
|
|
||||||
path: '/instance_groups',
|
|
||||||
component: InstanceGroups
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Integrations`),
|
|
||||||
path: '/applications',
|
|
||||||
component: Applications
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
groupTitle: i18n._(t`Settings`),
|
|
||||||
groupId: 'settings_group',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
title: i18n._(t`Authentication`),
|
|
||||||
path: '/auth_settings',
|
|
||||||
component: AuthSettings
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`Jobs`),
|
|
||||||
path: '/jobs_settings',
|
|
||||||
component: JobsSettings
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`System`),
|
|
||||||
path: '/system_settings',
|
|
||||||
component: SystemSettings
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`User Interface`),
|
|
||||||
path: '/ui_settings',
|
|
||||||
component: UISettings
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: i18n._(t`License`),
|
|
||||||
path: '/license',
|
|
||||||
component: License
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
render={({ routeGroups }) => (
|
|
||||||
routeGroups
|
|
||||||
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
|
||||||
.map(({ component: PageComponent, path }) => (
|
|
||||||
<Route
|
|
||||||
key={path}
|
|
||||||
path={path}
|
|
||||||
render={({ match }) => (
|
|
||||||
<PageComponent
|
|
||||||
api={api}
|
|
||||||
match={match}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</Config>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route exact path="/" render={() => <Redirect to="/home" />} />
|
||||||
|
<Route
|
||||||
|
render={() => (
|
||||||
|
<App
|
||||||
|
navLabel={i18n._(t`Primary Navigation`)}
|
||||||
|
routeGroups={[
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Views`),
|
||||||
|
groupId: 'views_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Dashboard`),
|
||||||
|
path: '/home',
|
||||||
|
component: Dashboard
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Jobs`),
|
||||||
|
path: '/jobs',
|
||||||
|
component: Jobs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Schedules`),
|
||||||
|
path: '/schedules',
|
||||||
|
component: Schedules
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`My View`),
|
||||||
|
path: '/portal',
|
||||||
|
component: Portal
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Resources`),
|
||||||
|
groupId: 'resources_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Templates`),
|
||||||
|
path: '/templates',
|
||||||
|
component: Templates
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Credentials`),
|
||||||
|
path: '/credentials',
|
||||||
|
component: Credentials
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Projects`),
|
||||||
|
path: '/projects',
|
||||||
|
component: Projects
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Inventories`),
|
||||||
|
path: '/inventories',
|
||||||
|
component: Inventories
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Inventory Scripts`),
|
||||||
|
path: '/inventory_scripts',
|
||||||
|
component: InventoryScripts
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Access`),
|
||||||
|
groupId: 'access_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Organizations`),
|
||||||
|
path: '/organizations',
|
||||||
|
component: Organizations
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Users`),
|
||||||
|
path: '/users',
|
||||||
|
component: Users
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Teams`),
|
||||||
|
path: '/teams',
|
||||||
|
component: Teams
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Administration`),
|
||||||
|
groupId: 'administration_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Credential Types`),
|
||||||
|
path: '/credential_types',
|
||||||
|
component: CredentialTypes
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Notifications`),
|
||||||
|
path: '/notification_templates',
|
||||||
|
component: NotificationTemplates
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Management Jobs`),
|
||||||
|
path: '/management_jobs',
|
||||||
|
component: ManagementJobs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Instance Groups`),
|
||||||
|
path: '/instance_groups',
|
||||||
|
component: InstanceGroups
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Integrations`),
|
||||||
|
path: '/applications',
|
||||||
|
component: Applications
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupTitle: i18n._(t`Settings`),
|
||||||
|
groupId: 'settings_group',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
title: i18n._(t`Authentication`),
|
||||||
|
path: '/auth_settings',
|
||||||
|
component: AuthSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Jobs`),
|
||||||
|
path: '/jobs_settings',
|
||||||
|
component: JobsSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`System`),
|
||||||
|
path: '/system_settings',
|
||||||
|
component: SystemSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`User Interface`),
|
||||||
|
path: '/ui_settings',
|
||||||
|
component: UISettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`License`),
|
||||||
|
path: '/license',
|
||||||
|
component: License
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
render={({ routeGroups }) => (
|
||||||
|
<Switch>
|
||||||
|
{routeGroups
|
||||||
|
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
||||||
|
.map(({ component: PageComponent, path }) => (
|
||||||
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={path}
|
||||||
|
render={({ match }) => (
|
||||||
|
<PageComponent match={match} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.concat([
|
||||||
|
<NotifyAndRedirect key="redirect" to="/" />
|
||||||
|
])}
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
)}
|
||||||
)}
|
/>
|
||||||
</Background>
|
</Switch>
|
||||||
)}
|
</Background>
|
||||||
</I18n>
|
)}
|
||||||
</I18nProvider>
|
</I18n>
|
||||||
</HashRouter>, el
|
</RootProvider>, el || document.createElement('div')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main(ReactDOM.render, new APIClient(http));
|
main(ReactDOM.render);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect, withRouter } from 'react-router-dom';
|
||||||
import { I18n } from '@lingui/react';
|
import { I18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
LoginPage,
|
LoginPage,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { withRootDialog } from '../contexts/RootDialog';
|
||||||
|
import { withNetwork } from '../contexts/Network';
|
||||||
|
|
||||||
import towerLogo from '../../images/tower-logo-header.svg';
|
import towerLogo from '../../images/tower-logo-header.svg';
|
||||||
|
|
||||||
class AWXLogin extends Component {
|
class AWXLogin extends Component {
|
||||||
@@ -17,7 +20,8 @@ class AWXLogin extends Component {
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
isInputValid: true,
|
isInputValid: true,
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
isAuthenticated: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onChangeUsername = this.onChangeUsername.bind(this);
|
this.onChangeUsername = this.onChangeUsername.bind(this);
|
||||||
@@ -35,7 +39,7 @@ class AWXLogin extends Component {
|
|||||||
|
|
||||||
async onLoginButtonClick (event) {
|
async onLoginButtonClick (event) {
|
||||||
const { username, password, isLoading } = this.state;
|
const { username, password, isLoading } = this.state;
|
||||||
const { api } = this.props;
|
const { api, handleHttpError, clearRootDialogMessage } = this.props;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -43,25 +47,23 @@ class AWXLogin extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearRootDialogMessage();
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.login(username, password);
|
await api.login(username, password);
|
||||||
|
this.setState({ isAuthenticated: true, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response && error.response.status === 401) {
|
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
||||||
this.setState({ isInputValid: false });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { username, password, isInputValid } = this.state;
|
const { username, password, isInputValid, isAuthenticated } = this.state;
|
||||||
const { api, alt, loginInfo, logo } = this.props;
|
const { alt, loginInfo, logo, bodyText: errorMessage } = this.props;
|
||||||
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
|
||||||
|
|
||||||
if (api.isAuthenticated()) {
|
if (isAuthenticated) {
|
||||||
return (<Redirect to="/" />);
|
return (<Redirect to="/" />);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,10 +77,11 @@ class AWXLogin extends Component {
|
|||||||
textContent={loginInfo}
|
textContent={loginInfo}
|
||||||
>
|
>
|
||||||
<LoginForm
|
<LoginForm
|
||||||
|
className={errorMessage && 'pf-m-error'}
|
||||||
usernameLabel={i18n._(t`Username`)}
|
usernameLabel={i18n._(t`Username`)}
|
||||||
passwordLabel={i18n._(t`Password`)}
|
passwordLabel={i18n._(t`Password`)}
|
||||||
showHelperText={!isInputValid}
|
showHelperText={!isInputValid || !!errorMessage}
|
||||||
helperText={i18n._(t`Invalid username or password. Please try again.`)}
|
helperText={errorMessage || i18n._(t`Invalid username or password. Please try again.`)}
|
||||||
usernameValue={username}
|
usernameValue={username}
|
||||||
passwordValue={password}
|
passwordValue={password}
|
||||||
isValidUsername={isInputValid}
|
isValidUsername={isInputValid}
|
||||||
@@ -94,4 +97,5 @@ class AWXLogin extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AWXLogin;
|
export { AWXLogin as _AWXLogin };
|
||||||
|
export default withNetwork(withRootDialog(withRouter(AWXLogin)));
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, withRouter, Switch } from 'react-router-dom';
|
||||||
import { i18nMark } from '@lingui/react';
|
import { i18nMark } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { NetworkProvider } from '../../contexts/Network';
|
||||||
|
import { withRootDialog } from '../../contexts/RootDialog';
|
||||||
|
|
||||||
|
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||||
|
|
||||||
import OrganizationsList from './screens/OrganizationsList';
|
import OrganizationsList from './screens/OrganizationsList';
|
||||||
import OrganizationAdd from './screens/OrganizationAdd';
|
import OrganizationAdd from './screens/OrganizationAdd';
|
||||||
import Organization from './screens/Organization/Organization';
|
import Organization from './screens/Organization/Organization';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
|
||||||
|
|
||||||
class Organizations extends Component {
|
class Organizations extends Component {
|
||||||
state = {
|
state = {
|
||||||
@@ -13,7 +18,7 @@ class Organizations extends Component {
|
|||||||
'/organizations': i18nMark('Organizations'),
|
'/organizations': i18nMark('Organizations'),
|
||||||
'/organizations/add': i18nMark('Create New Organization')
|
'/organizations/add': i18nMark('Create New Organization')
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
setBreadcrumbConfig = (organization) => {
|
setBreadcrumbConfig = (organization) => {
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
@@ -35,7 +40,7 @@ class Organizations extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { match, api, history, location } = this.props;
|
const { match, history, location, setRootDialogMessage } = this.props;
|
||||||
const { breadcrumbConfig } = this.state;
|
const { breadcrumbConfig } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,28 +52,40 @@ class Organizations extends Component {
|
|||||||
<Route
|
<Route
|
||||||
path={`${match.path}/add`}
|
path={`${match.path}/add`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationAdd
|
<OrganizationAdd />
|
||||||
api={api}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}/:id`}
|
path={`${match.path}/:id`}
|
||||||
render={() => (
|
render={({ match: newRouteMatch }) => (
|
||||||
<Organization
|
<NetworkProvider
|
||||||
api={api}
|
handle404={() => {
|
||||||
history={history}
|
history.replace('/organizations');
|
||||||
location={location}
|
setRootDialogMessage({
|
||||||
setBreadcrumb={this.setBreadcrumbConfig}
|
title: '404',
|
||||||
/>
|
bodyText: (
|
||||||
|
<Trans>
|
||||||
|
Cannot find organization with ID
|
||||||
|
<strong>{` ${newRouteMatch.params.id}`}</strong>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
|
variant: 'warning'
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Organization
|
||||||
|
history={history}
|
||||||
|
location={location}
|
||||||
|
setBreadcrumb={this.setBreadcrumbConfig}
|
||||||
|
/>
|
||||||
|
</NetworkProvider>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}`}
|
path={`${match.path}`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationsList
|
<OrganizationsList />
|
||||||
api={api}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -77,4 +94,5 @@ class Organizations extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Organizations;
|
export { Organizations as _Organizations };
|
||||||
|
export default withRootDialog(withRouter(Organizations));
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { t } from '@lingui/macro';
|
|||||||
|
|
||||||
import Lookup from '../../../components/Lookup';
|
import Lookup from '../../../components/Lookup';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../contexts/Network';
|
||||||
|
|
||||||
const INSTANCE_GROUPS_LOOKUP_COLUMNS = [
|
const INSTANCE_GROUPS_LOOKUP_COLUMNS = [
|
||||||
{ name: i18nMark('Name'), key: 'name', isSortable: true },
|
{ name: i18nMark('Name'), key: 'name', isSortable: true },
|
||||||
{ name: i18nMark('Modified'), key: 'modified', isSortable: false, isNumeric: true },
|
{ name: i18nMark('Modified'), key: 'modified', isSortable: false, isNumeric: true },
|
||||||
@@ -69,9 +71,6 @@ class InstanceGroupsLookup extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InstanceGroupsLookup.propTypes = {
|
InstanceGroupsLookup.propTypes = {
|
||||||
api: PropTypes.shape({
|
|
||||||
getInstanceGroups: PropTypes.func,
|
|
||||||
}).isRequired,
|
|
||||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
@@ -81,4 +80,4 @@ InstanceGroupsLookup.defaultProps = {
|
|||||||
tooltip: '',
|
tooltip: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InstanceGroupsLookup;
|
export default withNetwork(InstanceGroupsLookup);
|
||||||
|
|||||||
@@ -3,22 +3,25 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DataList, DataListItem, DataListCell, Text,
|
DataList, DataListItem, DataListCell, Text,
|
||||||
TextContent, TextVariants, Chip, Alert, AlertActionCloseButton, Button
|
TextContent, TextVariants, Chip, Button
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { I18n, i18nMark } from '@lingui/react';
|
import { I18n, i18nMark } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t, Trans } from '@lingui/macro';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Link
|
Link
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../contexts/Network';
|
||||||
|
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseQueryString,
|
parseQueryString,
|
||||||
} from '../../../qs';
|
} from '../../../util/qs';
|
||||||
|
|
||||||
const userRolesWrapperStyle = {
|
const userRolesWrapperStyle = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -237,20 +240,16 @@ class OrganizationAccessList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeAccessRole (roleId, resourceId, type) {
|
async removeAccessRole (roleId, resourceId, type) {
|
||||||
const { removeRole } = this.props;
|
const { removeRole, handleHttpError } = this.props;
|
||||||
const url = `/api/v2/${type}/${resourceId}/roles/`;
|
const url = `/api/v2/${type}/${resourceId}/roles/`;
|
||||||
try {
|
try {
|
||||||
await removeRole(url, roleId);
|
await removeRole(url, roleId);
|
||||||
|
const queryParams = this.getQueryParams();
|
||||||
|
await this.fetchOrgAccessList(queryParams);
|
||||||
|
this.setState({ showWarning: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ error });
|
handleHttpError(error) || this.setState({ error });
|
||||||
}
|
}
|
||||||
const queryParams = this.getQueryParams();
|
|
||||||
try {
|
|
||||||
this.fetchOrgAccessList(queryParams);
|
|
||||||
} catch (error) {
|
|
||||||
this.setState({ error });
|
|
||||||
}
|
|
||||||
this.setState({ showWarning: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWarning (roleName, roleId, resourceName, resourceId, type) {
|
handleWarning (roleName, roleId, resourceName, resourceId, type) {
|
||||||
@@ -258,16 +257,33 @@ class OrganizationAccessList extends React.Component {
|
|||||||
let warningMsg;
|
let warningMsg;
|
||||||
|
|
||||||
if (type === 'users') {
|
if (type === 'users') {
|
||||||
warningTitle = i18nMark('User Access Removal');
|
warningTitle = i18nMark('Remove User Access');
|
||||||
warningMsg = i18nMark(`Please confirm that you would like to remove ${roleName}
|
warningMsg = (
|
||||||
access from ${resourceName}.`);
|
<Trans>
|
||||||
|
Are you sure you want to remove
|
||||||
|
<b>{` ${roleName} `}</b>
|
||||||
|
access from
|
||||||
|
<strong>{` ${resourceName}`}</strong>
|
||||||
|
?
|
||||||
|
</Trans>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (type === 'teams') {
|
if (type === 'teams') {
|
||||||
warningTitle = i18nMark('Team Access Removal');
|
warningTitle = i18nMark('Remove Team Access');
|
||||||
warningMsg = i18nMark(`Please confirm that you would like to remove ${roleName}
|
warningMsg = (
|
||||||
access from the team ${resourceName}. This will affect all
|
<Trans>
|
||||||
members of the team. If you would like to only remove access
|
Are you sure you want to remove
|
||||||
for this particular user, please remove them from the team.`);
|
<b>{` ${roleName} `}</b>
|
||||||
|
access from
|
||||||
|
<b>{` ${resourceName}`}</b>
|
||||||
|
? Doing so affects all members of the team.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you
|
||||||
|
<b><i> only </i></b>
|
||||||
|
want to remove access for this particular user, please remove them from the team.
|
||||||
|
</Trans>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -333,17 +349,18 @@ class OrganizationAccessList extends React.Component {
|
|||||||
showExpandCollapse
|
showExpandCollapse
|
||||||
/>
|
/>
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<Alert
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={warningTitle}
|
title={warningTitle}
|
||||||
action={<AlertActionCloseButton onClose={this.hideWarning} />}
|
isOpen={showWarning}
|
||||||
|
onClose={this.hideWarning}
|
||||||
|
actions={[
|
||||||
|
<Button key="delete" variant="danger" aria-label="Confirm delete" onClick={this.confirmDelete}>{i18n._(t`Delete`)}</Button>,
|
||||||
|
<Button key="cancel" variant="secondary" onClick={this.hideWarning}>{i18n._(t`Cancel`)}</Button>
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{warningMsg}
|
{warningMsg}
|
||||||
<span className="awx-c-form-action-group">
|
</AlertModal>
|
||||||
<Button variant="danger" aria-label="confirm-delete" onClick={this.confirmDelete}>Delete</Button>
|
|
||||||
<Button variant="secondary" onClick={this.hideWarning}>Cancel</Button>
|
|
||||||
</span>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
<DataList aria-label={i18n._(t`Access List`)}>
|
<DataList aria-label={i18n._(t`Access List`)}>
|
||||||
{results.map(result => (
|
{results.map(result => (
|
||||||
@@ -430,4 +447,5 @@ OrganizationAccessList.propTypes = {
|
|||||||
removeRole: PropTypes.func.isRequired,
|
removeRole: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OrganizationAccessList;
|
export { OrganizationAccessList as _OrganizationAccessList };
|
||||||
|
export default withNetwork(OrganizationAccessList);
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { ConfigContext } from '../../../context';
|
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';
|
import FormActionGroup from '../../../components/FormActionGroup';
|
||||||
@@ -27,6 +28,7 @@ class OrganizationForm extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
instanceGroups: [],
|
instanceGroups: [],
|
||||||
|
initialInstanceGroups: [],
|
||||||
formIsValid: true,
|
formIsValid: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ class OrganizationForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { api, organization, handleCancel } = this.props;
|
const { organization, handleCancel } = this.props;
|
||||||
const { instanceGroups, formIsValid, error } = this.state;
|
const { instanceGroups, formIsValid, error } = this.state;
|
||||||
const defaultVenv = '/venv/ansible/';
|
const defaultVenv = '/venv/ansible/';
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ class OrganizationForm extends Component {
|
|||||||
type="text"
|
type="text"
|
||||||
label={i18n._(t`Description`)}
|
label={i18n._(t`Description`)}
|
||||||
/>
|
/>
|
||||||
<ConfigContext.Consumer>
|
<Config>
|
||||||
{({ custom_virtualenvs }) => (
|
{({ custom_virtualenvs }) => (
|
||||||
custom_virtualenvs && custom_virtualenvs.length > 1 && (
|
custom_virtualenvs && custom_virtualenvs.length > 1 && (
|
||||||
<Field
|
<Field
|
||||||
@@ -133,10 +135,9 @@ class OrganizationForm extends Component {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</ConfigContext.Consumer>
|
</Config>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<InstanceGroupsLookup
|
<InstanceGroupsLookup
|
||||||
api={api}
|
|
||||||
value={instanceGroups}
|
value={instanceGroups}
|
||||||
onChange={this.handleInstanceGroupsChange}
|
onChange={this.handleInstanceGroupsChange}
|
||||||
tooltip={i18n._(t`Select the Instance Groups for this Organization to run on.`)}
|
tooltip={i18n._(t`Select the Instance Groups for this Organization to run on.`)}
|
||||||
@@ -157,7 +158,6 @@ class OrganizationForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OrganizationForm.propTypes = {
|
OrganizationForm.propTypes = {
|
||||||
api: PropTypes.shape().isRequired,
|
|
||||||
organization: PropTypes.shape(),
|
organization: PropTypes.shape(),
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
handleCancel: PropTypes.func.isRequired,
|
handleCancel: PropTypes.func.isRequired,
|
||||||
@@ -175,4 +175,5 @@ OrganizationForm.contextTypes = {
|
|||||||
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(OrganizationForm);
|
export { OrganizationForm as _OrganizationForm };
|
||||||
|
export default withNetwork(withRouter(OrganizationForm));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { withRouter, Link } from 'react-router-dom';
|
|||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
|
|
||||||
import { encodeQueryString } from '../../../qs';
|
import { encodeQueryString } from '../../../util/qs';
|
||||||
|
|
||||||
const detailWrapperStyle = {
|
const detailWrapperStyle = {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
@@ -13,13 +13,17 @@ import {
|
|||||||
PageSection
|
PageSection
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../../contexts/Network';
|
||||||
|
|
||||||
|
import Tabs from '../../../../components/Tabs/Tabs';
|
||||||
|
import Tab from '../../../../components/Tabs/Tab';
|
||||||
|
import NotifyAndRedirect from '../../../../components/NotifyAndRedirect';
|
||||||
|
|
||||||
import OrganizationAccess from './OrganizationAccess';
|
import OrganizationAccess from './OrganizationAccess';
|
||||||
import OrganizationDetail from './OrganizationDetail';
|
import OrganizationDetail from './OrganizationDetail';
|
||||||
import OrganizationEdit from './OrganizationEdit';
|
import OrganizationEdit from './OrganizationEdit';
|
||||||
import OrganizationNotifications from './OrganizationNotifications';
|
import OrganizationNotifications from './OrganizationNotifications';
|
||||||
import OrganizationTeams from './OrganizationTeams';
|
import OrganizationTeams from './OrganizationTeams';
|
||||||
import Tabs from '../../../../components/Tabs/Tabs';
|
|
||||||
import Tab from '../../../../components/Tabs/Tab';
|
|
||||||
|
|
||||||
class Organization extends Component {
|
class Organization extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -47,18 +51,18 @@ class Organization extends Component {
|
|||||||
|
|
||||||
async fetchOrganization () {
|
async fetchOrganization () {
|
||||||
const {
|
const {
|
||||||
api,
|
|
||||||
match,
|
match,
|
||||||
setBreadcrumb
|
setBreadcrumb,
|
||||||
|
api,
|
||||||
|
handleHttpError
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.getOrganizationDetails(+match.params.id);
|
const { data } = await api.getOrganizationDetails(parseInt(match.params.id, 10));
|
||||||
this.setState({ organization: data });
|
|
||||||
setBreadcrumb(data);
|
setBreadcrumb(data);
|
||||||
|
this.setState({ organization: data, loading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ error: true });
|
handleHttpError(error) || this.setState({ error: true, loading: false });
|
||||||
} finally {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +70,6 @@ class Organization extends Component {
|
|||||||
const {
|
const {
|
||||||
location,
|
location,
|
||||||
match,
|
match,
|
||||||
api,
|
|
||||||
history
|
history
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -125,7 +128,6 @@ class Organization extends Component {
|
|||||||
path="/organizations/:id/edit"
|
path="/organizations/:id/edit"
|
||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationEdit
|
<OrganizationEdit
|
||||||
api={api}
|
|
||||||
match={match}
|
match={match}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
/>
|
/>
|
||||||
@@ -137,7 +139,6 @@ class Organization extends Component {
|
|||||||
path="/organizations/:id/details"
|
path="/organizations/:id/details"
|
||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationDetail
|
<OrganizationDetail
|
||||||
api={api}
|
|
||||||
match={match}
|
match={match}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
/>
|
/>
|
||||||
@@ -148,7 +149,6 @@ class Organization extends Component {
|
|||||||
path="/organizations/:id/access"
|
path="/organizations/:id/access"
|
||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationAccess
|
<OrganizationAccess
|
||||||
api={api}
|
|
||||||
match={match}
|
match={match}
|
||||||
location={location}
|
location={location}
|
||||||
history={history}
|
history={history}
|
||||||
@@ -160,7 +160,9 @@ class Organization extends Component {
|
|||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationTeams
|
<OrganizationTeams
|
||||||
id={Number(match.params.id)}
|
id={Number(match.params.id)}
|
||||||
api={api}
|
match={match}
|
||||||
|
location={location}
|
||||||
|
history={history}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -168,13 +170,13 @@ class Organization extends Component {
|
|||||||
path="/organizations/:id/notifications"
|
path="/organizations/:id/notifications"
|
||||||
render={() => (
|
render={() => (
|
||||||
<OrganizationNotifications
|
<OrganizationNotifications
|
||||||
api={api}
|
|
||||||
match={match}
|
match={match}
|
||||||
location={location}
|
location={location}
|
||||||
history={history}
|
history={history}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
||||||
</Switch>
|
</Switch>
|
||||||
{error ? 'error!' : ''}
|
{error ? 'error!' : ''}
|
||||||
{loading ? 'loading...' : ''}
|
{loading ? 'loading...' : ''}
|
||||||
@@ -184,4 +186,4 @@ class Organization extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Organization);
|
export default withNetwork(withRouter(Organization));
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../../contexts/Network';
|
||||||
|
|
||||||
import OrganizationAccessList from '../../components/OrganizationAccessList';
|
import OrganizationAccessList from '../../components/OrganizationAccessList';
|
||||||
|
|
||||||
class OrganizationAccess extends React.Component {
|
class OrganizationAccess extends React.Component {
|
||||||
@@ -38,4 +41,4 @@ class OrganizationAccess extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrganizationAccess;
|
export default withNetwork(OrganizationAccess);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { I18n } from '@lingui/react';
|
import { I18n } from '@lingui/react';
|
||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CardBody,
|
CardBody,
|
||||||
Button,
|
Button,
|
||||||
@@ -9,6 +10,9 @@ import {
|
|||||||
TextContent,
|
TextContent,
|
||||||
TextVariants,
|
TextVariants,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../../contexts/Network';
|
||||||
|
|
||||||
import BasicChip from '../../../../components/BasicChip/BasicChip';
|
import BasicChip from '../../../../components/BasicChip/BasicChip';
|
||||||
|
|
||||||
const detailWrapperStyle = {
|
const detailWrapperStyle = {
|
||||||
@@ -68,6 +72,7 @@ class OrganizationDetail extends Component {
|
|||||||
async loadInstanceGroups () {
|
async loadInstanceGroups () {
|
||||||
const {
|
const {
|
||||||
api,
|
api,
|
||||||
|
handleHttpError,
|
||||||
match
|
match
|
||||||
} = this.props;
|
} = this.props;
|
||||||
try {
|
try {
|
||||||
@@ -78,7 +83,7 @@ class OrganizationDetail extends Component {
|
|||||||
instanceGroups: [...data.results]
|
instanceGroups: [...data.results]
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
handleHttpError(err) || this.setState({ error: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,4 +178,4 @@ class OrganizationDetail extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrganizationDetail;
|
export default withNetwork(OrganizationDetail);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { Component } 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 { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@patternfly/react-core';
|
||||||
|
|
||||||
import OrganizationForm from '../../components/OrganizationForm';
|
import OrganizationForm from '../../components/OrganizationForm';
|
||||||
|
import { withNetwork } from '../../../../contexts/Network';
|
||||||
|
|
||||||
class OrganizationEdit extends Component {
|
class OrganizationEdit extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -20,14 +20,13 @@ class OrganizationEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
|
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
|
||||||
const { api, organization } = this.props;
|
const { api, organization, handleHttpError } = this.props;
|
||||||
try {
|
try {
|
||||||
await api.updateOrganizationDetails(organization.id, values);
|
await api.updateOrganizationDetails(organization.id, values);
|
||||||
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
|
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
|
||||||
} catch (err) {
|
|
||||||
this.setState({ error: err });
|
|
||||||
} finally {
|
|
||||||
this.handleSuccess();
|
this.handleSuccess();
|
||||||
|
} catch (err) {
|
||||||
|
handleHttpError(err) || this.setState({ error: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,29 +41,24 @@ class OrganizationEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
|
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
|
||||||
const { api, organization } = this.props;
|
const { api, organization, handleHttpError } = this.props;
|
||||||
const url = organization.related.instance_groups;
|
const url = organization.related.instance_groups;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(groupsToAssociate.map(async id => {
|
await Promise.all(groupsToAssociate.map(id => api.associateInstanceGroup(url, id)));
|
||||||
await api.associateInstanceGroup(url, id);
|
await Promise.all(groupsToDisassociate.map(id => api.disassociate(url, id)));
|
||||||
}));
|
|
||||||
await Promise.all(groupsToDisassociate.map(async id => {
|
|
||||||
await api.disassociate(url, id);
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
handleHttpError(err) || this.setState({ error: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { api, organization } = this.props;
|
const { organization } = this.props;
|
||||||
const { error } = this.state;
|
const { error } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<OrganizationForm
|
<OrganizationForm
|
||||||
api={api}
|
|
||||||
organization={organization}
|
organization={organization}
|
||||||
handleSubmit={this.handleSubmit}
|
handleSubmit={this.handleSubmit}
|
||||||
handleCancel={this.handleCancel}
|
handleCancel={this.handleCancel}
|
||||||
@@ -76,7 +70,6 @@ class OrganizationEdit extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OrganizationEdit.propTypes = {
|
OrganizationEdit.propTypes = {
|
||||||
api: PropTypes.shape().isRequired,
|
|
||||||
organization: PropTypes.shape().isRequired,
|
organization: PropTypes.shape().isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,4 +78,4 @@ OrganizationEdit.contextTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationEdit as _OrganizationEdit };
|
export { OrganizationEdit as _OrganizationEdit };
|
||||||
export default withRouter(OrganizationEdit);
|
export default withNetwork(withRouter(OrganizationEdit));
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../../contexts/Network';
|
||||||
|
|
||||||
import NotificationsList from '../../../../components/NotificationsList/Notifications.list';
|
import NotificationsList from '../../../../components/NotificationsList/Notifications.list';
|
||||||
|
|
||||||
class OrganizationNotifications extends Component {
|
class OrganizationNotifications extends Component {
|
||||||
@@ -60,4 +62,5 @@ class OrganizationNotifications extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrganizationNotifications;
|
export { OrganizationNotifications as _OrganizationNotifications };
|
||||||
|
export default withNetwork(OrganizationNotifications);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import React, { Fragment } 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 OrganizationTeamsList from '../../components/OrganizationTeamsList';
|
import OrganizationTeamsList from '../../components/OrganizationTeamsList';
|
||||||
import { parseQueryString } from '../../../../qs';
|
import { parseQueryString } from '../../../../util/qs';
|
||||||
|
import { withNetwork } from '../../../../contexts/Network';
|
||||||
|
|
||||||
const DEFAULT_QUERY_PARAMS = {
|
const DEFAULT_QUERY_PARAMS = {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -47,7 +48,7 @@ class OrganizationTeams extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async readOrganizationTeamsList () {
|
async readOrganizationTeamsList () {
|
||||||
const { api, id } = this.props;
|
const { api, handleHttpError, id } = this.props;
|
||||||
const params = this.getQueryParams();
|
const params = this.getQueryParams();
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +62,7 @@ class OrganizationTeams extends React.Component {
|
|||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({
|
handleHttpError(error) && this.setState({
|
||||||
error,
|
error,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
@@ -94,9 +95,8 @@ class OrganizationTeams extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OrganizationTeams.propTypes = {
|
OrganizationTeams.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired
|
||||||
api: PropTypes.shape().isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationTeams as _OrganizationTeams };
|
export { OrganizationTeams as _OrganizationTeams };
|
||||||
export default withRouter(OrganizationTeams);
|
export default withNetwork(withRouter(OrganizationTeams));
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { TimesIcon } from '@patternfly/react-icons';
|
import { TimesIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../contexts/Network';
|
||||||
|
|
||||||
import OrganizationForm from '../components/OrganizationForm';
|
import OrganizationForm from '../components/OrganizationForm';
|
||||||
|
|
||||||
class OrganizationAdd extends React.Component {
|
class OrganizationAdd extends React.Component {
|
||||||
@@ -29,18 +31,16 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit (values, groupsToAssociate) {
|
async handleSubmit (values, groupsToAssociate) {
|
||||||
const { api } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
try {
|
try {
|
||||||
const { data: response } = await api.createOrganization(values);
|
const { data: response } = await api.createOrganization(values);
|
||||||
const instanceGroupsUrl = response.related.instance_groups;
|
const instanceGroupsUrl = response.related.instance_groups;
|
||||||
try {
|
try {
|
||||||
await Promise.all(groupsToAssociate.map(async id => {
|
await Promise.all(groupsToAssociate.map(id => api
|
||||||
await api.associateInstanceGroup(instanceGroupsUrl, id);
|
.associateInstanceGroup(instanceGroupsUrl, id)));
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ error: err });
|
|
||||||
} finally {
|
|
||||||
this.handleSuccess(response.id);
|
this.handleSuccess(response.id);
|
||||||
|
} catch (err) {
|
||||||
|
handleHttpError(err) || this.setState({ error: err });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
@@ -58,7 +58,6 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { api } = this.props;
|
|
||||||
const { error } = this.state;
|
const { error } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,7 +81,6 @@ class OrganizationAdd extends React.Component {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<OrganizationForm
|
<OrganizationForm
|
||||||
api={api}
|
|
||||||
handleSubmit={this.handleSubmit}
|
handleSubmit={this.handleSubmit}
|
||||||
handleCancel={this.handleCancel}
|
handleCancel={this.handleCancel}
|
||||||
/>
|
/>
|
||||||
@@ -105,4 +103,4 @@ OrganizationAdd.contextTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { OrganizationAdd as _OrganizationAdd };
|
export { OrganizationAdd as _OrganizationAdd };
|
||||||
export default withRouter(OrganizationAdd);
|
export default withNetwork(withRouter(OrganizationAdd));
|
||||||
|
|||||||
@@ -5,31 +5,34 @@ import React, {
|
|||||||
import {
|
import {
|
||||||
withRouter
|
withRouter
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { I18n, i18nMark } from '@lingui/react';
|
import { I18n, i18nMark } from '@lingui/react';
|
||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateBody,
|
EmptyStateBody,
|
||||||
Modal,
|
|
||||||
PageSection,
|
PageSection,
|
||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
Title,
|
Title,
|
||||||
Button,
|
Button
|
||||||
Progress,
|
|
||||||
ProgressVariant
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { CubesIcon } from '@patternfly/react-icons';
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import { withNetwork } from '../../../contexts/Network';
|
||||||
|
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import OrganizationListItem from '../components/OrganizationListItem';
|
import OrganizationListItem from '../components/OrganizationListItem';
|
||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
encodeQueryString,
|
encodeQueryString,
|
||||||
parseQueryString,
|
parseQueryString,
|
||||||
} from '../../../qs';
|
} from '../../../util/qs';
|
||||||
|
|
||||||
class OrganizationsList extends Component {
|
class OrganizationsList extends Component {
|
||||||
columns = [
|
columns = [
|
||||||
@@ -61,8 +64,6 @@ class OrganizationsList extends Component {
|
|||||||
selected: [],
|
selected: [],
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
orgsToDelete: [],
|
orgsToDelete: [],
|
||||||
orgsDeleted: [],
|
|
||||||
deleteSuccess: false,
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,17 +146,14 @@ class OrganizationsList extends Component {
|
|||||||
handleClearOrgsToDelete () {
|
handleClearOrgsToDelete () {
|
||||||
this.setState({
|
this.setState({
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
orgsDeleted: [],
|
orgsToDelete: []
|
||||||
deleteSuccess: false,
|
|
||||||
orgsToDelete: [],
|
|
||||||
deleteStarted: false
|
|
||||||
});
|
});
|
||||||
this.onSelectAll();
|
this.onSelectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenOrgDeleteModal () {
|
handleOpenOrgDeleteModal () {
|
||||||
const { results, selected } = this.state;
|
const { results, selected } = this.state;
|
||||||
const warningTitle = i18nMark('Delete Organization');
|
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
||||||
const warningMsg = i18nMark('Are you sure you want to delete:');
|
const warningMsg = i18nMark('Are you sure you want to delete:');
|
||||||
|
|
||||||
const orgsToDelete = [];
|
const orgsToDelete = [];
|
||||||
@@ -175,24 +173,21 @@ class OrganizationsList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleOrgDelete (event) {
|
async handleOrgDelete (event) {
|
||||||
const { orgsToDelete, orgsDeleted } = this.state;
|
const { orgsToDelete } = this.state;
|
||||||
const { api } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
this.setState({ deleteStarted: true });
|
let errorHandled;
|
||||||
|
|
||||||
orgsToDelete.forEach(async (org) => {
|
try {
|
||||||
try {
|
await Promise.all(orgsToDelete.map((org) => api.destroyOrganization(org.id)));
|
||||||
const res = await api.destroyOrganization(org.id);
|
this.handleClearOrgsToDelete();
|
||||||
this.setState({
|
} catch (err) {
|
||||||
orgsDeleted: orgsDeleted.concat(res)
|
errorHandled = handleHttpError(err);
|
||||||
});
|
} finally {
|
||||||
} catch {
|
if (!errorHandled) {
|
||||||
this.setState({ deleteSuccess: false });
|
|
||||||
} finally {
|
|
||||||
this.setState({ deleteSuccess: true });
|
|
||||||
const queryParams = this.getQueryParams();
|
const queryParams = this.getQueryParams();
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +202,7 @@ class OrganizationsList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrganizations (queryParams) {
|
async fetchOrganizations (queryParams) {
|
||||||
const { api } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
const { page, page_size, order_by } = queryParams;
|
const { page, page_size, order_by } = queryParams;
|
||||||
|
|
||||||
let sortOrder = 'ascending';
|
let sortOrder = 'ascending';
|
||||||
@@ -235,6 +230,7 @@ class OrganizationsList extends Component {
|
|||||||
sortedColumnKey,
|
sortedColumnKey,
|
||||||
results,
|
results,
|
||||||
selected: [],
|
selected: [],
|
||||||
|
loading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is in place to track whether or not the initial request
|
// This is in place to track whether or not the initial request
|
||||||
@@ -248,9 +244,7 @@ class OrganizationsList extends Component {
|
|||||||
this.setState(stateToUpdate);
|
this.setState(stateToUpdate);
|
||||||
this.updateUrl(queryParams);
|
this.updateUrl(queryParams);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
handleHttpError(err) || this.setState({ error: true, loading: false });
|
||||||
} finally {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,12 +255,9 @@ class OrganizationsList extends Component {
|
|||||||
const {
|
const {
|
||||||
count,
|
count,
|
||||||
error,
|
error,
|
||||||
deleteSuccess,
|
|
||||||
deleteStarted,
|
|
||||||
loading,
|
loading,
|
||||||
noInitialResults,
|
noInitialResults,
|
||||||
orgsToDelete,
|
orgsToDelete,
|
||||||
orgsDeleted,
|
|
||||||
page,
|
page,
|
||||||
pageCount,
|
pageCount,
|
||||||
page_size,
|
page_size,
|
||||||
@@ -280,74 +271,60 @@ class OrganizationsList extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
const { match } = this.props;
|
const { match } = this.props;
|
||||||
return (
|
return (
|
||||||
<PageSection variant={medium}>
|
<I18n>
|
||||||
<Card>
|
{({ i18n }) => (
|
||||||
{ isModalOpen && (
|
<PageSection variant={medium}>
|
||||||
<Modal
|
<Card>
|
||||||
className="orgListAlert"
|
{ isModalOpen && (
|
||||||
title={warningTitle}
|
<AlertModal
|
||||||
isOpen={isModalOpen}
|
variant="danger"
|
||||||
style={{ width: '1000px' }}
|
title={warningTitle}
|
||||||
variant="danger"
|
isOpen={isModalOpen}
|
||||||
onClose={this.handleClearOrgsToDelete}
|
onClose={this.handleClearOrgsToDelete}
|
||||||
>
|
actions={[
|
||||||
{warningMsg}
|
<Button variant="danger" key="delete" aria-label="confirm-delete" onClick={this.handleOrgDelete}>{i18n._(t`Delete`)}</Button>,
|
||||||
<br />
|
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleClearOrgsToDelete}>{i18n._(t`Cancel`)}</Button>
|
||||||
{orgsToDelete.map((org) => (
|
]}
|
||||||
<span key={org.id}>
|
>
|
||||||
<strong>
|
{warningMsg}
|
||||||
{org.name}
|
|
||||||
</strong>
|
|
||||||
<br />
|
<br />
|
||||||
</span>
|
{orgsToDelete.map((org) => (
|
||||||
))}
|
<span key={org.id}>
|
||||||
<div className={deleteStarted ? 'orgListDetete-progressBar' : 'orgListDelete-progressBar-noShow'}>
|
<strong>
|
||||||
<Progress
|
{org.name}
|
||||||
value={deleteSuccess ? 100 : 67}
|
</strong>
|
||||||
variant={deleteStarted ? ProgressVariant.success : ProgressVariant.danger}
|
<br />
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div className="awx-c-form-action-group">
|
|
||||||
{orgsDeleted.length
|
|
||||||
? <Button className="orgListAlert-actionBtn" keys="cancel" variant="primary" aria-label="close-delete" onClick={this.handleClearOrgsToDelete}>Close</Button>
|
|
||||||
: (
|
|
||||||
<span>
|
|
||||||
<Button className="orgListAlert-actionBtn" keys="cancel" variant="secondary" aria-label="cancel-delete" onClick={this.handleClearOrgsToDelete}>Cancel</Button>
|
|
||||||
<Button className="orgListAlert-actionBtn" keys="cancel" variant="danger" aria-label="confirm-delete" onClick={this.handleOrgDelete}>Delete</Button>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
))}
|
||||||
</div>
|
<br />
|
||||||
</Modal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
{noInitialResults && (
|
{noInitialResults && (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<EmptyStateIcon icon={CubesIcon} />
|
<EmptyStateIcon icon={CubesIcon} />
|
||||||
<Title size="lg">
|
<Title size="lg">
|
||||||
<Trans>No Organizations Found</Trans>
|
<Trans>No Organizations Found</Trans>
|
||||||
</Title>
|
</Title>
|
||||||
<EmptyStateBody>
|
<EmptyStateBody>
|
||||||
<Trans>Please add an organization to populate this list</Trans>
|
<Trans>Please add an organization to populate this list</Trans>
|
||||||
</EmptyStateBody>
|
</EmptyStateBody>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
) || (
|
) || (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
addUrl={`${match.url}/add`}
|
addUrl={`${match.url}/add`}
|
||||||
isAllSelected={selected.length === results.length}
|
isAllSelected={selected.length === results.length}
|
||||||
sortedColumnKey={sortedColumnKey}
|
sortedColumnKey={sortedColumnKey}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
columns={this.columns}
|
columns={this.columns}
|
||||||
onSearch={this.onSearch}
|
onSearch={this.onSearch}
|
||||||
onSort={this.onSort}
|
onSort={this.onSort}
|
||||||
onSelectAll={this.onSelectAll}
|
onSelectAll={this.onSelectAll}
|
||||||
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
||||||
disableTrashCanIcon={selected.length === 0}
|
disableTrashCanIcon={selected.length === 0}
|
||||||
showDelete
|
showDelete
|
||||||
showSelectAll
|
showSelectAll
|
||||||
/>
|
/>
|
||||||
<I18n>
|
|
||||||
{({ i18n }) => (
|
|
||||||
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
||||||
{ results.map(o => (
|
{ results.map(o => (
|
||||||
<OrganizationListItem
|
<OrganizationListItem
|
||||||
@@ -363,23 +340,24 @@ class OrganizationsList extends Component {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
<Pagination
|
||||||
</I18n>
|
count={count}
|
||||||
<Pagination
|
page={page}
|
||||||
count={count}
|
pageCount={pageCount}
|
||||||
page={page}
|
page_size={page_size}
|
||||||
pageCount={pageCount}
|
onSetPage={this.onSetPage}
|
||||||
page_size={page_size}
|
/>
|
||||||
onSetPage={this.onSetPage}
|
{ loading ? <div>loading...</div> : '' }
|
||||||
/>
|
{ error ? <div>error</div> : '' }
|
||||||
{ loading ? <div>loading...</div> : '' }
|
</Fragment>
|
||||||
{ error ? <div>error</div> : '' }
|
)}
|
||||||
</Fragment>
|
</Card>
|
||||||
)}
|
</PageSection>
|
||||||
</Card>
|
)}
|
||||||
</PageSection>
|
</I18n>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(OrganizationsList);
|
export { OrganizationsList as _OrganizationsList };
|
||||||
|
export default withNetwork(withRouter(OrganizationsList));
|
||||||
|
|||||||
Reference in New Issue
Block a user