mirror of
https://github.com/ansible/awx.git
synced 2026-03-08 05:01:09 -02:30
update content loading and error handling
unwind error handling use auth cookie as source of truth, fetch config only when authenticated
This commit is contained in:
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import { withNetwork } from '../../contexts/Network';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
import SelectableCard from './SelectableCard';
|
||||
@@ -245,4 +244,4 @@ AddResourceRole.defaultProps = {
|
||||
};
|
||||
|
||||
export { AddResourceRole as _AddResourceRole };
|
||||
export default withI18n()(withNetwork(AddResourceRole));
|
||||
export default withI18n()(AddResourceRole);
|
||||
|
||||
25
src/components/ContentEmpty.jsx
Normal file
25
src/components/ContentEmpty.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
|
||||
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">
|
||||
{title || i18n._(t`No items found.`)}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{message}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentEmpty as _ContentEmpty };
|
||||
export default withI18n()(ContentEmpty);
|
||||
26
src/components/ContentError.jsx
Normal file
26
src/components/ContentError.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||
|
||||
// TODO: Pass actual error as prop and display expandable details for network errors.
|
||||
const ContentError = ({ i18n }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
||||
<Title size="lg">
|
||||
{i18n._(t`Something went wrong...`)}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{i18n._(t`There was an error loading this content. Please reload the page.`)}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentError as _ContentError };
|
||||
export default withI18n()(ContentError);
|
||||
19
src/components/ContentLoading.jsx
Normal file
19
src/components/ContentLoading.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
// TODO: Better loading state - skeleton lines / spinner, etc.
|
||||
const ContentLoading = ({ i18n }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateBody>
|
||||
{i18n._(t`Loading...`)}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentLoading as _ContentLoading };
|
||||
export default withI18n()(ContentLoading);
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { withNetwork } from '../../contexts/Network';
|
||||
import PaginatedDataList from '../PaginatedDataList';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import CheckboxListItem from '../ListItem';
|
||||
@@ -53,8 +52,8 @@ class Lookup extends React.Component {
|
||||
}
|
||||
|
||||
async getData () {
|
||||
const { getItems, handleHttpError, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
|
||||
const { getItems, location: { search } } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(this.qsConfig, search);
|
||||
|
||||
this.setState({ error: false });
|
||||
try {
|
||||
@@ -66,7 +65,7 @@ class Lookup extends React.Component {
|
||||
count
|
||||
});
|
||||
} catch (err) {
|
||||
handleHttpError(err) || this.setState({ error: true });
|
||||
this.setState({ error: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,4 +213,4 @@ Lookup.defaultProps = {
|
||||
};
|
||||
|
||||
export { Lookup as _Lookup };
|
||||
export default withI18n()(withNetwork(withRouter(Lookup)));
|
||||
export default withI18n()(withRouter(Lookup));
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { Redirect, withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { withRootDialog } from '../contexts/RootDialog';
|
||||
|
||||
const NotifyAndRedirect = ({
|
||||
to,
|
||||
push,
|
||||
from,
|
||||
exact,
|
||||
strict,
|
||||
sensitive,
|
||||
setRootDialogMessage,
|
||||
location,
|
||||
i18n
|
||||
}) => {
|
||||
setRootDialogMessage({
|
||||
title: '404',
|
||||
bodyText: (
|
||||
<Fragment>{i18n._(t`Cannot find route ${(<strong>{location.pathname}</strong>)}.`)}</Fragment>
|
||||
),
|
||||
variant: 'warning'
|
||||
});
|
||||
|
||||
return (
|
||||
<Redirect
|
||||
to={to}
|
||||
push={push}
|
||||
from={from}
|
||||
exact={exact}
|
||||
strict={strict}
|
||||
sensitive={sensitive}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { NotifyAndRedirect as _NotifyAndRedirect };
|
||||
export default withI18n()(withRootDialog(withRouter(NotifyAndRedirect)));
|
||||
@@ -1,18 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes, { arrayOf, shape, string, bool } from 'prop-types';
|
||||
import {
|
||||
DataList,
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
import { DataList } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ContentEmpty from '../ContentEmpty';
|
||||
import ContentError from '../ContentError';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import Pagination from '../Pagination';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import PaginatedDataListItem from './PaginatedDataListItem';
|
||||
@@ -37,11 +33,6 @@ const EmptyStateControlsWrapper = styled.div`
|
||||
class PaginatedDataList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
this.handleSetPage = this.handleSetPage.bind(this);
|
||||
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
||||
this.handleSort = this.handleSort.bind(this);
|
||||
@@ -79,7 +70,10 @@ class PaginatedDataList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const [orderBy, sortOrder] = this.getSortOrder();
|
||||
const {
|
||||
contentError,
|
||||
contentLoading,
|
||||
emptyStateControls,
|
||||
items,
|
||||
itemCount,
|
||||
@@ -93,66 +87,67 @@ class PaginatedDataList extends React.Component {
|
||||
i18n,
|
||||
renderToolbar,
|
||||
} = this.props;
|
||||
const { error } = this.state;
|
||||
const [orderBy, sortOrder] = this.getSortOrder();
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true }];
|
||||
return (
|
||||
<Fragment>
|
||||
{error && (
|
||||
<Fragment>
|
||||
<div>{error.message}</div>
|
||||
{error.response && (
|
||||
<div>{error.response.data.detail}</div>
|
||||
)}
|
||||
</Fragment> // TODO: replace with proper error handling
|
||||
)}
|
||||
{items.length === 0 ? (
|
||||
<Fragment>
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
|
||||
const itemDisplayName = ucFirst(pluralize(itemName));
|
||||
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
|
||||
|
||||
const dataListLabel = i18n._(t`${itemDisplayName} List`);
|
||||
const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `);
|
||||
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
|
||||
|
||||
let Content;
|
||||
if (contentLoading && items.length <= 0) {
|
||||
Content = (<ContentLoading />);
|
||||
} else if (contentError) {
|
||||
Content = (<ContentError />);
|
||||
} else if (items.length <= 0) {
|
||||
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
||||
} else {
|
||||
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
|
||||
}
|
||||
|
||||
if (items.length <= 0) {
|
||||
return (
|
||||
<Fragment>
|
||||
{emptyStateControls && (
|
||||
<EmptyStateControlsWrapper>
|
||||
{emptyStateControls}
|
||||
</EmptyStateControlsWrapper>
|
||||
)}
|
||||
{emptyStateControls && (
|
||||
<div css="border-bottom: 1px solid #d2d2d2" />
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">
|
||||
{i18n._(t`No ${ucFirst(itemNamePlural || pluralize(itemName))} Found `)}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{i18n._(t`Please add ${ucFirst(itemNamePlural || pluralize(itemName))} to populate this list `)}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{renderToolbar({
|
||||
sortedColumnKey: orderBy,
|
||||
sortOrder,
|
||||
columns,
|
||||
onSearch: () => { },
|
||||
onSort: this.handleSort,
|
||||
})}
|
||||
<DataList aria-label={i18n._(t`${ucFirst(pluralize(itemName))} List`)}>
|
||||
{items.map(item => (renderItem ? renderItem(item) : (
|
||||
<PaginatedDataListItem key={item.id} item={item} />
|
||||
)))}
|
||||
</DataList>
|
||||
<Pagination
|
||||
variant="bottom"
|
||||
itemCount={itemCount}
|
||||
page={queryParams.page || 1}
|
||||
perPage={queryParams.page_size}
|
||||
perPageOptions={showPageSizeOptions ? [
|
||||
{ title: '5', value: 5 },
|
||||
{ title: '10', value: 10 },
|
||||
{ title: '20', value: 20 },
|
||||
{ title: '50', value: 50 }
|
||||
] : []}
|
||||
onSetPage={this.handleSetPage}
|
||||
onPerPageSelect={this.handleSetPageSize}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
)}
|
||||
{Content}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderToolbar({
|
||||
sortedColumnKey: orderBy,
|
||||
sortOrder,
|
||||
columns,
|
||||
onSearch: () => { },
|
||||
onSort: this.handleSort,
|
||||
})}
|
||||
{Content}
|
||||
<Pagination
|
||||
variant="bottom"
|
||||
itemCount={itemCount}
|
||||
page={queryParams.page || 1}
|
||||
perPage={queryParams.page_size}
|
||||
perPageOptions={showPageSizeOptions ? [
|
||||
{ title: '5', value: 5 },
|
||||
{ title: '10', value: 10 },
|
||||
{ title: '20', value: 20 },
|
||||
{ title: '50', value: 50 }
|
||||
] : []}
|
||||
onSetPage={this.handleSetPage}
|
||||
onPerPageSelect={this.handleSetPageSize}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -178,14 +173,18 @@ PaginatedDataList.propTypes = {
|
||||
})),
|
||||
showPageSizeOptions: PropTypes.bool,
|
||||
renderToolbar: PropTypes.func,
|
||||
contentLoading: PropTypes.bool,
|
||||
contentError: PropTypes.bool,
|
||||
};
|
||||
|
||||
PaginatedDataList.defaultProps = {
|
||||
renderItem: null,
|
||||
contentLoading: false,
|
||||
contentError: false,
|
||||
toolbarColumns: [],
|
||||
itemName: 'item',
|
||||
itemNamePlural: '',
|
||||
showPageSizeOptions: true,
|
||||
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
|
||||
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user