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

@@ -4,9 +4,6 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Config } from '../../contexts/Config';
import { NetworkProvider } from '../../contexts/Network';
import { withRootDialog } from '../../contexts/RootDialog';
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
import OrganizationsList from './screens/OrganizationsList';
@@ -49,7 +46,7 @@ class Organizations extends Component {
}
render () {
const { match, history, location, setRootDialogMessage, i18n } = this.props;
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return (
@@ -66,34 +63,17 @@ class Organizations extends Component {
/>
<Route
path={`${match.path}/:id`}
render={({ match: newRouteMatch }) => (
<NetworkProvider
handle404={() => {
history.replace('/organizations');
setRootDialogMessage({
title: '404',
bodyText: (
<Fragment>
{i18n._(t`Cannot find organization with ID`)}
<strong>{` ${newRouteMatch.params.id}`}</strong>
.
</Fragment>
),
variant: 'warning'
});
}}
>
<Config>
{({ me }) => (
<Organization
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
/>
)}
</Config>
</NetworkProvider>
render={() => (
<Config>
{({ me }) => (
<Organization
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
/>
)}
</Config>
)}
/>
<Route
@@ -109,4 +89,4 @@ class Organizations extends Component {
}
export { Organizations as _Organizations };
export default withI18n()(withRootDialog(withRouter(Organizations)));
export default withI18n()(withRouter(Organizations));

View File

@@ -7,8 +7,6 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
import Lookup from '../../../components/Lookup';
import { withNetwork } from '../../../contexts/Network';
import { InstanceGroupsAPI } from '../../../api';
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params);
@@ -66,4 +64,4 @@ InstanceGroupsLookup.defaultProps = {
tooltip: '',
};
export default withI18n()(withNetwork(InstanceGroupsLookup));
export default withI18n()(InstanceGroupsLookup);

View File

@@ -14,7 +14,6 @@ import {
} from '@patternfly/react-core';
import { Config } from '../../../contexts/Config';
import { withNetwork } from '../../../contexts/Network';
import FormRow from '../../../components/FormRow';
import FormField from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
@@ -210,4 +209,4 @@ OrganizationForm.contextTypes = {
};
export { OrganizationForm as _OrganizationForm };
export default withI18n()(withNetwork(withRouter(OrganizationForm)));
export default withI18n()(withRouter(OrganizationForm));

View File

@@ -3,9 +3,8 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Switch, Route, withRouter, Redirect } from 'react-router-dom';
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
import { withNetwork } from '../../../../contexts/Network';
import NotifyAndRedirect from '../../../../components/NotifyAndRedirect';
import CardCloseButton from '../../../../components/CardCloseButton';
import ContentError from '../../../../components/ContentError';
import OrganizationAccess from './OrganizationAccess';
import OrganizationDetail from './OrganizationDetail';
import OrganizationEdit from './OrganizationEdit';
@@ -20,77 +19,74 @@ class Organization extends Component {
this.state = {
organization: null,
error: false,
loading: true,
contentLoading: true,
contentError: false,
isInitialized: false,
isNotifAdmin: false,
isAuditorOfThisOrg: false,
isAdminOfThisOrg: false
isAdminOfThisOrg: false,
};
this.fetchOrganization = this.fetchOrganization.bind(this);
this.fetchOrganizationAndRoles = this.fetchOrganizationAndRoles.bind(this);
this.loadOrganization = this.loadOrganization.bind(this);
this.loadOrganizationAndRoles = this.loadOrganizationAndRoles.bind(this);
}
componentDidMount () {
this.fetchOrganizationAndRoles();
async componentDidMount () {
await this.loadOrganizationAndRoles();
this.setState({ isInitialized: true });
}
async componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
await this.fetchOrganization();
await this.loadOrganization();
}
}
async fetchOrganizationAndRoles () {
async loadOrganizationAndRoles () {
const {
match,
setBreadcrumb,
handleHttpError
} = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: false, contentLoading: true });
try {
const [{ data }, notifAdminRest, auditorRes, adminRes] = await Promise.all([
OrganizationsAPI.readDetail(parseInt(match.params.id, 10)),
OrganizationsAPI.read({
role_level: 'notification_admin_role',
page_size: 1
}),
OrganizationsAPI.read({
role_level: 'auditor_role',
id: parseInt(match.params.id, 10)
}),
OrganizationsAPI.read({
role_level: 'admin_role',
id: parseInt(match.params.id, 10)
})
const [{ data }, notifAdminRes, auditorRes, adminRes] = await Promise.all([
OrganizationsAPI.readDetail(id),
OrganizationsAPI.read({ page_size: 1, role_level: 'notification_admin_role' }),
OrganizationsAPI.read({ id, role_level: 'auditor_role' }),
OrganizationsAPI.read({ id, role_level: 'admin_role' }),
]);
setBreadcrumb(data);
this.setState({
organization: data,
loading: false,
isNotifAdmin: notifAdminRest.data.results.length > 0,
isNotifAdmin: notifAdminRes.data.results.length > 0,
isAuditorOfThisOrg: auditorRes.data.results.length > 0,
isAdminOfThisOrg: adminRes.data.results.length > 0
});
} catch (error) {
handleHttpError(error) || this.setState({ error: true, loading: false });
} catch (err) {
this.setState(({ contentError: true }));
} finally {
this.setState({ contentLoading: false });
}
}
async fetchOrganization () {
async loadOrganization () {
const {
match,
setBreadcrumb,
handleHttpError
} = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: false, contentLoading: true });
try {
const { data } = await OrganizationsAPI.readDetail(parseInt(match.params.id, 10));
const { data } = await OrganizationsAPI.readDetail(id);
setBreadcrumb(data);
this.setState({ organization: data, loading: false });
} catch (error) {
handleHttpError(error) || this.setState({ error: true, loading: false });
this.setState({ organization: data });
} catch (err) {
this.setState(({ contentError: true }));
} finally {
this.setState({ contentLoading: false });
}
}
@@ -105,8 +101,9 @@ class Organization extends Component {
const {
organization,
error,
loading,
contentError,
contentLoading,
isInitialized,
isNotifAdmin,
isAuditorOfThisOrg,
isAdminOfThisOrg
@@ -134,25 +131,28 @@ class Organization extends Component {
}
let cardHeader = (
loading ? '' : (
<CardHeader style={{ padding: 0 }}>
<React.Fragment>
<div className="awx-orgTabs-container">
<RoutedTabs
match={match}
history={history}
labeltext={i18n._(t`Organization detail tabs`)}
tabsArray={tabsArray}
/>
<CardCloseButton linkTo="/organizations" />
<div
className="awx-orgTabs__bottom-border"
/>
</div>
</React.Fragment>
</CardHeader>
)
<CardHeader style={{ padding: 0 }}>
<React.Fragment>
<div className="awx-orgTabs-container">
<RoutedTabs
match={match}
history={history}
labeltext={i18n._(t`Organization detail tabs`)}
tabsArray={tabsArray}
/>
<CardCloseButton linkTo="/organizations" />
<div
className="awx-orgTabs__bottom-border"
/>
</div>
</React.Fragment>
</CardHeader>
);
if (!isInitialized) {
cardHeader = null;
}
if (!match) {
cardHeader = null;
}
@@ -161,10 +161,20 @@ class Organization extends Component {
cardHeader = null;
}
if (!contentLoading && contentError) {
return (
<PageSection>
<Card className="awx-c-card">
<ContentError />
</Card>
</PageSection>
);
}
return (
<PageSection>
<Card className="awx-c-card">
{ cardHeader }
{cardHeader}
<Switch>
<Redirect
from="/organizations/:id"
@@ -220,18 +230,12 @@ class Organization extends Component {
)}
/>
)}
{organization && (
<NotifyAndRedirect
to={`/organizations/${match.params.id}/details`}
/>
)}
</Switch>
{error ? 'error!' : ''}
{loading ? 'loading...' : ''}
</Card>
</PageSection>
);
}
}
export default withI18n()(withNetwork(withRouter(Organization)));
export default withI18n()(withRouter(Organization));
export { Organization as _Organization };

