mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 21:49:27 -02:30
Merge pull request #72 from ansible/react-context-api
Implement React Context API
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
const axios = require('axios');
|
import * as endpoints from '../src/endpoints';
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
const mockAPIConfigData = {
|
||||||
|
data: {
|
||||||
|
custom_virtualenvs: ['foo', 'bar'],
|
||||||
|
ansible_version: "2.7.2",
|
||||||
|
version: "2.1.1-40-g2758a3848"
|
||||||
|
}
|
||||||
|
};
|
||||||
jest.genMockFromModule('axios');
|
jest.genMockFromModule('axios');
|
||||||
|
|
||||||
axios.create = jest.fn(() => axios);
|
axios.create = jest.fn(() => axios);
|
||||||
@@ -9,7 +17,16 @@ axios.create.mockReturnValue({
|
|||||||
get: axios.get,
|
get: axios.get,
|
||||||
post: axios.post
|
post: axios.post
|
||||||
});
|
});
|
||||||
axios.get.mockResolvedValue('get results');
|
axios.get.mockImplementation((endpoint) => {
|
||||||
|
if (endpoint === endpoints.API_CONFIG) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve(mockAPIConfigData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'get results';
|
||||||
|
}
|
||||||
|
});
|
||||||
axios.post.mockResolvedValue('post results');
|
axios.post.mockResolvedValue('post results');
|
||||||
|
|
||||||
axios.customClearMocks = () => {
|
axios.customClearMocks = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
import { shallow, mount } from 'enzyme';
|
import { shallow, mount } from 'enzyme';
|
||||||
import App from '../src/App';
|
import App from '../src/App';
|
||||||
import api from '../src/api';
|
import api from '../src/api';
|
||||||
import { API_LOGOUT } from '../src/endpoints';
|
import { API_LOGOUT, API_CONFIG } from '../src/endpoints';
|
||||||
|
|
||||||
import Dashboard from '../src/pages/Dashboard';
|
import Dashboard from '../src/pages/Dashboard';
|
||||||
import Login from '../src/pages/Login';
|
import Login from '../src/pages/Login';
|
||||||
@@ -53,7 +53,13 @@ describe('<App />', () => {
|
|||||||
expect(logOutButton.length).toBe(1);
|
expect(logOutButton.length).toBe(1);
|
||||||
logOutButton.simulate('click');
|
logOutButton.simulate('click');
|
||||||
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' });
|
||||||
expect(api.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(api.get).toHaveBeenCalledWith(API_LOGOUT);
|
expect(api.get).toHaveBeenCalledWith(API_LOGOUT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Componenet makes REST call to API_CONFIG endpoint when mounted', () => {
|
||||||
|
api.get = jest.fn().mockImplementation(() => Promise.resolve({}));
|
||||||
|
const appWrapper = shallow(<App.WrappedComponent />);
|
||||||
|
expect(api.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(api.get).toHaveBeenCalledWith(API_CONFIG);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,26 +31,4 @@ describe('<About />', () => {
|
|||||||
expect(onAboutModalClose).toBeCalled();
|
expect(onAboutModalClose).toBeCalled();
|
||||||
aboutWrapper.unmount();
|
aboutWrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sets error on api request failure', async () => {
|
|
||||||
api.get = jest.fn().mockImplementation(() => {
|
|
||||||
const err = new Error('404 error');
|
|
||||||
err.response = { status: 404, message: 'problem' };
|
|
||||||
return Promise.reject(err);
|
|
||||||
});
|
|
||||||
aboutWrapper = mount(
|
|
||||||
<I18nProvider>
|
|
||||||
<About isOpen />
|
|
||||||
</I18nProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const aboutComponentInstance = aboutWrapper.find(About).instance();
|
|
||||||
await aboutComponentInstance.componentDidMount();
|
|
||||||
expect(aboutComponentInstance.state.error.response.status).toBe(404);
|
|
||||||
aboutWrapper.unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('API Config endpoint is valid', () => {
|
|
||||||
expect(API_CONFIG).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,16 +2,29 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import AnsibleSelect from '../../src/components/AnsibleSelect';
|
import AnsibleSelect from '../../src/components/AnsibleSelect';
|
||||||
|
|
||||||
const mockData = ['foo', 'bar'];
|
const label = "test select"
|
||||||
|
const mockData = ["/venv/baz/", "/venv/ansible/"];
|
||||||
describe('<AnsibleSelect />', () => {
|
describe('<AnsibleSelect />', () => {
|
||||||
test('initially renders succesfully', async() => {
|
test('initially renders succesfully', async () => {
|
||||||
const wrapper = mount(<AnsibleSelect selected="foo" data={mockData} selectChange={() => {}} />);
|
mount(
|
||||||
wrapper.setState({ isHidden: false });
|
<AnsibleSelect
|
||||||
|
selected="foo"
|
||||||
|
selectChange={() => { }}
|
||||||
|
labelName={label}
|
||||||
|
data={mockData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
test('calls "onSelectChange" on dropdown select change', () => {
|
test('calls "onSelectChange" on dropdown select change', () => {
|
||||||
const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange');
|
const spy = jest.spyOn(AnsibleSelect.prototype, 'onSelectChange');
|
||||||
const wrapper = mount(<AnsibleSelect selected="foo" data={mockData} selectChange={() => {}} />);
|
const wrapper = mount(
|
||||||
wrapper.setState({ isHidden: false });
|
<AnsibleSelect
|
||||||
|
selected="foo"
|
||||||
|
selectChange={() => { }}
|
||||||
|
labelName={label}
|
||||||
|
data={mockData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
wrapper.find('select').simulate('change');
|
wrapper.find('select').simulate('change');
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import OrganizationAdd from '../../../../src/pages/Organizations/views/Organization.add';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
let OrganizationAdd;
|
||||||
|
const getAppWithConfigContext = (context = {
|
||||||
|
custom_virtualenvs: ['foo', 'bar']
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// Mock the ConfigContext module being used in our OrganizationAdd component
|
||||||
|
jest.doMock('../../../../src/context', () => {
|
||||||
|
return {
|
||||||
|
ConfigContext: {
|
||||||
|
Consumer: (props) => props.children(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the updated OrganizationAdd module with mocked context
|
||||||
|
return require('../../../../src/pages/Organizations/views/Organization.add').default;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
OrganizationAdd = getAppWithConfigContext();
|
||||||
|
})
|
||||||
|
|
||||||
describe('<OrganizationAdd />', () => {
|
describe('<OrganizationAdd />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mount(
|
mount(
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
"@patternfly/react-styles": "^2.3.0",
|
"@patternfly/react-styles": "^2.3.0",
|
||||||
"@patternfly/react-tokens": "^1.9.0",
|
"@patternfly/react-tokens": "^1.9.0",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.4.1",
|
"react": "^16.4.1",
|
||||||
"react-dom": "^16.4.1",
|
"react-dom": "^16.4.1",
|
||||||
"react-router-dom": "^4.3.1"
|
"react-router-dom": "^4.3.1"
|
||||||
|
|||||||
264
src/App.jsx
264
src/App.jsx
@@ -1,4 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
import { ConfigContext } from './context';
|
||||||
|
|
||||||
import { I18nProvider, I18n } from '@lingui/react';
|
import { I18nProvider, I18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
@@ -22,7 +24,7 @@ import {
|
|||||||
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
|
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
|
||||||
|
|
||||||
import api from './api';
|
import api from './api';
|
||||||
import { API_LOGOUT } from './endpoints';
|
import { API_LOGOUT, API_CONFIG } from './endpoints';
|
||||||
|
|
||||||
import HelpDropdown from './components/HelpDropdown';
|
import HelpDropdown from './components/HelpDropdown';
|
||||||
import LogoutButton from './components/LogoutButton';
|
import LogoutButton from './components/LogoutButton';
|
||||||
@@ -61,19 +63,21 @@ const catalogs = { en, ja };
|
|||||||
|
|
||||||
// This spits out the language and the region. Example: es-US
|
// This spits out the language and the region. Example: es-US
|
||||||
const language = (navigator.languages && navigator.languages[0])
|
const language = (navigator.languages && navigator.languages[0])
|
||||||
|| navigator.language
|
|| navigator.language
|
||||||
|| navigator.userLanguage;
|
|| navigator.userLanguage;
|
||||||
|
|
||||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||||
|
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
|
const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10);
|
||||||
this.state = {
|
this.state = {
|
||||||
isNavOpen
|
isNavOpen,
|
||||||
|
config: {},
|
||||||
|
error: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +85,27 @@ class App extends React.Component {
|
|||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
};
|
};
|
||||||
|
|
||||||
onDevLogout = async () => {
|
onLogoClick = () => {
|
||||||
await api.get(API_LOGOUT);
|
this.setState({ activeGroup: 'views_group' });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
onDevLogout = async () => {
|
||||||
const { isNavOpen } = this.state;
|
await api.get(API_LOGOUT);
|
||||||
|
this.setState({ activeGroup: 'views_group', activeItem: 'views_group_dashboard' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
// Grab our config data from the API and store in state
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(API_CONFIG);
|
||||||
|
this.setState({ config: data });
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isNavOpen, config } = this.state;
|
||||||
const { logo, loginInfo, history } = this.props;
|
const { logo, loginInfo, history } = this.props;
|
||||||
|
|
||||||
const PageToolbar = (
|
const PageToolbar = (
|
||||||
@@ -118,118 +137,121 @@ class App extends React.Component {
|
|||||||
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
|
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Switch>
|
<ConfigContext.Provider value={config}>
|
||||||
<ConditionalRedirect
|
<Switch>
|
||||||
shouldRedirect={() => api.isAuthenticated()}
|
<ConditionalRedirect
|
||||||
redirectPath="/"
|
shouldRedirect={() => api.isAuthenticated()}
|
||||||
path="/login"
|
redirectPath="/"
|
||||||
component={() => <Login logo={logo} loginInfo={loginInfo} />}
|
path="/login"
|
||||||
/>
|
component={() => <Login logo={logo} loginInfo={loginInfo} />}
|
||||||
<Fragment>
|
/>
|
||||||
<Page
|
<Fragment>
|
||||||
header={(
|
<Page
|
||||||
<PageHeader
|
header={(
|
||||||
logo={<TowerLogo />}
|
<PageHeader
|
||||||
toolbar={PageToolbar}
|
logo={<TowerLogo onClick={this.onLogoClick} />}
|
||||||
showNavToggle
|
toolbar={PageToolbar}
|
||||||
onNavToggle={this.onNavToggle}
|
showNavToggle
|
||||||
/>
|
onNavToggle={this.onNavToggle}
|
||||||
)}
|
/>
|
||||||
sidebar={(
|
)}
|
||||||
<PageSidebar
|
sidebar={(
|
||||||
isNavOpen={isNavOpen}
|
<PageSidebar
|
||||||
nav={(
|
isNavOpen={isNavOpen}
|
||||||
<I18n>
|
nav={(
|
||||||
{({ i18n }) => (
|
<I18n>
|
||||||
<Nav aria-label={i18n._(t`Primary Navigation`)}>
|
{({ i18n }) => (
|
||||||
<NavList>
|
<Nav aria-label={i18n._(t`Primary Navigation`)}>
|
||||||
<NavExpandableGroup
|
<NavList>
|
||||||
groupId="views_group"
|
<NavExpandableGroup
|
||||||
title={i18n._("Views")}
|
groupId="views_group"
|
||||||
routes={[
|
title={i18n._("Views")}
|
||||||
{ path: '/home', title: i18n._('Dashboard') },
|
routes={[
|
||||||
{ path: '/jobs', title: i18n._('Jobs') },
|
{ path: '/home', title: i18n._('Dashboard') },
|
||||||
{ path: '/schedules', title: i18n._('Schedules') },
|
{ path: '/jobs', title: i18n._('Jobs') },
|
||||||
{ path: '/portal', title: i18n._('Portal Mode') },
|
{ path: '/schedules', title: i18n._('Schedules') },
|
||||||
]}
|
{ path: '/portal', title: i18n._('Portal Mode') },
|
||||||
/>
|
]}
|
||||||
<NavExpandableGroup
|
/>
|
||||||
groupId="resources_group"
|
<NavExpandableGroup
|
||||||
title={i18n._("Resources")}
|
groupId="resources_group"
|
||||||
routes={[
|
title={i18n._("Resources")}
|
||||||
{ path: '/templates', title: i18n._('Templates') },
|
routes={[
|
||||||
{ path: '/credentials', title: i18n._('Credentials') },
|
{ path: '/templates', title: i18n._('Templates') },
|
||||||
{ path: '/projects', title: i18n._('Projects') },
|
{ path: '/credentials', title: i18n._('Credentials') },
|
||||||
{ path: '/inventories', title: i18n._('Inventories') },
|
{ path: '/projects', title: i18n._('Projects') },
|
||||||
{ path: '/inventory_scripts', title: i18n._('Inventory Scripts') }
|
{ path: '/inventories', title: i18n._('Inventories') },
|
||||||
]}
|
{ path: '/inventory_scripts', title: i18n._('Inventory Scripts') }
|
||||||
/>
|
]}
|
||||||
<NavExpandableGroup
|
/>
|
||||||
groupId="access_group"
|
<NavExpandableGroup
|
||||||
title={i18n._("Access")}
|
groupId="access_group"
|
||||||
routes={[
|
title={i18n._("Access")}
|
||||||
{ path: '/organizations', title: i18n._('Organizations') },
|
routes={[
|
||||||
{ path: '/users', title: i18n._('Users') },
|
{ path: '/organizations', title: i18n._('Organizations') },
|
||||||
{ path: '/teams', title: i18n._('Teams') }
|
{ path: '/users', title: i18n._('Users') },
|
||||||
]}
|
{ path: '/teams', title: i18n._('Teams') }
|
||||||
/>
|
]}
|
||||||
<NavExpandableGroup
|
/>
|
||||||
groupId="administration_group"
|
<NavExpandableGroup
|
||||||
title={i18n._("Administration")}
|
groupId="administration_group"
|
||||||
routes={[
|
title={i18n._("Administration")}
|
||||||
{ path: '/credential_types', title: i18n._('Credential Types') },
|
routes={[
|
||||||
{ path: '/notification_templates', title: i18n._('Notifications') },
|
{ path: '/credential_types', title: i18n._('Credential Types') },
|
||||||
{ path: '/management_jobs', title: i18n._('Management Jobs') },
|
{ path: '/notification_templates', title: i18n._('Notifications') },
|
||||||
{ path: '/instance_groups', title: i18n._('Instance Groups') },
|
{ path: '/management_jobs', title: i18n._('Management Jobs') },
|
||||||
{ path: '/applications', title: i18n._('Integrations') }
|
{ path: '/instance_groups', title: i18n._('Instance Groups') },
|
||||||
]}
|
{ path: '/applications', title: i18n._('Integrations') }
|
||||||
/>
|
]}
|
||||||
<NavExpandableGroup
|
/>
|
||||||
groupId="settings_group"
|
<NavExpandableGroup
|
||||||
title={i18n._("Settings")}
|
groupId="settings_group"
|
||||||
routes={[
|
title={i18n._("Settings")}
|
||||||
{ path: '/auth_settings', title: i18n._('Authentication') },
|
routes={[
|
||||||
{ path: '/jobs_settings', title: i18n._('Jobs') },
|
{ path: '/auth_settings', title: i18n._('Authentication') },
|
||||||
{ path: '/system_settings', title: i18n._('System') },
|
{ path: '/jobs_settings', title: i18n._('Jobs') },
|
||||||
{ path: '/ui_settings', title: i18n._('User Interface') },
|
{ path: '/system_settings', title: i18n._('System') },
|
||||||
{ path: '/license', title: i18n._('License') }
|
{ path: '/ui_settings', title: i18n._('User Interface') },
|
||||||
]}
|
{ path: '/license', title: i18n._('License') }
|
||||||
/>
|
]}
|
||||||
</NavList>
|
/>
|
||||||
</Nav>
|
</NavList>
|
||||||
)}
|
</Nav>
|
||||||
</I18n>
|
)}
|
||||||
)}
|
</I18n>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
useCondensed
|
)}
|
||||||
>
|
useCondensed
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => (<Redirect to="/home" />)} />
|
>
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => (<Redirect to="/home" />)} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/auth_settings" component={AuthSettings} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/auth_settings" component={AuthSettings} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} />
|
||||||
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} />
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} />
|
||||||
</Page>
|
<ConditionalRedirect shouldRedirect={() => !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} />
|
||||||
</Fragment>
|
|
||||||
</Switch>
|
</Page>
|
||||||
|
</Fragment>
|
||||||
|
</Switch>
|
||||||
|
</ConfigContext.Provider>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,46 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { I18n } from '@lingui/react';
|
import { I18n } from '@lingui/react';
|
||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
AboutModal,
|
AboutModal,
|
||||||
TextContent,
|
TextContent,
|
||||||
TextList,
|
TextList,
|
||||||
TextListItem } from '@patternfly/react-core';
|
TextListItem
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg';
|
import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg';
|
||||||
import brandImg from '../../images/tower-logo-white.svg';
|
import brandImg from '../../images/tower-logo-white.svg';
|
||||||
import logoImg from '../../images/tower-logo-login.svg';
|
import logoImg from '../../images/tower-logo-login.svg';
|
||||||
|
|
||||||
import api from '../api';
|
import { ConfigContext } from '../context';
|
||||||
import { API_CONFIG } from '../endpoints';
|
|
||||||
|
|
||||||
class About extends React.Component {
|
class About extends React.Component {
|
||||||
unmounting = false;
|
|
||||||
|
|
||||||
constructor (props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
config: {},
|
|
||||||
error: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount () {
|
|
||||||
try {
|
|
||||||
const { data } = await api.get(API_CONFIG);
|
|
||||||
this.safeSetState({ config: data });
|
|
||||||
} catch (error) {
|
|
||||||
this.safeSetState({ error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.unmounting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
safeSetState = obj => !this.unmounting && this.setState(obj);
|
|
||||||
|
|
||||||
createSpeechBubble = (version) => {
|
createSpeechBubble = (version) => {
|
||||||
let text = `Tower ${version}`;
|
let text = `Tower ${version}`;
|
||||||
let top = '';
|
let top = '';
|
||||||
@@ -65,26 +40,25 @@ class About extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isOpen } = this.props;
|
const { isOpen } = this.props;
|
||||||
const { config = {}, error } = this.state;
|
|
||||||
const { ansible_version = 'loading', version = 'loading' } = config;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<AboutModal
|
<ConfigContext.Consumer>
|
||||||
isOpen={isOpen}
|
{({ ansible_version, version }) => (
|
||||||
onClose={this.handleModalToggle}
|
<AboutModal
|
||||||
productName="Ansible Tower"
|
isOpen={isOpen}
|
||||||
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
onClose={this.handleModalToggle}
|
||||||
brandImageSrc={brandImg}
|
productName="Ansible Tower"
|
||||||
brandImageAlt={i18n._(t`Brand Image`)}
|
trademark={i18n._(t`Copyright 2018 Red Hat, Inc.`)}
|
||||||
logoImageSrc={logoImg}
|
brandImageSrc={brandImg}
|
||||||
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
brandImageAlt={i18n._(t`Brand Image`)}
|
||||||
heroImageSrc={heroImg}
|
logoImageSrc={logoImg}
|
||||||
>
|
logoImageAlt={i18n._(t`AboutModal Logo`)}
|
||||||
<pre>
|
heroImageSrc={heroImg}
|
||||||
{ this.createSpeechBubble(version) }
|
>
|
||||||
{`
|
<pre>
|
||||||
|
{this.createSpeechBubble(version)}
|
||||||
|
{`
|
||||||
\\
|
\\
|
||||||
\\ ^__^
|
\\ ^__^
|
||||||
(oo)\\_______
|
(oo)\\_______
|
||||||
@@ -92,22 +66,28 @@ class About extends React.Component {
|
|||||||
||----w |
|
||----w |
|
||||||
|| ||
|
|| ||
|
||||||
`}
|
`}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<TextContent>
|
<TextContent>
|
||||||
<TextList component="dl">
|
<TextList component="dl">
|
||||||
<TextListItem component="dt">
|
<TextListItem component="dt">
|
||||||
<Trans>Ansible Version</Trans>
|
<Trans>Ansible Version</Trans>
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
<TextListItem component="dd">{ ansible_version }</TextListItem>
|
<TextListItem component="dd">{ansible_version}</TextListItem>
|
||||||
</TextList>
|
</TextList>
|
||||||
</TextContent>
|
</TextContent>
|
||||||
{ error ? <div>error</div> : ''}
|
</AboutModal>
|
||||||
</AboutModal>
|
)}
|
||||||
|
</ConfigContext.Consumer>
|
||||||
)}
|
)}
|
||||||
</I18n>
|
</I18n>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
About.contextTypes = {
|
||||||
|
ansible_version: PropTypes.string,
|
||||||
|
version: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
Select,
|
Select,
|
||||||
@@ -11,24 +12,37 @@ class AnsibleSelect extends React.Component {
|
|||||||
this.onSelectChange = this.onSelectChange.bind(this);
|
this.onSelectChange = this.onSelectChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
count: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(nexProps, _) {
|
||||||
|
if (nexProps.data) {
|
||||||
|
return {
|
||||||
|
count: nexProps.data.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
onSelectChange(val, _) {
|
onSelectChange(val, _) {
|
||||||
this.props.selectChange(val);
|
this.props.selectChange(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hidden } = this.props;
|
const { count } = this.state;
|
||||||
if (hidden) {
|
if (count > 1) {
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label={this.props.labelName} fieldId="ansible-select">
|
<FormGroup label={this.props.labelName} fieldId="ansible-select">
|
||||||
<Select value={this.props.selected} onChange={this.onSelectChange} aria-label="Select Input">
|
<Select value={this.props.selected} onChange={this.onSelectChange} aria-label="Select Input">
|
||||||
{this.props.data.map((env, index) => (
|
{this.props.data.map((datum, index) => (
|
||||||
<SelectOption isDisabled={env.disabled} key={index} value={env} label={env} />
|
<SelectOption isDisabled={datum.disabled} key={index} value={datum} label={datum} />
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/context.jsx
Normal file
3
src/context.jsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const ConfigContext = React.createContext({});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +18,8 @@ import {
|
|||||||
CardBody,
|
CardBody,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { API_ORGANIZATIONS, API_CONFIG } from '../../../endpoints';
|
import { ConfigContext } from '../../../context';
|
||||||
|
import { API_ORGANIZATIONS } from '../../../endpoints';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import AnsibleSelect from '../../../components/AnsibleSelect'
|
import AnsibleSelect from '../../../components/AnsibleSelect'
|
||||||
const { light } = PageSectionVariants;
|
const { light } = PageSectionVariants;
|
||||||
@@ -38,8 +40,6 @@ class OrganizationAdd extends React.Component {
|
|||||||
description: '',
|
description: '',
|
||||||
instanceGroups: '',
|
instanceGroups: '',
|
||||||
custom_virtualenv: '',
|
custom_virtualenv: '',
|
||||||
custom_virtualenvs: [],
|
|
||||||
hideAnsibleSelect: true,
|
|
||||||
error:'',
|
error:'',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,22 +69,10 @@ class OrganizationAdd extends React.Component {
|
|||||||
this.props.history.push('/organizations');
|
this.props.history.push('/organizations');
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
try {
|
|
||||||
const { data } = await api.get(API_CONFIG);
|
|
||||||
this.setState({ custom_virtualenvs: [...data.custom_virtualenvs] });
|
|
||||||
if (this.state.custom_virtualenvs.length > 1) {
|
|
||||||
// Show dropdown if we have more than one ansible environment
|
|
||||||
this.setState({ hideAnsibleSelect: !this.state.hideAnsibleSelect });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.setState({ error })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
render() {
|
render() {
|
||||||
const { name } = this.state;
|
const { name } = this.state;
|
||||||
const enabled = name.length > 0; // TODO: add better form validation
|
const enabled = name.length > 0; // TODO: add better form validation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageSection variant={light} className="pf-m-condensed">
|
<PageSection variant={light} className="pf-m-condensed">
|
||||||
@@ -128,13 +116,16 @@ class OrganizationAdd extends React.Component {
|
|||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<AnsibleSelect
|
<ConfigContext.Consumer>
|
||||||
labelName="Ansible Environment"
|
{({ custom_virtualenvs }) =>
|
||||||
selected={this.state.custom_virtualenv}
|
<AnsibleSelect
|
||||||
selectChange={this.onSelectChange}
|
labelName="Ansible Environment"
|
||||||
data={this.state.custom_virtualenvs}
|
selected={this.state.custom_virtualenv}
|
||||||
hidden={this.state.hideAnsibleSelect}
|
selectChange={this.onSelectChange}
|
||||||
/>
|
data={custom_virtualenvs}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</ConfigContext.Consumer>
|
||||||
</Gallery>
|
</Gallery>
|
||||||
<ActionGroup className="at-align-right">
|
<ActionGroup className="at-align-right">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -155,4 +146,8 @@ class OrganizationAdd extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OrganizationAdd.contextTypes = {
|
||||||
|
custom_virtualenvs: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
export default withRouter(OrganizationAdd);
|
export default withRouter(OrganizationAdd);
|
||||||
|
|||||||
Reference in New Issue
Block a user