Merge remote-tracking branch 'origin/master' into lookup-form-component

This commit is contained in:
kialam
2019-01-07 14:40:35 -05:00
49 changed files with 1971 additions and 1136 deletions

View File

@@ -8,53 +8,57 @@ import {
} from '@patternfly/react-core';
import towerLogo from '../../images/tower-logo-header.svg';
import api from '../api';
class AtLogin extends Component {
class AWXLogin extends Component {
constructor (props) {
super(props);
this.state = {
username: '',
password: '',
isValidPassword: true,
loading: false
isInputValid: true,
isLoading: false
};
this.onChangeUsername = this.onChangeUsername.bind(this);
this.onChangePassword = this.onChangePassword.bind(this);
this.onLoginButtonClick = this.onLoginButtonClick.bind(this);
}
componentWillUnmount () {
this.unmounting = true; // todo: state management
onChangeUsername (value) {
this.setState({ username: value, isInputValid: true });
}
safeSetState = obj => !this.unmounting && this.setState(obj);
onChangePassword (value) {
this.setState({ password: value, isInputValid: true });
}
handleUsernameChange = value => this.safeSetState({ username: value, isValidPassword: true });
handlePasswordChange = value => this.safeSetState({ password: value, isValidPassword: true });
handleSubmit = async event => {
const { username, password, loading } = this.state;
async onLoginButtonClick (event) {
const { username, password, isLoading } = this.state;
const { api } = this.props;
event.preventDefault();
if (!loading) {
this.safeSetState({ loading: true });
if (isLoading) {
return;
}
try {
await api.login(username, password);
} catch (error) {
if (error.response.status === 401) {
this.safeSetState({ isValidPassword: false });
}
} finally {
this.safeSetState({ loading: false });
this.setState({ isLoading: true });
try {
await api.login(username, password);
} catch (error) {
if (error.response && error.response.status === 401) {
this.setState({ isInputValid: false });
}
} finally {
this.setState({ isLoading: false });
}
}
render () {
const { username, password, isValidPassword } = this.state;
const { logo, alt } = this.props;
const { username, password, isInputValid } = this.state;
const { api, alt, loginInfo, logo } = this.props;
const logoSrc = logo ? `data:image/jpeg;${logo}` : towerLogo;
if (api.isAuthenticated()) {
@@ -65,20 +69,21 @@ class AtLogin extends Component {
<I18n>
{({ i18n }) => (
<LoginPage
mainBrandImgSrc={logoSrc}
mainBrandImgAlt={alt || 'Ansible Tower'}
brandImgSrc={logoSrc}
brandImgAlt={alt || 'Ansible Tower'}
loginTitle={i18n._(t`Welcome to Ansible Tower! Please Sign In.`)}
textContent={loginInfo}
>
<LoginForm
usernameLabel={i18n._(t`Username`)}
usernameValue={username}
onChangeUsername={this.handleUsernameChange}
passwordLabel={i18n._(t`Password`)}
passwordValue={password}
onChangePassword={this.handlePasswordChange}
isValidPassword={isValidPassword}
passwordHelperTextInvalid={i18n._(t`Invalid username or password. Please try again.`)}
onLoginButtonClick={this.handleSubmit}
usernameValue={username}
passwordValue={password}
isValidPassword={isInputValid}
onChangeUsername={this.onChangeUsername}
onChangePassword={this.onChangePassword}
onLoginButtonClick={this.onLoginButtonClick}
/>
</LoginPage>
)}
@@ -87,4 +92,4 @@ class AtLogin extends Component {
}
}
export default AtLogin;
export default AWXLogin;

View File

@@ -3,7 +3,9 @@ import { Trans } from '@lingui/macro';
import {
PageSection,
PageSectionVariants,
Title,
Breadcrumb,
BreadcrumbItem,
BreadcrumbHeading
} from '@patternfly/react-core';
import {
Link
@@ -21,20 +23,22 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
.map(({ url, name }, index) => {
let elem;
if (noLastLink && parentObj.length - 1 === index) {
elem = (<Fragment key={name}>{name}</Fragment>);
elem = (<BreadcrumbHeading className="heading" key={name}>{name}</BreadcrumbHeading>);
} else {
elem = (
<Link
key={name}
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
>
{name}
</Link>
<BreadcrumbItem key={name}>
<Link
key={name}
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
>
{name}
</Link>
</BreadcrumbItem>
);
}
return elem;
})
.reduce((prev, curr) => [prev, ' > ', curr])}
.reduce((prev, curr) => [prev, curr])}
</Fragment>
);
@@ -42,25 +46,31 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > '}
{getTabName(currentTab)}
<BreadcrumbHeading className="heading">
{getTabName(currentTab)}
</BreadcrumbHeading>
</Fragment>
);
} else if (location.pathname.indexOf('edit') > -1) {
breadcrumb = (
<Fragment>
{generateCrumb()}
<Trans>{' > edit'}</Trans>
<BreadcrumbHeading className="heading">
<Trans>Edit</Trans>
</BreadcrumbHeading>
</Fragment>
);
} else if (location.pathname.indexOf('add') > -1) {
breadcrumb = (
<Fragment>
{generateCrumb()}
<Trans>{' > add'}</Trans>
<BreadcrumbHeading className="heading">
<Trans>Add</Trans>
</BreadcrumbHeading>
</Fragment>
);
} else {
breadcrumb = (
<Fragment>
{generateCrumb(true)}
@@ -71,7 +81,7 @@ const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location
return (
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl">{breadcrumb}</Title>
<Breadcrumb>{breadcrumb}</Breadcrumb>
</PageSection>
);
};

View File

@@ -6,10 +6,7 @@ import {
CardHeader,
CardBody,
PageSection,
PageSectionVariants,
ToolbarGroup,
ToolbarItem,
ToolbarSection,
PageSectionVariants
} from '@patternfly/react-core';
import {
Switch,
@@ -17,39 +14,10 @@ import {
Route
} from 'react-router-dom';
import Tab from '../../../components/Tabs/Tab';
import Tabs from '../../../components/Tabs/Tabs';
import getTabName from '../utils';
import '../tabs.scss';
const DetailTab = ({ location, match, tab, currentTab, children, breadcrumb }) => {
const tabClasses = () => {
let classes = 'at-c-tabs__tab';
if (tab === currentTab) {
classes += ' at-m-selected';
}
return classes;
};
const updateTab = () => {
const params = new URLSearchParams(location.search);
if (params.get('tab') !== undefined) {
params.set('tab', tab);
} else {
params.append('tab', tab);
}
return `?${params.toString()}`;
};
return (
<ToolbarItem className={tabClasses()}>
<Link to={{ pathname: `${match.url}`, search: updateTab(), state: { breadcrumb } }} replace={tab === currentTab}>
{children}
</Link>
</ToolbarItem>
);
};
const OrganizationDetail = ({
location,
@@ -61,6 +29,7 @@ const OrganizationDetail = ({
}) => {
// TODO: set objectName by param or through grabbing org detail get from api
const { medium } = PageSectionVariants;
const tabList=['details', 'access', 'teams', 'notifications'];
const deleteResourceView = () => (
<Fragment>
@@ -93,34 +62,29 @@ const OrganizationDetail = ({
</Fragment>
);
const detailTabs = (tabs) => (
<I18n>
{({ i18n }) => (
<ToolbarSection aria-label={i18n._(t`Organization detail tabs`)}>
<ToolbarGroup className="at-c-tabs">
{tabs.map(tab => (
<DetailTab
key={tab}
tab={tab}
location={location}
match={match}
currentTab={currentTab}
breadcrumb={parentBreadcrumbObj}
>
{getTabName(tab)}
</DetailTab>
))}
</ToolbarGroup>
</ToolbarSection>
)}
</I18n>
);
return (
<PageSection variant={medium}>
<Card className="at-c-orgPane">
<CardHeader>
{detailTabs(['details', 'users', 'teams', 'admins', 'notifications'])}
<I18n>
{({ i18n }) => (
<Tabs labelText={i18n._(t`Organization detail tabs`)}>
{tabList.map(tab => (
<Tab
key={tab}
tab={tab}
location={location}
match={match}
currentTab={currentTab}
breadcrumb={parentBreadcrumbObj}
>
<Trans>{getTabName(tab)}</Trans>
</Tab>
))}
</Tabs>
)}
</I18n>
</CardHeader>
<CardBody>
{(currentTab && currentTab !== 'details') ? (

View File

@@ -14,7 +14,6 @@ export default ({
name,
userCount,
teamCount,
adminCount,
isSelected,
onSelect,
detailUrl,
@@ -46,7 +45,7 @@ export default ({
</span>
</div>
<div className="pf-c-data-list__cell">
<Link to={`${detailUrl}?tab=users`}>
<Link to={`${detailUrl}?tab=access`}>
<Trans>Users</Trans>
</Link>
<Badge isRead>
@@ -62,14 +61,6 @@ export default ({
{teamCount}
{' '}
</Badge>
<Link to={`${detailUrl}?tab=admins`}>
<Trans>Admins</Trans>
</Link>
<Badge isRead>
{' '}
{adminCount}
{' '}
</Badge>
</div>
<div className="pf-c-data-list__cell" />
</li>

View File

@@ -5,12 +5,31 @@ import OrganizationAdd from './views/Organization.add';
import OrganizationView from './views/Organization.view';
import OrganizationsList from './views/Organizations.list';
const Organizations = ({ match }) => (
export default ({ api, match }) => (
<Switch>
<Route path={`${match.path}/add`} component={OrganizationAdd} />
<Route path={`${match.path}/:id`} component={OrganizationView} />
<Route path={`${match.path}`} component={OrganizationsList} />
<Route
path={`${match.path}/add`}
render={() => (
<OrganizationAdd
api={api}
/>
)}
/>
<Route
path={`${match.path}/:id`}
render={() => (
<OrganizationView
api={api}
/>
)}
/>
<Route
path={`${match.path}`}
render={() => (
<OrganizationsList
api={api}
/>
)}
/>
</Switch>
);
export default Organizations;

View File

@@ -1,18 +0,0 @@
.at-c-tabs {
padding: 0 5px !important;
margin: 0 -10px !important;
.at-c-tabs__tab {
margin: 0 5px;
}
.at-c-tabs__tab.at-m-selected {
text-decoration: underline;
}
}
.at-c-orgPane {
a {
display: block;
}
}

View File

@@ -2,12 +2,10 @@ const getTabName = (tab) => {
let tabName = '';
if (tab === 'details') {
tabName = 'Details';
} else if (tab === 'users') {
tabName = 'Users';
} else if (tab === 'access') {
tabName = 'Access';
} else if (tab === 'teams') {
tabName = 'Teams';
} else if (tab === 'admins') {
tabName = 'Admins';
} else if (tab === 'notifications') {
tabName = 'Notifications';
}

View File

@@ -19,11 +19,9 @@ import {
} from '@patternfly/react-core';
import { ConfigContext } from '../../../context';
import { API_ORGANIZATIONS } from '../../../endpoints';
import { API_INSTANCE_GROUPS } from '../../../endpoints';
import api from '../../../api';
import AnsibleSelect from '../../../components/AnsibleSelect';
import Lookup from '../../../components/Lookup';
import AnsibleSelect from '../../../components/AnsibleSelect'
const { light } = PageSectionVariants;
class OrganizationAdd extends React.Component {
@@ -71,8 +69,9 @@ class OrganizationAdd extends React.Component {
}
async onSubmit() {
const { api } = this.props;
const data = Object.assign({}, { ...this.state });
await api.post(API_ORGANIZATIONS, data);
await api.createOrganization(data);
this.resetForm();
}
@@ -81,7 +80,8 @@ class OrganizationAdd extends React.Component {
}
async componentDidMount() {
const { data } = await api.get(API_INSTANCE_GROUPS);
const { api } = this.props;
const { data } = await api.getInstanceGroups();
let results = [];
data.results.map((result) => {
results.push({ id: result.id, name: result.name, isChecked: false });
@@ -92,6 +92,7 @@ class OrganizationAdd extends React.Component {
render() {
const { name, results } = this.state;
const enabled = name.length > 0; // TODO: add better form validation
return (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">

View File

@@ -2,16 +2,13 @@ import React, { Component, Fragment } from 'react';
import { i18nMark } from '@lingui/react';
import {
Switch,
Route
Route,
withRouter,
} from 'react-router-dom';
import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
import OrganizationDetail from '../components/OrganizationDetail';
import OrganizationEdit from '../components/OrganizationEdit';
import api from '../../../api';
import { API_ORGANIZATIONS } from '../../../endpoints';
class OrganizationView extends Component {
constructor (props) {
super(props);
@@ -30,6 +27,8 @@ class OrganizationView extends Component {
loading: false,
mounted: false
};
this.fetchOrganization = this.fetchOrganization.bind(this);
}
componentDidMount () {
@@ -47,13 +46,15 @@ class OrganizationView extends Component {
async fetchOrganization () {
const { mounted } = this.state;
const { api } = this.props;
if (mounted) {
this.setState({ error: false, loading: true });
const { match } = this.props;
const { parentBreadcrumbObj, organization } = this.state;
try {
const { data } = await api.get(`${API_ORGANIZATIONS}${match.params.id}/`);
const { data } = await api.getOrganizationDetails(match.params.id);
if (organization === 'loading') {
this.setState({ organization: data });
}
@@ -118,4 +119,4 @@ class OrganizationView extends Component {
}
}
export default OrganizationView;
export default withRouter(OrganizationView);

View File

@@ -17,9 +17,6 @@ import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem';
import Pagination from '../../../components/Pagination';
import api from '../../../api';
import { API_ORGANIZATIONS } from '../../../endpoints';
import {
encodeQueryString,
parseQueryString,
@@ -56,6 +53,15 @@ class Organizations extends Component {
results: [],
selected: [],
};
this.onSearch = this.onSearch.bind(this);
this.getQueryParams = this.getQueryParams.bind(this);
this.onSort = this.onSort.bind(this);
this.onSetPage = this.onSetPage.bind(this);
this.onSelectAll = this.onSelectAll.bind(this);
this.onSelect = this.onSelect.bind(this);
this.updateUrl = this.updateUrl.bind(this);
this.fetchOrganizations = this.fetchOrganizations.bind(this);
}
componentDidMount () {
@@ -78,7 +84,7 @@ class Organizations extends Component {
return Object.assign({}, this.defaultParams, searchParams, overrides);
}
onSort = (sortedColumnKey, sortOrder) => {
onSort(sortedColumnKey, sortOrder) {
const { page_size } = this.state;
let order_by = sortedColumnKey;
@@ -90,26 +96,26 @@ class Organizations extends Component {
const queryParams = this.getQueryParams({ order_by, page_size });
this.fetchOrganizations(queryParams);
};
}
onSetPage = (pageNumber, pageSize) => {
onSetPage (pageNumber, pageSize) {
const page = parseInt(pageNumber, 10);
const page_size = parseInt(pageSize, 10);
const queryParams = this.getQueryParams({ page, page_size });
this.fetchOrganizations(queryParams);
};
}
onSelectAll = isSelected => {
onSelectAll (isSelected) {
const { results } = this.state;
const selected = isSelected ? results.map(o => o.id) : [];
this.setState({ selected });
};
}
onSelect = id => {
onSelect (id) {
const { selected } = this.state;
const isSelected = selected.includes(id);
@@ -119,7 +125,7 @@ class Organizations extends Component {
} else {
this.setState({ selected: selected.concat(id) });
}
};
}
updateUrl (queryParams) {
const { history, location } = this.props;
@@ -132,6 +138,7 @@ class Organizations extends Component {
}
async fetchOrganizations (queryParams) {
const { api } = this.props;
const { page, page_size, order_by } = queryParams;
let sortOrder = 'ascending';
@@ -145,7 +152,7 @@ class Organizations extends Component {
this.setState({ error: false, loading: true });
try {
const { data } = await api.get(API_ORGANIZATIONS, queryParams);
const { data } = await api.getOrganizations(queryParams);
const { count, results } = data;
const pageCount = Math.ceil(count / page_size);
@@ -218,7 +225,6 @@ class Organizations extends Component {
parentBreadcrumb={parentBreadcrumb}
userCount={o.summary_fields.related_field_counts.users}
teamCount={o.summary_fields.related_field_counts.teams}
adminCount={o.summary_fields.related_field_counts.admins}
isSelected={selected.includes(o.id)}
onSelect={() => this.onSelect(o.id)}
/>