Merge pull request #93 from mabashian/63-org-notifications

Add notification list to org
This commit is contained in:
Michael Abashian
2019-01-24 09:21:13 -05:00
committed by GitHub
15 changed files with 946 additions and 74 deletions

View File

@@ -36,6 +36,7 @@ describe('<DataListToolbar />', () => {
onSearch={onSearch} onSearch={onSearch}
onSort={onSort} onSort={onSort}
onSelectAll={onSelectAll} onSelectAll={onSelectAll}
showSelectAll
/> />
</I18nProvider> </I18nProvider>
); );

View File

@@ -0,0 +1,160 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import Notifications from '../../src/components/NotificationsList/Notifications.list';
describe('<Notifications />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
/>
</I18nProvider>
</MemoryRouter>
);
});
test('fetches notifications on mount', () => {
const spy = jest.spyOn(Notifications.prototype, 'fetchNotifications');
mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
/>
</I18nProvider>
</MemoryRouter>
);
expect(spy).toHaveBeenCalled();
});
test('toggle success calls post', () => {
const spy = jest.spyOn(Notifications.prototype, 'postToSuccess');
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
/>
</I18nProvider>
</MemoryRouter>
).find('Notifications');
wrapper.instance().toggleNotification(1, true, 'success');
expect(spy).toHaveBeenCalledWith(1, true);
});
test('post success makes request and updates state properly', async () => {
const postSuccessFn = jest.fn();
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
postSuccess={postSuccessFn}
/>
</I18nProvider>
</MemoryRouter>
).find('Notifications');
wrapper.setState({ successTemplateIds: [44] });
await wrapper.instance().postToSuccess(44, true);
expect(postSuccessFn).toHaveBeenCalledWith(1, { id: 44, disassociate: true });
expect(wrapper.state('successTemplateIds')).not.toContain(44);
await wrapper.instance().postToSuccess(44, false);
expect(postSuccessFn).toHaveBeenCalledWith(1, { id: 44 });
expect(wrapper.state('successTemplateIds')).toContain(44);
});
test('toggle error calls post', () => {
const spy = jest.spyOn(Notifications.prototype, 'postToError');
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
/>
</I18nProvider>
</MemoryRouter>
).find('Notifications');
wrapper.instance().toggleNotification(1, true, 'error');
expect(spy).toHaveBeenCalledWith(1, true);
});
test('post error makes request and updates state properly', async () => {
const postErrorFn = jest.fn();
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
postError={postErrorFn}
/>
</I18nProvider>
</MemoryRouter>
).find('Notifications');
wrapper.setState({ errorTemplateIds: [44] });
await wrapper.instance().postToError(44, true);
expect(postErrorFn).toHaveBeenCalledWith(1, { id: 44, disassociate: true });
expect(wrapper.state('errorTemplateIds')).not.toContain(44);
await wrapper.instance().postToError(44, false);
expect(postErrorFn).toHaveBeenCalledWith(1, { id: 44 });
expect(wrapper.state('errorTemplateIds')).toContain(44);
});
test('fetchNotifications', async () => {
const mockQueryParams = {
page: 44,
page_size: 10,
order_by: 'name'
};
const getNotificationsFn = jest.fn().mockResolvedValue({
data: {
results: [
{ id: 1 },
{ id: 2 },
{ id: 3 }
]
}
});
const getSuccessFn = jest.fn().mockResolvedValue({
data: {
results: [
{ id: 1 }
]
}
});
const getErrorFn = jest.fn().mockResolvedValue({
data: {
results: [
{ id: 2 }
]
}
});
const wrapper = mount(
<MemoryRouter>
<I18nProvider>
<Notifications
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
getNotifications={getNotificationsFn}
getSuccess={getSuccessFn}
getError={getErrorFn}
/>
</I18nProvider>
</MemoryRouter>
).find('Notifications');
wrapper.instance().updateUrl = jest.fn();
await wrapper.instance().fetchNotifications(mockQueryParams);
expect(getNotificationsFn).toHaveBeenCalledWith(1, mockQueryParams);
expect(getSuccessFn).toHaveBeenCalledWith(1, {
id__in: '1,2,3'
});
expect(getErrorFn).toHaveBeenCalledWith(1, {
id__in: '1,2,3'
});
expect(wrapper.state('successTemplateIds')).toContain(1);
expect(wrapper.state('errorTemplateIds')).toContain(2);
});
});

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import NotificationListItem from '../../src/components/NotificationsList/NotificationListItem';
describe('<NotificationListItem />', () => {
let wrapper;
afterEach(() => {
if (wrapper) {
wrapper.unmount();
wrapper = null;
}
});
test('initially renders succesfully', () => {
wrapper = mount(
<I18nProvider>
<MemoryRouter>
<NotificationListItem />
</MemoryRouter>
</I18nProvider>
);
expect(wrapper.length).toBe(1);
});
test('handles success click when toggle is on', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
<NotificationListItem
itemId={9000}
successTurnedOn
toggleNotification={toggleNotification}
/>
</MemoryRouter>
</I18nProvider>
);
wrapper.find('Switch').first().find('input').simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
});
test('handles success click when toggle is off', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
<NotificationListItem
itemId={9000}
successTurnedOn={false}
toggleNotification={toggleNotification}
/>
</MemoryRouter>
</I18nProvider>
);
wrapper.find('Switch').first().find('input').simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'success');
});
test('handles error click when toggle is on', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
<NotificationListItem
itemId={9000}
errorTurnedOn
toggleNotification={toggleNotification}
/>
</MemoryRouter>
</I18nProvider>
);
wrapper.find('Switch').at(1).find('input').simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'error');
});
test('handles error click when toggle is off', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
<NotificationListItem
itemId={9000}
errorTurnedOn={false}
toggleNotification={toggleNotification}
/>
</MemoryRouter>
</I18nProvider>
);
wrapper.find('Switch').at(1).find('input').simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'error');
});
});

