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