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:
Jake McDermott
2019-05-09 15:59:43 -04:00
parent 534418c81a
commit e72f0bcfd4
50 changed files with 4721 additions and 4724 deletions

View File

@@ -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);

View 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);

View 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);

View 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);

View File

@@ -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));

View File

@@ -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)));

View File

@@ -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} />),
};