View File

@@ -2,13 +2,17 @@ import React, { Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import AlertModal from '../../../../components/AlertModal';
import PaginatedDataList, { ToolbarAddButton } from '../../../../components/PaginatedDataList';
import DataListToolbar from '../../../../components/DataListToolbar';
import OrganizationAccessItem from '../../components/OrganizationAccessItem';
import DeleteRoleConfirmationModal from '../../components/DeleteRoleConfirmationModal';
import AddResourceRole from '../../../../components/AddRole/AddResourceRole';
import { withNetwork } from '../../../../contexts/Network';
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
import {
getQSConfig,
encodeQueryString,
parseNamespacedQueryString
} from '../../../../util/qs';
import { Organization } from '../../../../types';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../api';
@@ -25,183 +29,191 @@ class OrganizationAccess extends React.Component {
constructor (props) {
super(props);
this.readOrgAccessList = this.readOrgAccessList.bind(this);
this.confirmRemoveRole = this.confirmRemoveRole.bind(this);
this.cancelRemoveRole = this.cancelRemoveRole.bind(this);
this.removeRole = this.removeRole.bind(this);
this.toggleAddModal = this.toggleAddModal.bind(this);
this.handleSuccessfulRoleAdd = this.handleSuccessfulRoleAdd.bind(this);
this.state = {
isLoading: false,
isInitialized: false,
isAddModalOpen: false,
error: null,
itemCount: 0,
accessRecords: [],
roleToDelete: null,
roleToDeleteAccessRecord: null,
contentError: false,
contentLoading: true,
deletionError: false,
deletionRecord: null,
deletionRole: null,
isAddModalOpen: false,
itemCount: 0,
};
this.loadAccessList = this.loadAccessList.bind(this);
this.handleAddClose = this.handleAddClose.bind(this);
this.handleAddOpen = this.handleAddOpen.bind(this);
this.handleAddSuccess = this.handleAddSuccess.bind(this);
this.handleDeleteCancel = this.handleDeleteCancel.bind(this);
this.handleDeleteConfirm = this.handleDeleteConfirm.bind(this);
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
}
componentDidMount () {
this.readOrgAccessList();
this.loadAccessList();
}
componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.readOrgAccessList();
const prevParams = parseNamespacedQueryString(QS_CONFIG, prevProps.location.search);
const currentParams = parseNamespacedQueryString(QS_CONFIG, location.search);
if (encodeQueryString(currentParams) !== encodeQueryString(prevParams)) {
this.loadAccessList();
}
}
async readOrgAccessList () {
const { organization, handleHttpError, location } = this.props;
this.setState({ isLoading: true });
async loadAccessList () {
const { organization, location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ contentError: false, contentLoading: true });
try {
const { data } = await OrganizationsAPI.readAccessList(
organization.id,
parseNamespacedQueryString(QS_CONFIG, location.search)
);
this.setState({
itemCount: data.count || 0,
accessRecords: data.results || [],
isLoading: false,
isInitialized: true,
});
const {
data: {
results: accessRecords = [],
count: itemCount = 0
}
} = await OrganizationsAPI.readAccessList(organization.id, params);
this.setState({ itemCount, accessRecords });
} catch (error) {
handleHttpError(error) || this.setState({
error,
isLoading: false,
});
this.setState({ contentError: true });
} finally {
this.setState({ contentLoading: false });
}
}
confirmRemoveRole (role, accessRecord) {
handleDeleteOpen (deletionRole, deletionRecord) {
this.setState({ deletionRole, deletionRecord });
}
handleDeleteCancel () {
this.setState({ deletionRole: null, deletionRecord: null });
}
handleDeleteErrorClose () {
this.setState({
roleToDelete: role,
roleToDeleteAccessRecord: accessRecord,
deletionError: false,
deletionRecord: null,
deletionRole: null
});
}
cancelRemoveRole () {
this.setState({
roleToDelete: null,
roleToDeleteAccessRecord: null
});
}
async handleDeleteConfirm () {
const { deletionRole, deletionRecord } = this.state;
async removeRole () {
const { handleHttpError } = this.props;
const { roleToDelete: role, roleToDeleteAccessRecord: accessRecord } = this.state;
if (!role || !accessRecord) {
if (!deletionRole || !deletionRecord) {
return;
}
const type = typeof role.team_id === 'undefined' ? 'users' : 'teams';
this.setState({ isLoading: true });
let promise;
if (typeof deletionRole.team_id !== 'undefined') {
promise = TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
} else {
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
}
this.setState({ contentLoading: true });
try {
if (type === 'teams') {
await TeamsAPI.disassociateRole(role.team_id, role.id);
} else {
await UsersAPI.disassociateRole(accessRecord.id, role.id);
}
await promise.then(this.loadAccessList);
this.setState({
isLoading: false,
roleToDelete: null,
roleToDeleteAccessRecord: null,
deletionRole: null,
deletionRecord: null
});
this.readOrgAccessList();
} catch (error) {
handleHttpError(error) || this.setState({
error,
isLoading: false,
this.setState({
contentLoading: false,
deletionError: true
});
}
}
toggleAddModal () {
const { isAddModalOpen } = this.state;
this.setState({
isAddModalOpen: !isAddModalOpen,
});
handleAddClose () {
this.setState({ isAddModalOpen: false });
}
handleSuccessfulRoleAdd () {
this.toggleAddModal();
this.readOrgAccessList();
handleAddOpen () {
this.setState({ isAddModalOpen: true });
}
handleAddSuccess () {
this.setState({ isAddModalOpen: false });
this.loadAccessList();
}
render () {
const { organization, i18n } = this.props;
const {
isLoading,
isInitialized,
accessRecords,
contentError,
contentLoading,
deletionRole,
deletionRecord,
deletionError,
itemCount,
isAddModalOpen,
accessRecords,
roleToDelete,
roleToDeleteAccessRecord,
error,
} = this.state;
const canEdit = organization.summary_fields.user_capabilities.edit;
const isDeleteModalOpen = !contentLoading && !deletionError && deletionRole;
if (error) {
// TODO: better error state
return <div>{error.message}</div>;
}
// TODO: better loading state
return (
<Fragment>
{isLoading && (<div>Loading...</div>)}
{roleToDelete && (
<DeleteRoleConfirmationModal
role={roleToDelete}
username={roleToDeleteAccessRecord.username}
onCancel={this.cancelRemoveRole}
onConfirm={this.removeRole}
/>
)}
{isInitialized && (
<PaginatedDataList
items={accessRecords}
itemCount={itemCount}
itemName="role"
qsConfig={QS_CONFIG}
toolbarColumns={[
{ name: i18n._(t`Name`), key: 'first_name', isSortable: true },
{ name: i18n._(t`Username`), key: 'username', isSortable: true },
{ name: i18n._(t`Last Name`), key: 'last_name', isSortable: true },
]}
renderToolbar={(props) => (
<DataListToolbar
{...props}
additionalControls={canEdit ? [
<ToolbarAddButton key="add" onClick={this.toggleAddModal} />
] : null}
/>
)}
renderItem={accessRecord => (
<OrganizationAccessItem
key={accessRecord.id}
accessRecord={accessRecord}
onRoleDelete={this.confirmRemoveRole}
/>
)}
/>
)}
<PaginatedDataList
contentError={contentError}
contentLoading={contentLoading}
items={accessRecords}
itemCount={itemCount}
itemName="role"
qsConfig={QS_CONFIG}
toolbarColumns={[
{ name: i18n._(t`Name`), key: 'first_name', isSortable: true },
{ name: i18n._(t`Username`), key: 'username', isSortable: true },
{ name: i18n._(t`Last Name`), key: 'last_name', isSortable: true },
]}
renderToolbar={(props) => (
<DataListToolbar
{...props}
additionalControls={canEdit ? [
<ToolbarAddButton key="add" onClick={this.handleAddOpen} />
] : null}
/>
)}
renderItem={accessRecord => (
<OrganizationAccessItem
key={accessRecord.id}
accessRecord={accessRecord}
onRoleDelete={this.handleDeleteOpen}
/>
)}
/>
{isAddModalOpen && (
<AddResourceRole
onClose={this.toggleAddModal}
onSave={this.handleSuccessfulRoleAdd}
onClose={this.handleAddClose}
onSave={this.handleAddSuccess}
roles={organization.summary_fields.object_roles}
/>
)}
{isDeleteModalOpen && (
<DeleteRoleConfirmationModal
role={deletionRole}
username={deletionRecord.username}
onCancel={this.handleDeleteCancel}
onConfirm={this.handleDeleteConfirm}
/>
)}
<AlertModal
isOpen={deletionError}
variant="danger"
title={i18n._(t`Error!`)}
onClose={this.handleDeleteErrorClose}
>
{i18n._(t`Failed to delete role`)}
</AlertModal>
</Fragment>
);
}
}
export { OrganizationAccess as _OrganizationAccess };
export default withI18n()(withNetwork(withRouter(OrganizationAccess)));
export default withI18n()(withRouter(OrganizationAccess));

View File

@@ -4,9 +4,11 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { CardBody as PFCardBody, Button } from '@patternfly/react-core';
import styled from 'styled-components';
import { DetailList, Detail } from '../../../../components/DetailList';
import { withNetwork } from '../../../../contexts/Network';
import { ChipGroup, Chip } from '../../../../components/Chip';
import ContentError from '../../../../components/ContentError';
import ContentLoading from '../../../../components/ContentLoading';
import { OrganizationsAPI } from '../../../../api';
const CardBody = styled(PFCardBody)`
@@ -18,8 +20,9 @@ class OrganizationDetail extends Component {
super(props);
this.state = {
contentError: false,
contentLoading: true,
instanceGroups: [],
error: false
};
this.loadInstanceGroups = this.loadInstanceGroups.bind(this);
}
@@ -29,25 +32,23 @@ class OrganizationDetail extends Component {
}
async loadInstanceGroups () {
const {
handleHttpError,
match
} = this.props;
const { match: { params: { id } } } = this.props;
this.setState({ contentLoading: true });
try {
const {
data
} = await OrganizationsAPI.readInstanceGroups(match.params.id);
this.setState({
instanceGroups: [...data.results]
});
const { data: { results = [] } } = await OrganizationsAPI.readInstanceGroups(id);
this.setState({ instanceGroups: [...results] });
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
this.setState({ contentError: true });
} finally {
this.setState({ contentLoading: false });
}
}
render () {
const {
error,
contentLoading,
contentError,
instanceGroups,
} = this.state;
@@ -65,6 +66,14 @@ class OrganizationDetail extends Component {
i18n
} = this.props;
if (contentLoading) {
return (<ContentLoading />);
}
if (contentError) {
return (<ContentError />);
}
return (
<CardBody>
<DetailList>
@@ -116,10 +125,9 @@ class OrganizationDetail extends Component {
</Button>
</div>
)}
{error ? 'error!' : ''}
</CardBody>
);
}
}
export default withI18n()(withRouter(withNetwork(OrganizationDetail)));
export default withI18n()(withRouter(OrganizationDetail));

View File

@@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom';
import { CardBody } from '@patternfly/react-core';
import OrganizationForm from '../../components/OrganizationForm';
import { Config } from '../../../../contexts/Config';
import { withNetwork } from '../../../../contexts/Network';
import { OrganizationsAPI } from '../../../../api';
class OrganizationEdit extends Component {
@@ -22,13 +22,13 @@ class OrganizationEdit extends Component {
}
async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
const { organization, handleHttpError } = this.props;
const { organization } = this.props;
try {
await OrganizationsAPI.update(organization.id, values);
await this.submitInstanceGroups(groupsToAssociate, groupsToDisassociate);
this.handleSuccess();
} catch (err) {
handleHttpError(err) || this.setState({ error: err });
this.setState({ error: err });
}
}
@@ -43,8 +43,7 @@ class OrganizationEdit extends Component {
}
async submitInstanceGroups (groupsToAssociate, groupsToDisassociate) {
const { organization, handleHttpError } = this.props;
const { organization } = this.props;
try {
await Promise.all(
groupsToAssociate.map(id => OrganizationsAPI.associateInstanceGroup(organization.id, id))
@@ -55,7 +54,7 @@ class OrganizationEdit extends Component {
)
);
} catch (err) {
handleHttpError(err) || this.setState({ error: err });
this.setState({ error: err });
}
}
@@ -90,4 +89,4 @@ OrganizationEdit.contextTypes = {
};
export { OrganizationEdit as _OrganizationEdit };
export default withNetwork(withRouter(OrganizationEdit));
export default withRouter(OrganizationEdit);

View File

@@ -1,7 +1,10 @@
import React, { Component, Fragment } from 'react';
import { number, shape, func, string, bool } from 'prop-types';
import { number, shape, string, bool } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withNetwork } from '../../../../contexts/Network';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import AlertModal from '../../../../components/AlertModal';
import PaginatedDataList from '../../../../components/PaginatedDataList';
import NotificationListItem from '../../../../components/NotificationsList/NotificationListItem';
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
@@ -22,194 +25,159 @@ const COLUMNS = [
class OrganizationNotifications extends Component {
constructor (props) {
super(props);
this.readNotifications = this.readNotifications.bind(this);
this.readSuccessesAndErrors = this.readSuccessesAndErrors.bind(this);
this.toggleNotification = this.toggleNotification.bind(this);
this.state = {
isInitialized: false,
isLoading: false,
error: null,
contentError: false,
contentLoading: true,
toggleError: false,
toggleLoading: false,
itemCount: 0,
notifications: [],
successTemplateIds: [],
errorTemplateIds: [],
};
this.handleNotificationToggle = this.handleNotificationToggle.bind(this);
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(this);
this.loadNotifications = this.loadNotifications.bind(this);
}
componentDidMount () {
this.readNotifications();
this.loadNotifications();
}
componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.readNotifications();
this.loadNotifications();
}
}
async readNotifications () {
const { id, handleHttpError, location } = this.props;
async loadNotifications () {
const { id, location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ isLoading: true });
try {
const { data } = await OrganizationsAPI.readNotificationTemplates(id, params);
this.setState(
{
itemCount: data.count || 0,
notifications: data.results || [],
isLoading: false,
isInitialized: true,
},
this.readSuccessesAndErrors
);
} catch (error) {
handleHttpError(error) || this.setState({
error,
isLoading: false,
});
}
}
async readSuccessesAndErrors () {
const { handleHttpError, id } = this.props;
const { notifications } = this.state;
if (!notifications.length) {
return;
}
const ids = notifications.map(n => n.id).join(',');
this.setState({ contentError: false, contentLoading: true });
try {
const successTemplatesPromise = OrganizationsAPI.readNotificationTemplatesSuccess(
id,
{ id__in: ids }
);
const errorTemplatesPromise = OrganizationsAPI.readNotificationTemplatesError(
id,
{ id__in: ids }
);
const {
data: {
count: itemCount = 0,
results: notifications = [],
}
} = await OrganizationsAPI.readNotificationTemplates(id, params);
const { data: successTemplates } = await successTemplatesPromise;
const { data: errorTemplates } = await errorTemplatesPromise;
let idMatchParams;
if (notifications.length > 0) {
idMatchParams = { id__in: notifications.map(n => n.id).join(',') };
} else {
idMatchParams = {};
}
const [
{ data: successTemplates },
{ data: errorTemplates },
] = await Promise.all([
OrganizationsAPI.readNotificationTemplatesSuccess(id, idMatchParams),
OrganizationsAPI.readNotificationTemplatesError(id, idMatchParams),
]);
this.setState({
itemCount,
notifications,
successTemplateIds: successTemplates.results.map(s => s.id),
errorTemplateIds: errorTemplates.results.map(e => e.id),
});
} catch (error) {
handleHttpError(error) || this.setState({
error,
isLoading: false,
} catch {
this.setState({ contentError: true });
} finally {
this.setState({ contentLoading: false });
}
}
async handleNotificationToggle (notificationId, isCurrentlyOn, status) {
const { id } = this.props;
let stateArrayName;
if (status === 'success') {
stateArrayName = 'successTemplateIds';
} else {
stateArrayName = 'errorTemplateIds';
}
let stateUpdateFunction;
if (isCurrentlyOn) {
// when switching off, remove the toggled notification id from the array
stateUpdateFunction = (prevState) => ({
[stateArrayName]: prevState[stateArrayName].filter(i => i !== notificationId)
});
} else {
// when switching on, add the toggled notification id to the array
stateUpdateFunction = (prevState) => ({
[stateArrayName]: prevState[stateArrayName].concat(notificationId)
});
}
}
toggleNotification = (notificationId, isCurrentlyOn, status) => {
if (status === 'success') {
if (isCurrentlyOn) {
this.disassociateSuccess(notificationId);
} else {
this.associateSuccess(notificationId);
}
} else if (status === 'error') {
if (isCurrentlyOn) {
this.disassociateError(notificationId);
} else {
this.associateError(notificationId);
}
}
};
async associateSuccess (notificationId) {
const { id, handleHttpError } = this.props;
this.setState({ toggleLoading: true });
try {
await OrganizationsAPI.associateNotificationTemplatesSuccess(id, notificationId);
this.setState(prevState => ({
successTemplateIds: [...prevState.successTemplateIds, notificationId]
}));
await OrganizationsAPI.updateNotificationTemplateAssociation(
id,
notificationId,
status,
!isCurrentlyOn
);
this.setState(stateUpdateFunction);
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
this.setState({ toggleError: true });
} finally {
this.setState({ toggleLoading: false });
}
}
async disassociateSuccess (notificationId) {
const { id, handleHttpError } = this.props;
try {
await OrganizationsAPI.disassociateNotificationTemplatesSuccess(id, notificationId);
this.setState((prevState) => ({
successTemplateIds: prevState.successTemplateIds
.filter((templateId) => templateId !== notificationId)
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
}
async associateError (notificationId) {
const { id, handleHttpError } = this.props;
try {
await OrganizationsAPI.associateNotificationTemplatesError(id, notificationId);
this.setState(prevState => ({
errorTemplateIds: [...prevState.errorTemplateIds, notificationId]
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
}
async disassociateError (notificationId) {
const { id, handleHttpError } = this.props;
try {
await OrganizationsAPI.disassociateNotificationTemplatesError(id, notificationId);
this.setState((prevState) => ({
errorTemplateIds: prevState.errorTemplateIds
.filter((templateId) => templateId !== notificationId)
}));
} catch (err) {
handleHttpError(err) || this.setState({ error: true });
}
handleNotificationErrorClose () {
this.setState({ toggleError: false });
}
render () {
const { canToggleNotifications } = this.props;
const { canToggleNotifications, i18n } = this.props;
const {
notifications,
contentError,
contentLoading,
toggleError,
toggleLoading,
itemCount,
isLoading,
isInitialized,
error,
notifications,
successTemplateIds,
errorTemplateIds,
} = this.state;
if (error) {
// TODO: better error state
return <div>{error.message}</div>;
}
// TODO: better loading state
return (
<Fragment>
{isLoading && (<div>Loading...</div>)}
{isInitialized && (
<PaginatedDataList
items={notifications}
itemCount={itemCount}
itemName="notification"
qsConfig={QS_CONFIG}
toolbarColumns={COLUMNS}
renderItem={(notification) => (
<NotificationListItem
key={notification.id}
notification={notification}
detailUrl={`/notifications/${notification.id}`}
canToggleNotifications={canToggleNotifications}
toggleNotification={this.toggleNotification}
errorTurnedOn={errorTemplateIds.includes(notification.id)}
successTurnedOn={successTemplateIds.includes(notification.id)}
/>
)}
/>
)}
<PaginatedDataList
contentError={contentError}
contentLoading={contentLoading}
items={notifications}
itemCount={itemCount}
itemName="notification"
qsConfig={QS_CONFIG}
toolbarColumns={COLUMNS}
renderItem={(notification) => (
<NotificationListItem
key={notification.id}
notification={notification}
detailUrl={`/notifications/${notification.id}`}
canToggleNotifications={canToggleNotifications && !toggleLoading}
toggleNotification={this.handleNotificationToggle}
errorTurnedOn={errorTemplateIds.includes(notification.id)}
successTurnedOn={successTemplateIds.includes(notification.id)}
/>
)}
/>
<AlertModal
variant="danger"
title={i18n._(t`Error!`)}
isOpen={toggleError && !toggleLoading}
onClose={this.handleNotificationErrorClose}
>
{i18n._(t`Failed to toggle notification.`)}
</AlertModal>
</Fragment>
);
}
@@ -218,11 +186,10 @@ class OrganizationNotifications extends Component {
OrganizationNotifications.propTypes = {
id: number.isRequired,
canToggleNotifications: bool.isRequired,
handleHttpError: func.isRequired,
location: shape({
search: string.isRequired,
}).isRequired,
};
export { OrganizationNotifications as _OrganizationNotifications };
export default withNetwork(withRouter(OrganizationNotifications));
export default withI18n()(withRouter(OrganizationNotifications));

View File

@@ -1,9 +1,8 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import PaginatedDataList from '../../../../components/PaginatedDataList';
import { getQSConfig, parseNamespacedQueryString } from '../../../../util/qs';
import { withNetwork } from '../../../../contexts/Network';
import { OrganizationsAPI } from '../../../../api';
const QS_CONFIG = getQSConfig('team', {
@@ -16,32 +15,32 @@ class OrganizationTeams extends React.Component {
constructor (props) {
super(props);
this.readOrganizationTeamsList = this.readOrganizationTeamsList.bind(this);
this.loadOrganizationTeamsList = this.loadOrganizationTeamsList.bind(this);
this.state = {
isInitialized: false,
isLoading: false,
error: null,
contentError: false,
contentLoading: true,
itemCount: 0,
teams: [],
};
}
componentDidMount () {
this.readOrganizationTeamsList();
this.loadOrganizationTeamsList();
}
componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.readOrganizationTeamsList();
this.loadOrganizationTeamsList();
}
}
async readOrganizationTeamsList () {
const { id, handleHttpError, location } = this.props;
async loadOrganizationTeamsList () {
const { id, location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ isLoading: true, error: null });
this.setState({ contentLoading: true, contentError: false });
try {
const {
data: { count = 0, results = [] },
@@ -49,38 +48,25 @@ class OrganizationTeams extends React.Component {
this.setState({
itemCount: count,
teams: results,
isLoading: false,
isInitialized: true,
});
} catch (error) {
handleHttpError(error) || this.setState({
error,
isLoading: false,
});
} catch {
this.setState({ contentError: true });
} finally {
this.setState({ contentLoading: false });
}
}
render () {
const { teams, itemCount, isLoading, isInitialized, error } = this.state;
if (error) {
// TODO: better error state
return <div>{error.message}</div>;
}
// TODO: better loading state
const { contentError, contentLoading, teams, itemCount } = this.state;
return (
<Fragment>
{isLoading && (<div>Loading...</div>)}
{isInitialized && (
<PaginatedDataList
items={teams}
itemCount={itemCount}
itemName="team"
qsConfig={QS_CONFIG}
/>
)}
</Fragment>
<PaginatedDataList
contentError={contentError}
contentLoading={contentLoading}
items={teams}
itemCount={itemCount}
itemName="team"
qsConfig={QS_CONFIG}
/>
);
}
}
@@ -90,4 +76,4 @@ OrganizationTeams.propTypes = {
};
export { OrganizationTeams as _OrganizationTeams };
export default withNetwork(withRouter(OrganizationTeams));
export default withRouter(OrganizationTeams);

View File

@@ -12,7 +12,6 @@ import {
} from '@patternfly/react-core';
import { Config } from '../../../contexts/Config';
import { withNetwork } from '../../../contexts/Network';
import CardCloseButton from '../../../components/CardCloseButton';
import OrganizationForm from '../components/OrganizationForm';
import { OrganizationsAPI } from '../../../api';
@@ -20,29 +19,20 @@ import { OrganizationsAPI } from '../../../api';
class OrganizationAdd extends React.Component {
constructor (props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSuccess = this.handleSuccess.bind(this);
this.state = {
error: '',
};
this.state = { error: '' };
}
async handleSubmit (values, groupsToAssociate) {
const { handleHttpError } = this.props;
const { history } = this.props;
try {
const { data: response } = await OrganizationsAPI.create(values);
try {
await Promise.all(groupsToAssociate.map(id => OrganizationsAPI
.associateInstanceGroup(response.id, id)));
this.handleSuccess(response.id);
} catch (err) {
handleHttpError(err) || this.setState({ error: err });
}
} catch (err) {
this.setState({ error: err });
await Promise.all(groupsToAssociate.map(id => OrganizationsAPI
.associateInstanceGroup(response.id, id)));
history.push(`/organizations/${response.id}`);
} catch (error) {
this.setState({ error });
}
}
@@ -51,11 +41,6 @@ class OrganizationAdd extends React.Component {
history.push('/organizations');
}
handleSuccess (id) {
const { history } = this.props;
history.push(`/organizations/${id}`);
}
render () {
const { error } = this.state;
const { i18n } = this.props;
@@ -94,4 +79,4 @@ OrganizationAdd.contextTypes = {
};
export { OrganizationAdd as _OrganizationAdd };
export default withI18n()(withNetwork(withRouter(OrganizationAdd)));
export default withI18n()(withRouter(OrganizationAdd));

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -8,13 +8,13 @@ import {
PageSectionVariants,
} from '@patternfly/react-core';
import { withNetwork } from '../../../contexts/Network';
import PaginatedDataList, {
ToolbarDeleteButton,
ToolbarAddButton
} from '../../../components/PaginatedDataList';
import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem';
import AlertModal from '../../../components/AlertModal';
import { getQSConfig, parseNamespacedQueryString } from '../../../util/qs';
import { OrganizationsAPI } from '../../../api';
@@ -29,29 +29,30 @@ class OrganizationsList extends Component {
super(props);
this.state = {
error: null,
isLoading: true,
isInitialized: false,
contentLoading: true,
contentError: false,
deletionError: false,
organizations: [],
selected: []
selected: [],
itemCount: 0,
actions: null,
};
this.handleSelectAll = this.handleSelectAll.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.fetchOptionsOrganizations = this.fetchOptionsOrganizations.bind(this);
this.fetchOrganizations = this.fetchOrganizations.bind(this);
this.handleOrgDelete = this.handleOrgDelete.bind(this);
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
this.loadOrganizations = this.loadOrganizations.bind(this);
}
componentDidMount () {
this.fetchOptionsOrganizations();
this.fetchOrganizations();
this.loadOrganizations();
}
componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.fetchOrganizations();
this.loadOrganizations();
}
}
@@ -72,63 +73,54 @@ class OrganizationsList extends Component {
}
}
handleDeleteErrorClose () {
this.setState({ deletionError: false });
}
async handleOrgDelete () {
const { selected } = this.state;
const { handleHttpError } = this.props;
let errorHandled;
this.setState({ contentLoading: true, deletionError: false });
try {
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
this.setState({
selected: []
});
this.setState({ selected: [] });
} catch (err) {
errorHandled = handleHttpError(err);
this.setState({ deletionError: true });
} finally {
if (!errorHandled) {
this.fetchOrganizations();
}
await this.loadOrganizations();
}
}
async fetchOrganizations () {
const { handleHttpError, location } = this.props;
async loadOrganizations () {
const { location } = this.props;
const { actions: cachedActions } = this.state;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
this.setState({ error: false, isLoading: true });
let optionsPromise;
if (cachedActions) {
optionsPromise = Promise.resolve({ data: { actions: cachedActions } });
} else {
optionsPromise = OrganizationsAPI.readOptions();
}
const promises = Promise.all([
OrganizationsAPI.read(params),
optionsPromise,
]);
this.setState({ contentError: false, contentLoading: true });
try {
const { data } = await OrganizationsAPI.read(params);
const { count, results } = data;
const stateToUpdate = {
const [{ data: { count, results } }, { data: { actions } }] = await promises;
this.setState({
actions,
itemCount: count,
organizations: results,
selected: [],
isLoading: false,
isInitialized: true,
};
this.setState(stateToUpdate);
});
} catch (err) {
handleHttpError(err) || this.setState({ error: true, isLoading: false });
}
}
async fetchOptionsOrganizations () {
try {
const { data } = await OrganizationsAPI.readOptions();
const { actions } = data;
const stateToUpdate = {
canAdd: Object.prototype.hasOwnProperty.call(actions, 'POST')
};
this.setState(stateToUpdate);
} catch (err) {
this.setState({ error: true });
this.setState(({ contentError: true }));
} finally {
this.setState({ isLoading: false });
this.setState({ contentLoading: false });
}
}
@@ -137,23 +129,26 @@ class OrganizationsList extends Component {
medium,
} = PageSectionVariants;
const {
canAdd,
actions,
itemCount,
error,
isLoading,
isInitialized,
contentError,
contentLoading,
deletionError,
selected,
organizations
organizations,
} = this.state;
const { match, i18n } = this.props;
const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const isAllSelected = selected.length === organizations.length;
return (
<PageSection variant={medium}>
<Card>
{isInitialized && (
<Fragment>
<PageSection variant={medium}>
<Card>
<PaginatedDataList
contentError={contentError}
contentLoading={contentLoading}
items={organizations}
itemCount={itemCount}
itemName="organization"
@@ -196,14 +191,20 @@ class OrganizationsList extends Component {
: null
}
/>
)}
{ isLoading ? <div>loading...</div> : '' }
{ error ? <div>error</div> : '' }
</Card>
</PageSection>
</Card>
</PageSection>
<AlertModal
isOpen={deletionError}
variant="danger"
title={i18n._(t`Error!`)}
onClose={this.handleDeleteErrorClose}
>
{i18n._(t`Failed to delete one or more organizations.`)}
</AlertModal>
</Fragment>
);
}
}
export { OrganizationsList as _OrganizationsList };
export default withI18n()(withNetwork(withRouter(OrganizationsList)));
export default withI18n()(withRouter(OrganizationsList));