View File

@@ -12,6 +12,7 @@ describe('<OrganizationDetail />', () => {
<OrganizationDetail <OrganizationDetail
match={{ path: '/organizations/:id', url: '/organizations/1' }} match={{ path: '/organizations/:id', url: '/organizations/1' }}
location={{ search: '', pathname: '/organizations/1' }} location={{ search: '', pathname: '/organizations/1' }}
params={{}}
/> />
</MemoryRouter> </MemoryRouter>
</I18nProvider> </I18nProvider>

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationNotifications from '../../../../../src/pages/Organizations/screens/Organization/OrganizationNotifications';
describe('<OrganizationNotifications />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
<OrganizationNotifications
match={{ path: '/organizations/:id/notifications', url: '/organizations/1/notifications' }}
location={{ search: '', pathname: '/organizations/1/notifications' }}
params={{}}
api={{
getOrganizationNotifications: jest.fn(),
getOrganizationNotificationSuccess: jest.fn(),
getOrganizationNotificationError: jest.fn(),
createOrganizationNotificationSuccess: jest.fn(),
createOrganizationNotificationError: jest.fn()
}}
/>
</MemoryRouter>
);
});
test('handles api requests', () => {
const getOrganizationNotifications = jest.fn();
const getOrganizationNotificationSuccess = jest.fn();
const getOrganizationNotificationError = jest.fn();
const createOrganizationNotificationSuccess = jest.fn();
const createOrganizationNotificationError = jest.fn();
const wrapper = mount(
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
<OrganizationNotifications
match={{ path: '/organizations/:id/notifications', url: '/organizations/1/notifications' }}
location={{ search: '', pathname: '/organizations/1/notifications' }}
params={{}}
api={{
getOrganizationNotifications,
getOrganizationNotificationSuccess,
getOrganizationNotificationError,
createOrganizationNotificationSuccess,
createOrganizationNotificationError
}}
/>
</MemoryRouter>
).find('OrganizationNotifications');
wrapper.instance().getOrgNotifications(1, { foo: 'bar' });
expect(getOrganizationNotifications).toHaveBeenCalledWith(1, { foo: 'bar' });
wrapper.instance().getOrgNotificationSuccess(1, { foo: 'bar' });
expect(getOrganizationNotificationSuccess).toHaveBeenCalledWith(1, { foo: 'bar' });
wrapper.instance().getOrgNotificationError(1, { foo: 'bar' });
expect(getOrganizationNotificationError).toHaveBeenCalledWith(1, { foo: 'bar' });
wrapper.instance().createOrgNotificationSuccess(1, { id: 2 });
expect(createOrganizationNotificationSuccess).toHaveBeenCalledWith(1, { id: 2 });
wrapper.instance().createOrgNotificationError(1, { id: 2 });
expect(createOrganizationNotificationError).toHaveBeenCalledWith(1, { id: 2 });
});
});

