mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 01:17:37 -02:30
Display details about network errors in alert modal and content error components (#288)
Display details about network errors in alert modal and content error components
This commit is contained in:
14
src/App.jsx
14
src/App.jsx
@@ -17,6 +17,7 @@ import AlertModal from '@components/AlertModal';
|
|||||||
import NavExpandableGroup from '@components/NavExpandableGroup';
|
import NavExpandableGroup from '@components/NavExpandableGroup';
|
||||||
import BrandLogo from '@components/BrandLogo';
|
import BrandLogo from '@components/BrandLogo';
|
||||||
import PageHeaderToolbar from '@components/PageHeaderToolbar';
|
import PageHeaderToolbar from '@components/PageHeaderToolbar';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import { ConfigProvider } from '@contexts/Config';
|
import { ConfigProvider } from '@contexts/Config';
|
||||||
|
|
||||||
const PageHeader = styled(PFPageHeader)`
|
const PageHeader = styled(PFPageHeader)`
|
||||||
@@ -48,7 +49,7 @@ class App extends Component {
|
|||||||
version: null,
|
version: null,
|
||||||
isAboutModalOpen: false,
|
isAboutModalOpen: false,
|
||||||
isNavOpen,
|
isNavOpen,
|
||||||
hasConfigError: false,
|
configError: null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleLogout = this.handleLogout.bind(this);
|
this.handleLogout = this.handleLogout.bind(this);
|
||||||
@@ -81,7 +82,9 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleConfigErrorClose () {
|
handleConfigErrorClose () {
|
||||||
this.setState({ hasConfigError: false });
|
this.setState({
|
||||||
|
configError: null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadConfig () {
|
async loadConfig () {
|
||||||
@@ -92,7 +95,7 @@ class App extends Component {
|
|||||||
|
|
||||||
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasConfigError: true });
|
this.setState({ configError: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ class App extends Component {
|
|||||||
isNavOpen,
|
isNavOpen,
|
||||||
me,
|
me,
|
||||||
version,
|
version,
|
||||||
hasConfigError,
|
configError
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
@@ -170,12 +173,13 @@ class App extends Component {
|
|||||||
onClose={this.handleAboutClose}
|
onClose={this.handleAboutClose}
|
||||||
/>
|
/>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={hasConfigError}
|
isOpen={configError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={this.handleConfigErrorClose}
|
onClose={this.handleConfigErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to retrieve configuration.`)}
|
{i18n._(t`Failed to retrieve configuration.`)}
|
||||||
|
<ErrorDetail error={configError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ const getIcon = (variant) => {
|
|||||||
return icon;
|
return icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ variant, children, ...props }) => (
|
export default ({ variant, children, ...props }) => {
|
||||||
<Modal className={`awx-c-modal${variant && ` at-c-alertModal at-c-alertModal--${variant}`}`} {...props}>
|
const { isOpen = null } = props;
|
||||||
{children}
|
props.isOpen = Boolean(isOpen);
|
||||||
{getIcon(variant)}
|
return (
|
||||||
</Modal>
|
<Modal className={`awx-c-modal${variant && ` at-c-alertModal at-c-alertModal--${variant}`}`} {...props}>
|
||||||
);
|
{children}
|
||||||
|
{getIcon(variant)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,26 +1,40 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import {
|
import {
|
||||||
Title,
|
Title,
|
||||||
EmptyState,
|
EmptyState as PFEmptyState,
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateBody
|
EmptyStateBody
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
// TODO: Pass actual error as prop and display expandable details for network errors.
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
const ContentError = ({ i18n }) => (
|
|
||||||
<EmptyState>
|
const EmptyState = styled(PFEmptyState)`
|
||||||
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
width: var(--pf-c-empty-state--m-lg--MaxWidth);
|
||||||
<Title size="lg">
|
`;
|
||||||
{i18n._(t`Something went wrong...`)}
|
|
||||||
</Title>
|
class ContentError extends React.Component {
|
||||||
<EmptyStateBody>
|
render () {
|
||||||
{i18n._(t`There was an error loading this content. Please reload the page.`)}
|
const { error, i18n } = this.props;
|
||||||
</EmptyStateBody>
|
return (
|
||||||
</EmptyState>
|
<EmptyState>
|
||||||
);
|
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
||||||
|
<Title size="lg">
|
||||||
|
{i18n._(t`Something went wrong...`)}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{i18n._(t`There was an error loading this content. Please reload the page.`)}
|
||||||
|
</EmptyStateBody>
|
||||||
|
{error && (
|
||||||
|
<ErrorDetail error={error} />
|
||||||
|
)}
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { ContentError as _ContentError };
|
export { ContentError as _ContentError };
|
||||||
export default withI18n()(ContentError);
|
export default withI18n()(ContentError);
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import ContentError from './ContentError';
|
|||||||
|
|
||||||
describe('ContentError', () => {
|
describe('ContentError', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(<ContentError />);
|
const wrapper = mountWithContexts(<ContentError error={new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'post'
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
95
src/components/ErrorDetail/ErrorDetail.jsx
Normal file
95
src/components/ErrorDetail/ErrorDetail.jsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card as PFCard,
|
||||||
|
CardBody as PFCardBody,
|
||||||
|
Expandable as PFExpandable
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const Card = styled(PFCard)`
|
||||||
|
background-color: var(--pf-global--BackgroundColor--200);
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardBody = styled(PFCardBody)`
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: scroll;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Expandable = styled(PFExpandable)`
|
||||||
|
text-align: left;
|
||||||
|
`;
|
||||||
|
|
||||||
|
class ErrorDetail extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isExpanded: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleToggle = this.handleToggle.bind(this);
|
||||||
|
this.renderNetworkError = this.renderNetworkError.bind(this);
|
||||||
|
this.renderStack = this.renderStack.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggle () {
|
||||||
|
const { isExpanded } = this.state;
|
||||||
|
this.setState({ isExpanded: !isExpanded });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNetworkError () {
|
||||||
|
const { error } = this.props;
|
||||||
|
const { response } = error;
|
||||||
|
|
||||||
|
const message = typeof response.data === 'string'
|
||||||
|
? response.data
|
||||||
|
: response.data.detail;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<CardBody>
|
||||||
|
{response.config.method.toUpperCase()}
|
||||||
|
{' '}
|
||||||
|
{response.config.url}
|
||||||
|
{' '}
|
||||||
|
<strong>
|
||||||
|
{response.status}
|
||||||
|
</strong>
|
||||||
|
</CardBody>
|
||||||
|
<CardBody>{message}</CardBody>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStack () {
|
||||||
|
const { error } = this.props;
|
||||||
|
return (<CardBody>{error.stack}</CardBody>);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isExpanded } = this.state;
|
||||||
|
const { error, i18n } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Expandable toggleText={i18n._(t`Details`)} onToggle={this.handleToggle} isExpanded={isExpanded}>
|
||||||
|
<Card>
|
||||||
|
{Object.prototype.hasOwnProperty.call(error, 'response')
|
||||||
|
? this.renderNetworkError()
|
||||||
|
: this.renderStack()
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
</Expandable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorDetail.propTypes = {
|
||||||
|
error: PropTypes.instanceOf(Error).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(ErrorDetail);
|
||||||
19
src/components/ErrorDetail/ErrorDetail.test.jsx
Normal file
19
src/components/ErrorDetail/ErrorDetail.test.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
|
||||||
|
import ErrorDetail from './ErrorDetail';
|
||||||
|
|
||||||
|
describe('ErrorDetail', () => {
|
||||||
|
test('renders the expected content', () => {
|
||||||
|
const wrapper = mountWithContexts(<ErrorDetail error={new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'post'
|
||||||
|
},
|
||||||
|
data: 'An error occurred'
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
1
src/components/ErrorDetail/index.js
Normal file
1
src/components/ErrorDetail/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ErrorDetail';
|
||||||
@@ -8,6 +8,7 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI } from '@api';
|
||||||
|
|
||||||
const StyledLaunchButton = styled(Button)`
|
const StyledLaunchButton = styled(Button)`
|
||||||
@@ -28,7 +29,7 @@ class LaunchButton extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
launchError: false,
|
launchError: null,
|
||||||
promptError: false
|
promptError: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ class LaunchButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLaunchErrorClose () {
|
handleLaunchErrorClose () {
|
||||||
this.setState({ launchError: false });
|
this.setState({ launchError: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePromptErrorClose () {
|
handlePromptErrorClose () {
|
||||||
@@ -55,8 +56,8 @@ class LaunchButton extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
this.setState({ promptError: true });
|
this.setState({ promptError: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
this.setState({ launchError: true });
|
this.setState({ launchError: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ class LaunchButton extends React.Component {
|
|||||||
onClose={this.handleLaunchErrorClose}
|
onClose={this.handleLaunchErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to launch job.`)}
|
{i18n._(t`Failed to launch job.`)}
|
||||||
|
<ErrorDetail error={launchError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={promptError}
|
isOpen={promptError}
|
||||||
|
|||||||
@@ -43,7 +43,16 @@ describe('LaunchButton', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
test('displays error modal after unsuccessful launch', async (done) => {
|
test('displays error modal after unsuccessful launch', async (done) => {
|
||||||
JobTemplatesAPI.launch.mockRejectedValue({});
|
JobTemplatesAPI.launch.mockRejectedValue(new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/v2/job_templates/1/launch'
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
status: 403
|
||||||
|
}
|
||||||
|
}));
|
||||||
const wrapper = mountWithContexts(<LaunchButton templateId={1} />);
|
const wrapper = mountWithContexts(<LaunchButton templateId={1} />);
|
||||||
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
|
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
|
||||||
launchButton.simulate('click');
|
launchButton.simulate('click');
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class PaginatedDataList extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
const [orderBy, sortOrder] = this.getSortOrder();
|
const [orderBy, sortOrder] = this.getSortOrder();
|
||||||
const {
|
const {
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
emptyStateControls,
|
emptyStateControls,
|
||||||
items,
|
items,
|
||||||
@@ -100,8 +100,8 @@ class PaginatedDataList extends React.Component {
|
|||||||
let Content;
|
let Content;
|
||||||
if (hasContentLoading && items.length <= 0) {
|
if (hasContentLoading && items.length <= 0) {
|
||||||
Content = (<ContentLoading />);
|
Content = (<ContentLoading />);
|
||||||
} else if (hasContentError) {
|
} else if (contentError) {
|
||||||
Content = (<ContentError />);
|
Content = (<ContentError error={contentError} />);
|
||||||
} else if (items.length <= 0) {
|
} else if (items.length <= 0) {
|
||||||
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
||||||
} else {
|
} else {
|
||||||
@@ -174,12 +174,12 @@ PaginatedDataList.propTypes = {
|
|||||||
showPageSizeOptions: PropTypes.bool,
|
showPageSizeOptions: PropTypes.bool,
|
||||||
renderToolbar: PropTypes.func,
|
renderToolbar: PropTypes.func,
|
||||||
hasContentLoading: PropTypes.bool,
|
hasContentLoading: PropTypes.bool,
|
||||||
hasContentError: PropTypes.bool,
|
contentError: PropTypes.shape(),
|
||||||
};
|
};
|
||||||
|
|
||||||
PaginatedDataList.defaultProps = {
|
PaginatedDataList.defaultProps = {
|
||||||
hasContentLoading: false,
|
hasContentLoading: false,
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
toolbarColumns: [],
|
toolbarColumns: [],
|
||||||
itemName: 'item',
|
itemName: 'item',
|
||||||
itemNamePlural: '',
|
itemNamePlural: '',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Job extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
job: null,
|
job: null,
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
isInitialized: false
|
isInitialized: false
|
||||||
};
|
};
|
||||||
@@ -46,13 +46,13 @@ class Job extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
const id = parseInt(match.params.id, 10);
|
const id = parseInt(match.params.id, 10);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await JobsAPI.readDetail(id);
|
const { data } = await JobsAPI.readDetail(id);
|
||||||
setBreadcrumb(data);
|
setBreadcrumb(data);
|
||||||
this.setState({ job: data });
|
this.setState({ job: data });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ class Job extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
job,
|
job,
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
isInitialized
|
isInitialized
|
||||||
} = this.state;
|
} = this.state;
|
||||||
@@ -103,11 +103,11 @@ class Job extends Component {
|
|||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && hasContentError) {
|
if (!hasContentLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
<ContentError />
|
<ContentError error={contentError} />
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { UnifiedJobsAPI } from '@api';
|
import { UnifiedJobsAPI } from '@api';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import DatalistToolbar from '@components/DataListToolbar';
|
import DatalistToolbar from '@components/DataListToolbar';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarDeleteButton
|
ToolbarDeleteButton
|
||||||
} from '@components/PaginatedDataList';
|
} from '@components/PaginatedDataList';
|
||||||
@@ -31,8 +32,8 @@ class JobList extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasDeletionError: false,
|
deletionError: null,
|
||||||
selected: [],
|
selected: [],
|
||||||
jobs: [],
|
jobs: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
@@ -56,7 +57,7 @@ class JobList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteErrorClose () {
|
handleDeleteErrorClose () {
|
||||||
this.setState({ hasDeletionError: false });
|
this.setState({ deletionError: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectAll (isSelected) {
|
handleSelectAll (isSelected) {
|
||||||
@@ -76,11 +77,11 @@ class JobList extends Component {
|
|||||||
|
|
||||||
async handleDelete () {
|
async handleDelete () {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
this.setState({ hasContentLoading: true, hasDeletionError: false });
|
this.setState({ hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
await Promise.all(selected.map(({ id }) => UnifiedJobsAPI.destroy(id)));
|
await Promise.all(selected.map(({ id }) => UnifiedJobsAPI.destroy(id)));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasDeletionError: true });
|
this.setState({ deletionError: err });
|
||||||
} finally {
|
} finally {
|
||||||
await this.loadJobs();
|
await this.loadJobs();
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ class JobList extends Component {
|
|||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data: { count, results } } = await UnifiedJobsAPI.read(params);
|
const { data: { count, results } } = await UnifiedJobsAPI.read(params);
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -99,7 +100,7 @@ class JobList extends Component {
|
|||||||
selected: [],
|
selected: [],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -107,9 +108,9 @@ class JobList extends Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
hasDeletionError,
|
deletionError,
|
||||||
jobs,
|
jobs,
|
||||||
itemCount,
|
itemCount,
|
||||||
selected,
|
selected,
|
||||||
@@ -125,7 +126,7 @@ class JobList extends Component {
|
|||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={hasContentError}
|
contentError={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={hasContentLoading}
|
||||||
items={jobs}
|
items={jobs}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -166,12 +167,13 @@ class JobList extends Component {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={hasDeletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={this.handleDeleteErrorClose}
|
onClose={this.handleDeleteErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more jobs.`)}
|
{i18n._(t`Failed to delete one or more jobs.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Organization extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
organization: null,
|
organization: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
isNotifAdmin: false,
|
isNotifAdmin: false,
|
||||||
isAuditorOfThisOrg: false,
|
isAuditorOfThisOrg: false,
|
||||||
@@ -50,7 +50,7 @@ class Organization extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
const id = parseInt(match.params.id, 10);
|
const id = parseInt(match.params.id, 10);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const [{ data }, notifAdminRes, auditorRes, adminRes] = await Promise.all([
|
const [{ data }, notifAdminRes, auditorRes, adminRes] = await Promise.all([
|
||||||
OrganizationsAPI.readDetail(id),
|
OrganizationsAPI.readDetail(id),
|
||||||
@@ -66,7 +66,7 @@ class Organization extends Component {
|
|||||||
isAdminOfThisOrg: adminRes.data.results.length > 0
|
isAdminOfThisOrg: adminRes.data.results.length > 0
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState(({ hasContentError: true }));
|
this.setState(({ contentError: err }));
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,13 @@ class Organization extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
const id = parseInt(match.params.id, 10);
|
const id = parseInt(match.params.id, 10);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await OrganizationsAPI.readDetail(id);
|
const { data } = await OrganizationsAPI.readDetail(id);
|
||||||
setBreadcrumb(data);
|
setBreadcrumb(data);
|
||||||
this.setState({ organization: data });
|
this.setState({ organization: data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState(({ hasContentError: true }));
|
this.setState(({ contentError: err }));
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ class Organization extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
organization,
|
organization,
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
isInitialized,
|
isInitialized,
|
||||||
isNotifAdmin,
|
isNotifAdmin,
|
||||||
@@ -162,11 +162,11 @@ class Organization extends Component {
|
|||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && hasContentError) {
|
if (!hasContentLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
<ContentError />
|
<ContentError error={contentError} />
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { OrganizationsAPI, TeamsAPI, UsersAPI } from '@api';
|
|||||||
import AddResourceRole from '@components/AddRole/AddResourceRole';
|
import AddResourceRole from '@components/AddRole/AddResourceRole';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import DataListToolbar from '@components/DataListToolbar';
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import PaginatedDataList, { ToolbarAddButton } from '@components/PaginatedDataList';
|
import PaginatedDataList, { ToolbarAddButton } from '@components/PaginatedDataList';
|
||||||
import {
|
import {
|
||||||
getQSConfig,
|
getQSConfig,
|
||||||
@@ -33,9 +34,9 @@ class OrganizationAccess extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
accessRecords: [],
|
accessRecords: [],
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
hasDeletionError: false,
|
deletionError: null,
|
||||||
deletionRecord: null,
|
deletionRecord: null,
|
||||||
deletionRole: null,
|
deletionRole: null,
|
||||||
isAddModalOpen: false,
|
isAddModalOpen: false,
|
||||||
@@ -70,7 +71,7 @@ class OrganizationAccess extends React.Component {
|
|||||||
const { organization, location } = this.props;
|
const { organization, location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@@ -79,8 +80,8 @@ class OrganizationAccess extends React.Component {
|
|||||||
}
|
}
|
||||||
} = await OrganizationsAPI.readAccessList(organization.id, params);
|
} = await OrganizationsAPI.readAccessList(organization.id, params);
|
||||||
this.setState({ itemCount, accessRecords });
|
this.setState({ itemCount, accessRecords });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ class OrganizationAccess extends React.Component {
|
|||||||
|
|
||||||
handleDeleteErrorClose () {
|
handleDeleteErrorClose () {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasDeletionError: false,
|
deletionError: null,
|
||||||
deletionRecord: null,
|
deletionRecord: null,
|
||||||
deletionRole: null
|
deletionRole: null
|
||||||
});
|
});
|
||||||
@@ -123,10 +124,10 @@ class OrganizationAccess extends React.Component {
|
|||||||
deletionRole: null,
|
deletionRole: null,
|
||||||
deletionRecord: null
|
deletionRecord: null
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
this.setState({
|
this.setState({
|
||||||
hasContentLoading: false,
|
hasContentLoading: false,
|
||||||
hasDeletionError: true
|
deletionError: err
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,21 +149,21 @@ class OrganizationAccess extends React.Component {
|
|||||||
const { organization, i18n } = this.props;
|
const { organization, i18n } = this.props;
|
||||||
const {
|
const {
|
||||||
accessRecords,
|
accessRecords,
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
deletionRole,
|
deletionRole,
|
||||||
deletionRecord,
|
deletionRecord,
|
||||||
hasDeletionError,
|
deletionError,
|
||||||
itemCount,
|
itemCount,
|
||||||
isAddModalOpen,
|
isAddModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const canEdit = organization.summary_fields.user_capabilities.edit;
|
const canEdit = organization.summary_fields.user_capabilities.edit;
|
||||||
const isDeleteModalOpen = !hasContentLoading && !hasDeletionError && deletionRole;
|
const isDeleteModalOpen = !hasContentLoading && !deletionError && deletionRole;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={hasContentError}
|
error={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={hasContentLoading}
|
||||||
items={accessRecords}
|
items={accessRecords}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -205,12 +206,13 @@ class OrganizationAccess extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={hasDeletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={this.handleDeleteErrorClose}
|
onClose={this.handleDeleteErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete role`)}
|
{i18n._(t`Failed to delete role`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ describe('<OrganizationAccess />', () => {
|
|||||||
await waitForElement(wrapper, 'OrganizationAccessItem', el => el.length === 2);
|
await waitForElement(wrapper, 'OrganizationAccessItem', el => el.length === 2);
|
||||||
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(data.results);
|
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(data.results);
|
||||||
expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe(false);
|
expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe(false);
|
||||||
expect(wrapper.find('OrganizationAccess').state('hasContentError')).toBe(false);
|
expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(null);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<WithI18n
|
<WithI18n
|
||||||
hasContentError={false}
|
error={null}
|
||||||
hasContentLoading={true}
|
hasContentLoading={true}
|
||||||
itemCount={0}
|
itemCount={0}
|
||||||
itemName="role"
|
itemName="role"
|
||||||
@@ -80,7 +80,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
|||||||
withHash={true}
|
withHash={true}
|
||||||
>
|
>
|
||||||
<withRouter(PaginatedDataList)
|
<withRouter(PaginatedDataList)
|
||||||
hasContentError={false}
|
error={null}
|
||||||
hasContentLoading={true}
|
hasContentLoading={true}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
itemCount={0}
|
itemCount={0}
|
||||||
@@ -124,7 +124,8 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Route>
|
<Route>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={false}
|
contentError={null}
|
||||||
|
error={null}
|
||||||
hasContentLoading={true}
|
hasContentLoading={true}
|
||||||
history={"/history/"}
|
history={"/history/"}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
@@ -220,7 +221,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
|
|||||||
</I18n>
|
</I18n>
|
||||||
</WithI18n>
|
</WithI18n>
|
||||||
<_default
|
<_default
|
||||||
isOpen={false}
|
isOpen={null}
|
||||||
onClose={[Function]}
|
onClose={[Function]}
|
||||||
title="Error!"
|
title="Error!"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class OrganizationDetail extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
instanceGroups: [],
|
instanceGroups: [],
|
||||||
};
|
};
|
||||||
@@ -39,7 +39,7 @@ class OrganizationDetail extends Component {
|
|||||||
const { data: { results = [] } } = await OrganizationsAPI.readInstanceGroups(id);
|
const { data: { results = [] } } = await OrganizationsAPI.readInstanceGroups(id);
|
||||||
this.setState({ instanceGroups: [...results] });
|
this.setState({ instanceGroups: [...results] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ class OrganizationDetail extends Component {
|
|||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
hasContentError,
|
contentError,
|
||||||
instanceGroups,
|
instanceGroups,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ class OrganizationDetail extends Component {
|
|||||||
return (<ContentLoading />);
|
return (<ContentLoading />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasContentError) {
|
if (contentError) {
|
||||||
return (<ContentError />);
|
return (<ContentError error={contentError} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import DataListToolbar from '@components/DataListToolbar';
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarAddButton,
|
ToolbarAddButton,
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
@@ -31,8 +32,8 @@ class OrganizationsList extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasDeletionError: false,
|
deletionError: null,
|
||||||
organizations: [],
|
organizations: [],
|
||||||
selected: [],
|
selected: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
@@ -75,17 +76,17 @@ class OrganizationsList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteErrorClose () {
|
handleDeleteErrorClose () {
|
||||||
this.setState({ hasDeletionError: false });
|
this.setState({ deletionError: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleOrgDelete () {
|
async handleOrgDelete () {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
|
|
||||||
this.setState({ hasContentLoading: true, hasDeletionError: false });
|
this.setState({ hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
|
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasDeletionError: true });
|
this.setState({ deletionError: err });
|
||||||
} finally {
|
} finally {
|
||||||
await this.loadOrganizations();
|
await this.loadOrganizations();
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ class OrganizationsList extends Component {
|
|||||||
optionsPromise,
|
optionsPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const [{ data: { count, results } }, { data: { actions } }] = await promises;
|
const [{ data: { count, results } }, { data: { actions } }] = await promises;
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -118,7 +119,7 @@ class OrganizationsList extends Component {
|
|||||||
selected: [],
|
selected: [],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState(({ hasContentError: true }));
|
this.setState(({ contentError: err }));
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -131,9 +132,9 @@ class OrganizationsList extends Component {
|
|||||||
const {
|
const {
|
||||||
actions,
|
actions,
|
||||||
itemCount,
|
itemCount,
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
hasDeletionError,
|
deletionError,
|
||||||
selected,
|
selected,
|
||||||
organizations,
|
organizations,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
@@ -147,7 +148,7 @@ class OrganizationsList extends Component {
|
|||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={hasContentError}
|
contentError={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={hasContentLoading}
|
||||||
items={organizations}
|
items={organizations}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -194,12 +195,13 @@ class OrganizationsList extends Component {
|
|||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={hasDeletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={this.handleDeleteErrorClose}
|
onClose={this.handleDeleteErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more organizations.`)}
|
{i18n._(t`Failed to delete one or more organizations.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ const mockAPIOrgsList = {
|
|||||||
describe('<OrganizationsList />', () => {
|
describe('<OrganizationsList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
OrganizationsAPI.read = () => Promise.resolve({
|
||||||
|
data: {
|
||||||
|
count: 0,
|
||||||
|
results: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
OrganizationsAPI.readOptions = () => Promise.resolve({
|
||||||
|
data: {
|
||||||
|
actions: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<OrganizationsList />);
|
mountWithContexts(<OrganizationsList />);
|
||||||
});
|
});
|
||||||
@@ -122,8 +136,17 @@ describe('<OrganizationsList />', () => {
|
|||||||
expect(fetchOrgs).toBeCalled();
|
expect(fetchOrgs).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('error is shown when org not successfully deleted from api', async () => {
|
test('error is shown when org not successfully deleted from api', async (done) => {
|
||||||
OrganizationsAPI.destroy = () => Promise.reject();
|
OrganizationsAPI.destroy.mockRejectedValue(new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/organizations/1'
|
||||||
|
},
|
||||||
|
data: 'An error occurred'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
wrapper = mountWithContexts(<OrganizationsList />);
|
wrapper = mountWithContexts(<OrganizationsList />);
|
||||||
wrapper.find('OrganizationsList').setState({
|
wrapper.find('OrganizationsList').setState({
|
||||||
organizations: mockAPIOrgsList.data.results,
|
organizations: mockAPIOrgsList.data.results,
|
||||||
@@ -133,5 +156,6 @@ describe('<OrganizationsList />', () => {
|
|||||||
});
|
});
|
||||||
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
|
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
|
||||||
await waitForElement(wrapper, 'Modal', (el) => el.props().isOpen === true && el.props().title === 'Error!');
|
await waitForElement(wrapper, 'Modal', (el) => el.props().isOpen === true && el.props().title === 'Error!');
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { t } from '@lingui/macro';
|
|||||||
|
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import NotificationListItem from '@components/NotificationsList/NotificationListItem';
|
import NotificationListItem from '@components/NotificationsList/NotificationListItem';
|
||||||
import PaginatedDataList from '@components/PaginatedDataList';
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
import { getQSConfig, parseNamespacedQueryString } from '@util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '@util/qs';
|
||||||
@@ -26,9 +27,9 @@ class OrganizationNotifications extends Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
toggleError: false,
|
toggleError: null,
|
||||||
toggleLoading: false,
|
toggleLoading: false,
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
@@ -55,7 +56,7 @@ class OrganizationNotifications extends Component {
|
|||||||
const { id, location } = this.props;
|
const { id, location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@@ -85,8 +86,8 @@ class OrganizationNotifications extends Component {
|
|||||||
successTemplateIds: successTemplates.results.map(s => s.id),
|
successTemplateIds: successTemplates.results.map(s => s.id),
|
||||||
errorTemplateIds: errorTemplates.results.map(e => e.id),
|
errorTemplateIds: errorTemplates.results.map(e => e.id),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -125,20 +126,20 @@ class OrganizationNotifications extends Component {
|
|||||||
);
|
);
|
||||||
this.setState(stateUpdateFunction);
|
this.setState(stateUpdateFunction);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ toggleError: true });
|
this.setState({ toggleError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ toggleLoading: false });
|
this.setState({ toggleLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNotificationErrorClose () {
|
handleNotificationErrorClose () {
|
||||||
this.setState({ toggleError: false });
|
this.setState({ toggleError: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { canToggleNotifications, i18n } = this.props;
|
const { canToggleNotifications, i18n } = this.props;
|
||||||
const {
|
const {
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
toggleError,
|
toggleError,
|
||||||
toggleLoading,
|
toggleLoading,
|
||||||
@@ -151,7 +152,7 @@ class OrganizationNotifications extends Component {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={hasContentError}
|
contentError={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={hasContentLoading}
|
||||||
items={notifications}
|
items={notifications}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -171,12 +172,13 @@ class OrganizationNotifications extends Component {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
|
isOpen={toggleError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
isOpen={toggleError && !toggleLoading}
|
|
||||||
onClose={this.handleNotificationErrorClose}
|
onClose={this.handleNotificationErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to toggle notification.`)}
|
{i18n._(t`Failed to toggle notification.`)}
|
||||||
|
<ErrorDetail error={toggleError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<WithI18n
|
<WithI18n
|
||||||
hasContentError={false}
|
contentError={null}
|
||||||
hasContentLoading={false}
|
hasContentLoading={false}
|
||||||
itemCount={2}
|
itemCount={2}
|
||||||
itemName="notification"
|
itemName="notification"
|
||||||
@@ -101,7 +101,7 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
withHash={true}
|
withHash={true}
|
||||||
>
|
>
|
||||||
<withRouter(PaginatedDataList)
|
<withRouter(PaginatedDataList)
|
||||||
hasContentError={false}
|
contentError={null}
|
||||||
hasContentLoading={false}
|
hasContentLoading={false}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
itemCount={2}
|
itemCount={2}
|
||||||
@@ -161,7 +161,7 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Route>
|
<Route>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={false}
|
contentError={null}
|
||||||
hasContentLoading={false}
|
hasContentLoading={false}
|
||||||
history={"/history/"}
|
history={"/history/"}
|
||||||
i18n={"/i18n/"}
|
i18n={"/i18n/"}
|
||||||
@@ -3295,7 +3295,7 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
|
|||||||
</I18n>
|
</I18n>
|
||||||
</WithI18n>
|
</WithI18n>
|
||||||
<_default
|
<_default
|
||||||
isOpen={false}
|
isOpen={null}
|
||||||
onClose={[Function]}
|
onClose={[Function]}
|
||||||
title="Error!"
|
title="Error!"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class OrganizationTeams extends React.Component {
|
|||||||
this.loadOrganizationTeamsList = this.loadOrganizationTeamsList.bind(this);
|
this.loadOrganizationTeamsList = this.loadOrganizationTeamsList.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
teams: [],
|
teams: [],
|
||||||
@@ -41,7 +41,7 @@ class OrganizationTeams extends React.Component {
|
|||||||
const { id, location } = this.props;
|
const { id, location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
this.setState({ hasContentLoading: true, hasContentError: false });
|
this.setState({ hasContentLoading: true, contentError: null });
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { count = 0, results = [] },
|
data: { count = 0, results = [] },
|
||||||
@@ -50,18 +50,18 @@ class OrganizationTeams extends React.Component {
|
|||||||
itemCount: count,
|
itemCount: count,
|
||||||
teams: results,
|
teams: results,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasContentError, hasContentLoading, teams, itemCount } = this.state;
|
const { contentError, hasContentLoading, teams, itemCount } = this.state;
|
||||||
return (
|
return (
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={hasContentError}
|
contentError={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={hasContentLoading}
|
||||||
items={teams}
|
items={teams}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class JobTemplateDetail extends Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
contentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
instanceGroups: []
|
instanceGroups: []
|
||||||
};
|
};
|
||||||
@@ -40,8 +40,8 @@ class JobTemplateDetail extends Component {
|
|||||||
try {
|
try {
|
||||||
const { data } = await JobTemplatesAPI.readInstanceGroups(match.params.id);
|
const { data } = await JobTemplatesAPI.readInstanceGroups(match.params.id);
|
||||||
this.setState({ instanceGroups: [...data.results] });
|
this.setState({ instanceGroups: [...data.results] });
|
||||||
} catch {
|
} catch (err) {
|
||||||
this.setState({ contentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ class JobTemplateDetail extends Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (contentError) {
|
if (contentError) {
|
||||||
return (<ContentError />);
|
return (<ContentError error={contentError} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Template extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
template: null,
|
template: null,
|
||||||
};
|
};
|
||||||
@@ -46,15 +46,13 @@ class Template extends Component {
|
|||||||
const { setBreadcrumb, match } = this.props;
|
const { setBreadcrumb, match } = this.props;
|
||||||
const { id } = match.params;
|
const { id } = match.params;
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await JobTemplatesAPI.readDetail(id);
|
const { data } = await JobTemplatesAPI.readDetail(id);
|
||||||
setBreadcrumb(data);
|
setBreadcrumb(data);
|
||||||
this.setState({
|
this.setState({ template: data });
|
||||||
template: data,
|
} catch (err) {
|
||||||
});
|
this.setState({ contentError: err });
|
||||||
} catch {
|
|
||||||
this.setState({ hasContentError: true });
|
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -68,7 +66,7 @@ class Template extends Component {
|
|||||||
match,
|
match,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
template
|
template
|
||||||
} = this.state;
|
} = this.state;
|
||||||
@@ -98,11 +96,11 @@ class Template extends Component {
|
|||||||
cardHeader = null;
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && hasContentError) {
|
if (!hasContentLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
<ContentError />
|
<ContentError error={contentError} />
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@api';
|
} from '@api';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import DatalistToolbar from '@components/DataListToolbar';
|
import DatalistToolbar from '@components/DataListToolbar';
|
||||||
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import PaginatedDataList, {
|
import PaginatedDataList, {
|
||||||
ToolbarDeleteButton
|
ToolbarDeleteButton
|
||||||
} from '@components/PaginatedDataList';
|
} from '@components/PaginatedDataList';
|
||||||
@@ -37,8 +38,8 @@ class TemplatesList extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasContentLoading: true,
|
hasContentLoading: true,
|
||||||
hasContentError: false,
|
contentError: null,
|
||||||
hasDeletionError: false,
|
deletionError: null,
|
||||||
selected: [],
|
selected: [],
|
||||||
templates: [],
|
templates: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
@@ -62,7 +63,7 @@ class TemplatesList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteErrorClose () {
|
handleDeleteErrorClose () {
|
||||||
this.setState({ hasDeletionError: false });
|
this.setState({ deletionError: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectAll (isSelected) {
|
handleSelectAll (isSelected) {
|
||||||
@@ -83,7 +84,7 @@ class TemplatesList extends Component {
|
|||||||
async handleTemplateDelete () {
|
async handleTemplateDelete () {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
|
|
||||||
this.setState({ hasContentLoading: true, hasDeletionError: false });
|
this.setState({ hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
await Promise.all(selected.map(({ type, id }) => {
|
await Promise.all(selected.map(({ type, id }) => {
|
||||||
let deletePromise;
|
let deletePromise;
|
||||||
@@ -95,7 +96,7 @@ class TemplatesList extends Component {
|
|||||||
return deletePromise;
|
return deletePromise;
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasDeletionError: true });
|
this.setState({ deletionError: err });
|
||||||
} finally {
|
} finally {
|
||||||
await this.loadTemplates();
|
await this.loadTemplates();
|
||||||
}
|
}
|
||||||
@@ -105,7 +106,7 @@ class TemplatesList extends Component {
|
|||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||||
|
|
||||||
this.setState({ hasContentError: false, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params);
|
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params);
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -114,7 +115,7 @@ class TemplatesList extends Component {
|
|||||||
selected: [],
|
selected: [],
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ hasContentError: true });
|
this.setState({ contentError: err });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ hasContentLoading: false });
|
this.setState({ hasContentLoading: false });
|
||||||
}
|
}
|
||||||
@@ -122,9 +123,9 @@ class TemplatesList extends Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
hasContentError,
|
contentError,
|
||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
hasDeletionError,
|
deletionError,
|
||||||
templates,
|
templates,
|
||||||
itemCount,
|
itemCount,
|
||||||
selected,
|
selected,
|
||||||
@@ -139,7 +140,7 @@ class TemplatesList extends Component {
|
|||||||
<PageSection variant={medium}>
|
<PageSection variant={medium}>
|
||||||
<Card>
|
<Card>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
hasContentError={hasContentError}
|
error={contentError}
|
||||||
hasContentLoading={hasContentLoading}
|
hasContentLoading={hasContentLoading}
|
||||||
items={templates}
|
items={templates}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -180,12 +181,13 @@ class TemplatesList extends Component {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={hasDeletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={this.handleDeleteErrorClose}
|
onClose={this.handleDeleteErrorClose}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more template.`)}
|
{i18n._(t`Failed to delete one or more template.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -151,7 +151,15 @@ describe('<TemplatesList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('error is shown when template not successfully deleted from api', async () => {
|
test('error is shown when template not successfully deleted from api', async () => {
|
||||||
JobTemplatesAPI.destroy = () => Promise.reject();
|
JobTemplatesAPI.destroy.mockRejectedValue(new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/job_templates/1'
|
||||||
|
},
|
||||||
|
data: 'An error occurred'
|
||||||
|
}
|
||||||
|
}));
|
||||||
const wrapper = mountWithContexts(<TemplatesList />);
|
const wrapper = mountWithContexts(<TemplatesList />);
|
||||||
wrapper.find('TemplatesList').setState({
|
wrapper.find('TemplatesList').setState({
|
||||||
templates: mockTemplates,
|
templates: mockTemplates,
|
||||||
|
|||||||
Reference in New Issue
Block a user