View File

@@ -70,6 +70,36 @@ class APIClient {
return this.http.get(endpoint); return this.http.get(endpoint);
} }
getOrganizationNotifications (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates/`;
return this.http.get(endpoint, { params });
}
getOrganizationNotificationSuccess (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_success/`;
return this.http.get(endpoint, { params });
}
getOrganizationNotificationError (id, params = {}) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_error/`;
return this.http.get(endpoint, { params });
}
createOrganizationNotificationSuccess (id, data) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_success/`;
return this.http.post(endpoint, data);
}
createOrganizationNotificationError (id, data) {
const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates_error/`;
return this.http.post(endpoint, data);
}
getInstanceGroups () { getInstanceGroups () {
return this.http.get(API_INSTANCE_GROUPS); return this.http.get(API_INSTANCE_GROUPS);
} }

View File

@@ -82,6 +82,10 @@
--pf-c-data-list__item--PaddingTop: 16px; --pf-c-data-list__item--PaddingTop: 16px;
--pf-c-data-list__item--PaddingBottom: 16px; --pf-c-data-list__item--PaddingBottom: 16px;
.pf-c-badge:not(:last-child), .pf-c-switch:not(:last-child) {
margin-right: 18px;
}
} }
.pf-c-data-list__item { .pf-c-data-list__item {
@@ -107,10 +111,6 @@
margin-right: 8px; margin-right: 8px;
} }
.pf-c-data-list__cell span {
margin-right: 18px;
}
// //
// about modal overrides // about modal overrides
// //
@@ -158,6 +158,16 @@
border-top: 1px solid #d7d7d7; border-top: 1px solid #d7d7d7;
border-bottom: 1px solid #d7d7d7; border-bottom: 1px solid #d7d7d7;
} }
.at-c-listCardBody {
--pf-c-card__footer--PaddingX: 0;
--pf-c-card__footer--PaddingY: 0;
--pf-c-card__body--PaddingX: 0;
--pf-c-card__body--PaddingY: 0;
}
.pf-c-data-list__item {
--pf-c-data-list__item--PaddingLeft: 20px;
--pf-c-data-list__item--PaddingRight: 20px;
}
// //
// pf modal overrides // pf modal overrides
// //

View File

@@ -106,7 +106,9 @@ class DataListToolbar extends React.Component {
sortedColumnKey, sortedColumnKey,
sortOrder, sortOrder,
addUrl, addUrl,
showExpandCollapse showExpandCollapse,
showDelete,
showSelectAll
} = this.props; } = this.props;
const { const {
// isActionDropdownOpen, // isActionDropdownOpen,
@@ -149,19 +151,19 @@ class DataListToolbar extends React.Component {
<div className="awx-toolbar"> <div className="awx-toolbar">
<Level> <Level>
<LevelItem> <LevelItem>
<Toolbar <Toolbar style={{ marginLeft: '20px' }}>
style={{ marginLeft: '20px' }} { showSelectAll && (
> <ToolbarGroup>
<ToolbarGroup> <ToolbarItem>
<ToolbarItem> <Checkbox
<Checkbox checked={isAllSelected}
checked={isAllSelected} onChange={onSelectAll}
onChange={onSelectAll} aria-label={i18n._(t`Select all`)}
aria-label={i18n._(t`Select all`)} id="select-all"
id="select-all" />
/> </ToolbarItem>
</ToolbarItem> </ToolbarGroup>
</ToolbarGroup> )}
<ToolbarGroup> <ToolbarGroup>
<ToolbarItem> <ToolbarItem>
<div className="pf-c-input-group"> <div className="pf-c-input-group">
@@ -248,17 +250,19 @@ class DataListToolbar extends React.Component {
</Toolbar> </Toolbar>
</LevelItem> </LevelItem>
<LevelItem> <LevelItem>
<Tooltip { showDelete && (
message={i18n._(t`Delete`)} <Tooltip
position="top" message={i18n._(t`Delete`)}
> position="top"
<Button
variant="plain"
aria-label={i18n._(t`Delete`)}
> >
<TrashAltIcon /> <Button
</Button> variant="plain"
</Tooltip> aria-label={i18n._(t`Delete`)}
>
<TrashAltIcon />
</Button>
</Tooltip>
)}
{addUrl && ( {addUrl && (
<Link to={addUrl}> <Link to={addUrl}>
<Button <Button

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Link
} from 'react-router-dom';
import {
Badge,
Switch
} from '@patternfly/react-core';
class NotificationListItem extends React.Component {
render () {
const {
itemId,
name,
notificationType,
detailUrl,
successTurnedOn,
errorTurnedOn,
toggleNotification
} = this.props;
const capText = {
textTransform: 'capitalize'
};
return (
<I18n>
{({ i18n }) => (
<li key={itemId} className="pf-c-data-list__item pf-u-flex-row pf-u-align-items-center">
<div className="pf-c-data-list__cell pf-u-flex-row">
<div className="pf-u-display-inline-flex">
<Link
to={{
pathname: detailUrl
}}
>
<b>{name}</b>
</Link>
</div>
<Badge
style={capText}
className="pf-u-display-inline-flex"
isRead
>
{notificationType}
</Badge>
</div>
<div className="pf-c-data-list__cell" />
<div className="pf-c-data-list__cell pf-u-display-flex pf-u-justify-content-flex-end">
<Switch
label={i18n._(t`Successful`)}
isChecked={successTurnedOn}
onChange={() => toggleNotification(itemId, successTurnedOn, 'success')}
aria-label={i18n._(t`Notification success toggle`)}
/>
<Switch
label={i18n._(t`Failure`)}
isChecked={errorTurnedOn}
onChange={() => toggleNotification(itemId, errorTurnedOn, 'error')}
aria-label={i18n._(t`Notification failure toggle`)}
/>
</div>
</li>
)}
</I18n>
);
}
}
export default NotificationListItem;

View File

@@ -0,0 +1,362 @@
import React, {
Component,
Fragment
} from 'react';
import { Title, EmptyState, EmptyStateIcon, EmptyStateBody } from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';
import { I18n, i18nMark } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import DataListToolbar from '../DataListToolbar';
import NotificationListItem from './NotificationListItem';
import Pagination from '../Pagination';
import {
encodeQueryString,
parseQueryString,
} from '../../qs';
class Notifications extends Component {
columns = [
{ name: i18nMark('Name'), key: 'name', isSortable: true },
{ name: i18nMark('Modified'), key: 'modified', isSortable: true, isNumeric: true },
{ name: i18nMark('Created'), key: 'created', isSortable: true, isNumeric: true },
];
defaultParams = {
page: 1,
page_size: 5,
order_by: 'name',
};
pageSizeOptions = [5, 10, 25, 50];
constructor (props) {
super(props);
const { page, page_size } = this.getQueryParams();
this.state = {
page,
page_size,
sortedColumnKey: 'name',
sortOrder: 'ascending',
count: null,
error: null,
loading: true,
results: [],
selected: [],
successTemplateIds: [],
errorTemplateIds: []
};
this.onSearch = this.onSearch.bind(this);
this.getQueryParams = this.getQueryParams.bind(this);
this.onSort = this.onSort.bind(this);
this.onSetPage = this.onSetPage.bind(this);
this.onSelectAll = this.onSelectAll.bind(this);
this.onSelect = this.onSelect.bind(this);
this.toggleNotification = this.toggleNotification.bind(this);
this.updateUrl = this.updateUrl.bind(this);
this.postToError = this.postToError.bind(this);
this.postToSuccess = this.postToSuccess.bind(this);
this.fetchNotifications = this.fetchNotifications.bind(this);
}
componentDidMount () {
const queryParams = this.getQueryParams();
// TODO: remove this hack once tab query param is gone
const { tab, ...queryParamsWithoutTab } = queryParams;
this.fetchNotifications(queryParamsWithoutTab);
}
onSearch () {
const { sortedColumnKey, sortOrder } = this.state;
this.onSort(sortedColumnKey, sortOrder);
}
getQueryParams (overrides = {}) {
const { location } = this.props;
const { search } = location;
const searchParams = parseQueryString(search.substring(1));
// TODO: remove this hack once tab query param is gone
const { tab, ...queryParamsWithoutTab } = searchParams;
return Object.assign({}, this.defaultParams, queryParamsWithoutTab, overrides);
}
onSort = (sortedColumnKey, sortOrder) => {
const { page_size } = this.state;
let order_by = sortedColumnKey;
if (sortOrder === 'descending') {
order_by = `-${order_by}`;
}
const queryParams = this.getQueryParams({ order_by, page_size });
this.fetchNotifications(queryParams);
};
onSetPage = (pageNumber, pageSize) => {
const page = parseInt(pageNumber, 10);
const page_size = parseInt(pageSize, 10);
const queryParams = this.getQueryParams({ page, page_size });
this.fetchNotifications(queryParams);
};
onSelectAll = isSelected => {
const { results } = this.state;
const selected = isSelected ? results.map(o => o.id) : [];
this.setState({ selected });
};
onSelect = id => {
const { selected } = this.state;
const isSelected = selected.includes(id);
if (isSelected) {
this.setState({ selected: selected.filter(s => s !== id) });
} else {
this.setState({ selected: selected.concat(id) });
}
};
toggleNotification = (id, isCurrentlyOn, status) => {
if (status === 'success') {
this.postToSuccess(id, isCurrentlyOn);
} else if (status === 'error') {
this.postToError(id, isCurrentlyOn);
}
};
updateUrl (queryParams) {
const { history, location, match } = this.props;
const pathname = match.url;
const search = `?${encodeQueryString(queryParams)}`;
if (search !== location.search) {
history.replace({ pathname, search });
}
}
async postToError (id, isCurrentlyOn) {
const { postError, match } = this.props;
const postParams = { id };
if (isCurrentlyOn) {
postParams.disassociate = true;
}
try {
await postError(match.params.id, postParams);
} catch (err) {
this.setState({ error: true });
} finally {
if (isCurrentlyOn) {
// Remove it from state
this.setState((prevState) => ({
errorTemplateIds: prevState.errorTemplateIds.filter((templateId) => templateId !== id)
}));
} else {
// Add it to state
this.setState(prevState => ({
errorTemplateIds: [...prevState.errorTemplateIds, id]
}));
}
}
}
async postToSuccess (id, isCurrentlyOn) {
const { postSuccess, match } = this.props;
const postParams = { id };
if (isCurrentlyOn) {
postParams.disassociate = true;
}
try {
await postSuccess(match.params.id, postParams);
} catch (err) {
this.setState({ error: true });
} finally {
if (isCurrentlyOn) {
// Remove it from state
this.setState((prevState) => ({
successTemplateIds: prevState.successTemplateIds.filter((templateId) => templateId !== id)
}));
} else {
// Add it to state
this.setState(prevState => ({
successTemplateIds: [...prevState.successTemplateIds, id]
}));
}
}
}
async fetchNotifications (queryParams) {
const { noInitialResults } = this.state;
const { getNotifications, getSuccess, getError, match } = this.props;
const { page, page_size, order_by } = queryParams;
let sortOrder = 'ascending';
let sortedColumnKey = order_by;
if (order_by.startsWith('-')) {
sortOrder = 'descending';
sortedColumnKey = order_by.substring(1);
}
this.setState({ error: false, loading: true });
try {
const { data } = await getNotifications(match.params.id, queryParams);
const { count, results } = data;
const pageCount = Math.ceil(count / page_size);
const stateToUpdate = {
count,
page,
pageCount,
page_size,
sortOrder,
sortedColumnKey,
results,
noInitialResults,
selected: []
};
// This is in place to track whether or not the initial request
// return any results. If it did not, we show the empty state.
// This will become problematic once search is in play because
// the first load may have query params (think bookmarked search)
if (typeof noInitialResults === 'undefined') {
stateToUpdate.noInitialResults = results.length === 0;
}
this.setState(stateToUpdate);
// TODO: remove this hack once tab query param is gone
this.updateUrl({ ...queryParams, tab: 'notifications' });
const notificationTemplateIds = results
.map(notificationTemplate => notificationTemplate.id)
.join(',');
let successTemplateIds = [];
let errorTemplateIds = [];
if (results.length > 0) {
const successTemplatesPromise = getSuccess(match.params.id, {
id__in: notificationTemplateIds
});
const errorTemplatesPromise = getError(match.params.id, {
id__in: notificationTemplateIds
});
const successTemplatesResult = await successTemplatesPromise;
const errorTemplatesResult = await errorTemplatesPromise;
successTemplateIds = successTemplatesResult.data.results
.map(successTemplate => successTemplate.id);
errorTemplateIds = errorTemplatesResult.data.results
.map(errorTemplate => errorTemplate.id);
}
this.setState({
successTemplateIds,
errorTemplateIds
});
} catch (err) {
this.setState({ error: true });
} finally {
this.setState({ loading: false });
}
}
render () {
const {
count,
error,
loading,
page,
pageCount,
page_size,
sortedColumnKey,
sortOrder,
results,
noInitialResults,
selected,
successTemplateIds,
errorTemplateIds
} = this.state;
return (
<Fragment>
{noInitialResults && (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title size="lg">
<Trans>No Notifictions Found</Trans>
</Title>
<EmptyStateBody>
<Trans>Please add a notification template to populate this list</Trans>
</EmptyStateBody>
</EmptyState>
)}
{(
typeof noInitialResults !== 'undefined'
&& !noInitialResults
&& !loading
&& !error
) && (
<Fragment>
<DataListToolbar
isAllSelected={selected.length === results.length}
sortedColumnKey={sortedColumnKey}
sortOrder={sortOrder}
columns={this.columns}
onSearch={this.onSearch}
onSort={this.onSort}
onSelectAll={this.onSelectAll}
/>
<I18n>
{({ i18n }) => (
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
{results.map(o => (
<NotificationListItem
key={o.id}
itemId={o.id}
name={o.name}
notificationType={o.notification_type}
detailUrl={`notifications/${o.id}`}
isSelected={selected.includes(o.id)}
onSelect={() => this.onSelect(o.id)}
toggleNotification={this.toggleNotification}
errorTurnedOn={errorTemplateIds.includes(o.id)}
successTurnedOn={successTemplateIds.includes(o.id)}
/>
))}
</ul>
)}
</I18n>
<Pagination
count={count}
page={page}
pageCount={pageCount}
page_size={page_size}
pageSizeOptions={this.pageSizeOptions}
onSetPage={this.onSetPage}
/>
</Fragment>
)}
{loading ? <div>loading...</div> : ''}
{error ? <div>error</div> : ''}
</Fragment>
);
}
}
export default Notifications;

View File

@@ -2,10 +2,10 @@ import React from 'react';
import { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import OrganizationsList from './screens/OrganizationsList'; import OrganizationsList from './screens/OrganizationsList';
import OrganizationAdd from './screens/OrganizationAdd' import OrganizationAdd from './screens/OrganizationAdd';
import Organization from './screens/Organization/Organization'; import Organization from './screens/Organization/Organization';
export default ({ api, match }) => ( export default ({ api, match, history }) => (
<Switch> <Switch>
<Route <Route
path={`${match.path}/add`} path={`${match.path}/add`}
@@ -20,6 +20,7 @@ export default ({ api, match }) => (
render={() => ( render={() => (
<Organization <Organization
api={api} api={api}
history={history}
/> />
)} )}
/> />

View File

@@ -75,7 +75,7 @@ class Organization extends Component {
} }
render () { render () {
const { location, match } = this.props; const { location, match, api, history } = this.props;
const { parentBreadcrumbObj, organization, error, loading } = this.state; const { parentBreadcrumbObj, organization, error, loading } = this.state;
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const currentTab = params.get('tab') || 'details'; const currentTab = params.get('tab') || 'details';
@@ -92,7 +92,7 @@ class Organization extends Component {
<Switch> <Switch>
<Route <Route
path={`${match.path}/edit`} path={`${match.path}/edit`}
component={() => ( render={() => (
<OrganizationEdit <OrganizationEdit
location={location} location={location}
match={match} match={match}
@@ -105,7 +105,7 @@ class Organization extends Component {
/> />
<Route <Route
path={`${match.path}`} path={`${match.path}`}
component={() => ( render={() => (
<OrganizationDetail <OrganizationDetail
location={location} location={location}
match={match} match={match}
@@ -113,6 +113,8 @@ class Organization extends Component {
organization={organization} organization={organization}
params={params} params={params}
currentTab={currentTab} currentTab={currentTab}
history={history}
api={api}
/> />
)} )}
/> />

View File

@@ -12,21 +12,24 @@ import {
Route Route
} from 'react-router-dom'; } from 'react-router-dom';
import OrganizationNotifications from './OrganizationNotifications';
import Tab from '../../../../components/Tabs/Tab'; import Tab from '../../../../components/Tabs/Tab';
import Tabs from '../../../../components/Tabs/Tabs'; import Tabs from '../../../../components/Tabs/Tabs';
import getTabName from '../../utils'; import getTabName from '../../utils';
const OrganizationDetail = ({ const OrganizationDetail = ({
location, location,
match, match,
parentBreadcrumbObj, parentBreadcrumbObj,
organization, organization,
params, params,
currentTab currentTab,
api,
history
}) => { }) => {
// TODO: set objectName by param or through grabbing org detail get from api // TODO: set objectName by param or through grabbing org detail get from api
const tabList=['details', 'access', 'teams', 'notifications']; const tabList = ['details', 'access', 'teams', 'notifications'];
const deleteResourceView = () => ( const deleteResourceView = () => (
<Fragment> <Fragment>
@@ -46,19 +49,35 @@ const OrganizationDetail = ({
</Fragment> </Fragment>
); );
const resourceView = () => ( const resourceView = () => {
<Fragment> let relatedTemplate;
<Trans>{`${currentTab} detail view `}</Trans> switch (currentTab) {
<Link to={{ pathname: `${match.url}/add-resource`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}> case 'notifications':
<Trans>{`add ${currentTab}`}</Trans> relatedTemplate = (
</Link> <OrganizationNotifications
{' '} api={api}
<Link to={{ pathname: `${match.url}/delete-resources`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}> match={match}
<Trans>{`delete ${currentTab}`}</Trans> location={location}
</Link> history={history}
</Fragment> />
); );
break;
default:
relatedTemplate = (
<Fragment>
<Trans>{`${currentTab} detail view `}</Trans>
<Link to={{ pathname: `${match.url}/add-resource`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
<Trans>{`add ${currentTab}`}</Trans>
</Link>
{' '}
<Link to={{ pathname: `${match.url}/delete-resources`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
<Trans>{`delete ${currentTab}`}</Trans>
</Link>
</Fragment>
);
}
return relatedTemplate;
};
return ( return (
<Card className="at-c-orgPane"> <Card className="at-c-orgPane">
@@ -82,21 +101,12 @@ const OrganizationDetail = ({
)} )}
</I18n> </I18n>
</CardHeader> </CardHeader>
<CardBody> <CardBody className="at-c-listCardBody">
{(currentTab && currentTab !== 'details') ? ( <Switch>
<Switch> <Route path={`${match.path}/delete-resources`} component={() => deleteResourceView()} />
<Route path={`${match.path}/delete-resources`} component={() => deleteResourceView()} /> <Route path={`${match.path}/add-resource`} component={() => addResourceView()} />
<Route path={`${match.path}/add-resource`} component={() => addResourceView()} /> <Route path={`${match.path}`} render={(props) => resourceView(props)} />
<Route path={`${match.path}`} component={() => resourceView()} /> </Switch>
</Switch>
) : (
<Fragment>
{'detail view '}
<Link to={{ pathname: `${match.url}/edit`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{'edit'}
</Link>
</Fragment>
)}
</CardBody> </CardBody>
</Card> </Card>
); );

View File

@@ -0,0 +1,63 @@
import React, { Component } from 'react';
import NotificationsList from '../../../../components/NotificationsList/Notifications.list';
class OrganizationNotifications extends Component {
constructor (props) {
super(props);
this.getOrgNotifications = this.getOrgNotifications.bind(this);
this.getOrgNotificationSuccess = this.getOrgNotificationSuccess.bind(this);
this.getOrgNotificationError = this.getOrgNotificationError.bind(this);
this.createOrgNotificationSuccess = this.createOrgNotificationSuccess.bind(this);
this.createOrgNotificationError = this.createOrgNotificationError.bind(this);
}
getOrgNotifications (id, reqParams) {
const { api } = this.props;
return api.getOrganizationNotifications(id, reqParams);
}
getOrgNotificationSuccess (id, reqParams) {
const { api } = this.props;
return api.getOrganizationNotificationSuccess(id, reqParams);
}
getOrgNotificationError (id, reqParams) {
const { api } = this.props;
return api.getOrganizationNotificationError(id, reqParams);
}
createOrgNotificationSuccess (id, data) {
const { api } = this.props;
return api.createOrganizationNotificationSuccess(id, data);
}
createOrgNotificationError (id, data) {
const { api } = this.props;
return api.createOrganizationNotificationError(id, data);
}
render () {
const {
location,
match,
history
} = this.props;
return (
<NotificationsList
getNotifications={this.getOrgNotifications}
getSuccess={this.getOrgNotificationSuccess}
getError={this.getOrgNotificationError}
postSuccess={this.createOrgNotificationSuccess}
postError={this.createOrgNotificationError}
match={match}
location={location}
history={history}
/>
);
}
}
export default OrganizationNotifications;

View File

@@ -75,16 +75,7 @@ class OrganizationsList extends Component {
this.onSort(sortedColumnKey, sortOrder); this.onSort(sortedColumnKey, sortOrder);
} }
getQueryParams (overrides = {}) { onSort (sortedColumnKey, sortOrder) {
const { location } = this.props;
const { search } = location;
const searchParams = parseQueryString(search.substring(1));
return Object.assign({}, this.defaultParams, searchParams, overrides);
}
onSort(sortedColumnKey, sortOrder) {
const { page_size } = this.state; const { page_size } = this.state;
let order_by = sortedColumnKey; let order_by = sortedColumnKey;
@@ -127,6 +118,15 @@ class OrganizationsList extends Component {
} }
} }
getQueryParams (overrides = {}) {
const { location } = this.props;
const { search } = location;
const searchParams = parseQueryString(search.substring(1));
return Object.assign({}, this.defaultParams, searchParams, overrides);
}
updateUrl (queryParams) { updateUrl (queryParams) {
const { history, location } = this.props; const { history, location } = this.props;
const pathname = '/organizations'; const pathname = '/organizations';
@@ -212,6 +212,8 @@ class OrganizationsList extends Component {
onSearch={this.onSearch} onSearch={this.onSearch}
onSort={this.onSort} onSort={this.onSort}
onSelectAll={this.onSelectAll} onSelectAll={this.onSelectAll}
showDelete
showSelectAll
/> />
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (