mirror of
https://github.com/ansible/awx.git
synced 2026-02-15 18:20:00 -03:30
update content loading and error handling
unwind error handling use auth cookie as source of truth, fetch config only when authenticated
This commit is contained in:
233
src/App.jsx
233
src/App.jsx
@@ -6,19 +6,16 @@ import {
|
||||
Page,
|
||||
PageHeader as PFPageHeader,
|
||||
PageSidebar,
|
||||
Button
|
||||
} from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import { RootDialog } from './contexts/RootDialog';
|
||||
import { withNetwork } from './contexts/Network';
|
||||
import { Config } from './contexts/Config';
|
||||
import { RootAPI } from './api';
|
||||
import { ConfigAPI, MeAPI, RootAPI } from './api';
|
||||
import { ConfigProvider } from './contexts/Config';
|
||||
|
||||
import AlertModal from './components/AlertModal';
|
||||
import About from './components/About';
|
||||
import AlertModal from './components/AlertModal';
|
||||
import NavExpandableGroup from './components/NavExpandableGroup';
|
||||
import BrandLogo from './components/BrandLogo';
|
||||
import PageHeaderToolbar from './components/PageHeaderToolbar';
|
||||
@@ -46,129 +43,145 @@ class App extends Component {
|
||||
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||
|
||||
this.state = {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: null,
|
||||
me: null,
|
||||
version: null,
|
||||
isAboutModalOpen: false,
|
||||
isNavOpen
|
||||
isNavOpen,
|
||||
configError: false,
|
||||
};
|
||||
|
||||
this.onLogout = this.onLogout.bind(this);
|
||||
this.onAboutModalClose = this.onAboutModalClose.bind(this);
|
||||
this.onAboutModalOpen = this.onAboutModalOpen.bind(this);
|
||||
this.onNavToggle = this.onNavToggle.bind(this);
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
this.handleAboutClose = this.handleAboutClose.bind(this);
|
||||
this.handleAboutOpen = this.handleAboutOpen.bind(this);
|
||||
this.handleNavToggle = this.handleNavToggle.bind(this);
|
||||
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
||||
}
|
||||
|
||||
async onLogout () {
|
||||
const { handleHttpError } = this.props;
|
||||
try {
|
||||
await RootAPI.logout();
|
||||
window.location.replace('/#/login');
|
||||
} catch (err) {
|
||||
handleHttpError(err);
|
||||
}
|
||||
async componentDidMount () {
|
||||
await this.loadConfig();
|
||||
}
|
||||
|
||||
onAboutModalOpen () {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async handleLogout () {
|
||||
await RootAPI.logout();
|
||||
window.location.replace('/#/login');
|
||||
}
|
||||
|
||||
handleAboutOpen () {
|
||||
this.setState({ isAboutModalOpen: true });
|
||||
}
|
||||
|
||||
onAboutModalClose () {
|
||||
handleAboutClose () {
|
||||
this.setState({ isAboutModalOpen: false });
|
||||
}
|
||||
|
||||
onNavToggle () {
|
||||
handleNavToggle () {
|
||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isAboutModalOpen, isNavOpen } = this.state;
|
||||
handleConfigErrorClose () {
|
||||
this.setState({ configError: false });
|
||||
}
|
||||
|
||||
const { render, routeGroups = [], navLabel = '', i18n } = this.props;
|
||||
async loadConfig () {
|
||||
try {
|
||||
const [configRes, meRes] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
||||
const { data: { ansible_version, custom_virtualenvs, version } } = configRes;
|
||||
const { data: { results: [me] } } = meRes;
|
||||
|
||||
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
||||
} catch (err) {
|
||||
this.setState({ configError: true });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
isAboutModalOpen,
|
||||
isNavOpen,
|
||||
me,
|
||||
version,
|
||||
configError,
|
||||
} = this.state;
|
||||
const {
|
||||
i18n,
|
||||
render = () => {},
|
||||
routeGroups = [],
|
||||
navLabel = '',
|
||||
} = this.props;
|
||||
|
||||
const header = (
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={this.handleNavToggle}
|
||||
logo={<BrandLogo />}
|
||||
logoProps={{ href: '/' }}
|
||||
toolbar={(
|
||||
<PageHeaderToolbar
|
||||
loggedInUser={me}
|
||||
isAboutDisabled={!version}
|
||||
onAboutClick={this.handleAboutOpen}
|
||||
onLogoutClick={this.handleLogout}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
const sidebar = (
|
||||
<PageSidebar
|
||||
isNavOpen={isNavOpen}
|
||||
nav={(
|
||||
<Nav aria-label={navLabel}>
|
||||
<NavList>
|
||||
{routeGroups.map(
|
||||
({ groupId, groupTitle, routes }) => (
|
||||
<NavExpandableGroup
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
groupTitle={groupTitle}
|
||||
routes={routes}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</NavList>
|
||||
</Nav>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Config>
|
||||
{({ ansible_version, version, me }) => (
|
||||
<RootDialog>
|
||||
{({
|
||||
title,
|
||||
bodyText,
|
||||
variant = 'info',
|
||||
clearRootDialogMessage
|
||||
}) => (
|
||||
<Fragment>
|
||||
{(title || bodyText) && (
|
||||
<AlertModal
|
||||
variant={variant}
|
||||
isOpen={!!(title || bodyText)}
|
||||
onClose={clearRootDialogMessage}
|
||||
title={title}
|
||||
actions={[
|
||||
<Button
|
||||
key="close"
|
||||
variant="secondary"
|
||||
onClick={clearRootDialogMessage}
|
||||
>
|
||||
{i18n._(t`Close`)}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{bodyText}
|
||||
</AlertModal>
|
||||
)}
|
||||
<Page
|
||||
usecondensed="True"
|
||||
header={(
|
||||
<PageHeader
|
||||
showNavToggle
|
||||
onNavToggle={this.onNavToggle}
|
||||
logo={<BrandLogo />}
|
||||
logoProps={{ href: '/' }}
|
||||
toolbar={(
|
||||
<PageHeaderToolbar
|
||||
loggedInUser={me}
|
||||
isAboutDisabled={!version}
|
||||
onAboutClick={this.onAboutModalOpen}
|
||||
onLogoutClick={this.onLogout}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
sidebar={(
|
||||
<PageSidebar
|
||||
isNavOpen={isNavOpen}
|
||||
nav={(
|
||||
<Nav aria-label={navLabel}>
|
||||
<NavList>
|
||||
{routeGroups.map(
|
||||
({ groupId, groupTitle, routes }) => (
|
||||
<NavExpandableGroup
|
||||
key={groupId}
|
||||
groupId={groupId}
|
||||
groupTitle={groupTitle}
|
||||
routes={routes}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</NavList>
|
||||
</Nav>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{render && render({ routeGroups })}
|
||||
</Page>
|
||||
<About
|
||||
ansible_version={ansible_version}
|
||||
version={version}
|
||||
isOpen={isAboutModalOpen}
|
||||
onClose={this.onAboutModalClose}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</RootDialog>
|
||||
)}
|
||||
</Config>
|
||||
<Fragment>
|
||||
<Page
|
||||
usecondensed="True"
|
||||
header={header}
|
||||
sidebar={sidebar}
|
||||
>
|
||||
<ConfigProvider value={{ ansible_version, custom_virtualenvs, me, version }}>
|
||||
{render({ routeGroups })}
|
||||
</ConfigProvider>
|
||||
</Page>
|
||||
<About
|
||||
ansible_version={ansible_version}
|
||||
version={version}
|
||||
isOpen={isAboutModalOpen}
|
||||
onClose={this.handleAboutClose}
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={configError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleConfigErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to retrieve configuration.`)}
|
||||
</AlertModal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { App as _App };
|
||||
export default withI18n()(withNetwork(App));
|
||||
export default withI18n()(App);
|
||||
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
HashRouter
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { NetworkProvider } from './contexts/Network';
|
||||
import { RootDialogProvider } from './contexts/RootDialog';
|
||||
import { ConfigProvider } from './contexts/Config';
|
||||
|
||||
import ja from '../build/locales/ja/messages';
|
||||
import en from '../build/locales/en/messages';
|
||||
|
||||
@@ -34,13 +30,7 @@ class RootProvider extends Component {
|
||||
language={language}
|
||||
catalogs={catalogs}
|
||||
>
|
||||
<RootDialogProvider>
|
||||
<NetworkProvider>
|
||||
<ConfigProvider>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</NetworkProvider>
|
||||
</RootDialogProvider>
|
||||
{children}
|
||||
</I18nProvider>
|
||||
</HashRouter>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,36 @@ const NotificationsMixin = (parent) => class extends parent {
|
||||
disassociateNotificationTemplatesError (resourceId, notificationId) {
|
||||
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper method meant to simplify setting the "on" or "off" status of
|
||||
* a related notification.
|
||||
*
|
||||
* @param[resourceId] - id of the base resource
|
||||
* @param[notificationId] - id of the notification
|
||||
* @param[notificationType] - the type of notification, options are "success" and "error"
|
||||
* @param[associationState] - Boolean for associating or disassociating, options are true or false
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
updateNotificationTemplateAssociation (resourceId, notificationId, notificationType, associationState) {
|
||||
if (notificationType === 'success' && associationState === true) {
|
||||
return this.associateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||
}
|
||||
|
||||
if (notificationType === 'success' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === true) {
|
||||
return this.associateNotificationTemplatesError(resourceId, notificationId);
|
||||
}
|
||||
|
||||
if (notificationType === 'error' && associationState === false) {
|
||||
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default NotificationsMixin;
|
||||
|
||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import { withNetwork } from '../../contexts/Network';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
import SelectableCard from './SelectableCard';
|
||||
@@ -245,4 +244,4 @@ AddResourceRole.defaultProps = {
|
||||
};
|
||||
|
||||
export { AddResourceRole as _AddResourceRole };
|
||||
export default withI18n()(withNetwork(AddResourceRole));
|
||||
export default withI18n()(AddResourceRole);
|
||||
|
||||
25
src/components/ContentEmpty.jsx
Normal file
25
src/components/ContentEmpty.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
|
||||
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">
|
||||
{title || i18n._(t`No items found.`)}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{message}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentEmpty as _ContentEmpty };
|
||||
export default withI18n()(ContentEmpty);
|
||||
26
src/components/ContentError.jsx
Normal file
26
src/components/ContentError.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||
|
||||
// TODO: Pass actual error as prop and display expandable details for network errors.
|
||||
const ContentError = ({ i18n }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
||||
<Title size="lg">
|
||||
{i18n._(t`Something went wrong...`)}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{i18n._(t`There was an error loading this content. Please reload the page.`)}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentError as _ContentError };
|
||||
export default withI18n()(ContentError);
|
||||
19
src/components/ContentLoading.jsx
Normal file
19
src/components/ContentLoading.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
// TODO: Better loading state - skeleton lines / spinner, etc.
|
||||
const ContentLoading = ({ i18n }) => (
|
||||
<EmptyState>
|
||||
<EmptyStateBody>
|
||||
{i18n._(t`Loading...`)}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
);
|
||||
|
||||
export { ContentLoading as _ContentLoading };
|
||||
export default withI18n()(ContentLoading);
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { withNetwork } from '../../contexts/Network';
|
||||
import PaginatedDataList from '../PaginatedDataList';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import CheckboxListItem from '../ListItem';
|
||||
@@ -53,8 +52,8 @@ class Lookup extends React.Component {
|
||||
}
|
||||
|
||||
async getData () {
|
||||
const { getItems, handleHttpError, location } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
|
||||
const { getItems, location: { search } } = this.props;
|
||||
const queryParams = parseNamespacedQueryString(this.qsConfig, search);
|
||||
|
||||
this.setState({ error: false });
|
||||
try {
|
||||
@@ -66,7 +65,7 @@ class Lookup extends React.Component {
|
||||
count
|
||||
});
|
||||
} catch (err) {
|
||||
handleHttpError(err) || this.setState({ error: true });
|
||||
this.setState({ error: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,4 +213,4 @@ Lookup.defaultProps = {
|
||||
};
|
||||
|
||||
export { Lookup as _Lookup };
|
||||
export default withI18n()(withNetwork(withRouter(Lookup)));
|
||||
export default withI18n()(withRouter(Lookup));
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { Redirect, withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { withRootDialog } from '../contexts/RootDialog';
|
||||
|
||||
const NotifyAndRedirect = ({
|
||||
to,
|
||||
push,
|
||||
from,
|
||||
exact,
|
||||
strict,
|
||||
sensitive,
|
||||
setRootDialogMessage,
|
||||
location,
|
||||
i18n
|
||||
}) => {
|
||||
setRootDialogMessage({
|
||||
title: '404',
|
||||
bodyText: (
|
||||
<Fragment>{i18n._(t`Cannot find route ${(<strong>{location.pathname}</strong>)}.`)}</Fragment>
|
||||
),
|
||||
variant: 'warning'
|
||||
});
|
||||
|
||||
return (
|
||||
<Redirect
|
||||
to={to}
|
||||
push={push}
|
||||
from={from}
|
||||
exact={exact}
|
||||
strict={strict}
|
||||
sensitive={sensitive}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { NotifyAndRedirect as _NotifyAndRedirect };
|
||||
export default withI18n()(withRootDialog(withRouter(NotifyAndRedirect)));
|
||||
@@ -1,18 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes, { arrayOf, shape, string, bool } from 'prop-types';
|
||||
import {
|
||||
DataList,
|
||||
Title,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
import { DataList } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ContentEmpty from '../ContentEmpty';
|
||||
import ContentError from '../ContentError';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import Pagination from '../Pagination';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import PaginatedDataListItem from './PaginatedDataListItem';
|
||||
@@ -37,11 +33,6 @@ const EmptyStateControlsWrapper = styled.div`
|
||||
class PaginatedDataList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
this.handleSetPage = this.handleSetPage.bind(this);
|
||||
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
||||
this.handleSort = this.handleSort.bind(this);
|
||||
@@ -79,7 +70,10 @@ class PaginatedDataList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const [orderBy, sortOrder] = this.getSortOrder();
|
||||
const {
|
||||
contentError,
|
||||
contentLoading,
|
||||
emptyStateControls,
|
||||
items,
|
||||
itemCount,
|
||||
@@ -93,66 +87,67 @@ class PaginatedDataList extends React.Component {
|
||||
i18n,
|
||||
renderToolbar,
|
||||
} = this.props;
|
||||
const { error } = this.state;
|
||||
const [orderBy, sortOrder] = this.getSortOrder();
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true }];
|
||||
return (
|
||||
<Fragment>
|
||||
{error && (
|
||||
<Fragment>
|
||||
<div>{error.message}</div>
|
||||
{error.response && (
|
||||
<div>{error.response.data.detail}</div>
|
||||
)}
|
||||
</Fragment> // TODO: replace with proper error handling
|
||||
)}
|
||||
{items.length === 0 ? (
|
||||
<Fragment>
|
||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||
|
||||
const itemDisplayName = ucFirst(pluralize(itemName));
|
||||
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
|
||||
|
||||
const dataListLabel = i18n._(t`${itemDisplayName} List`);
|
||||
const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `);
|
||||
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
|
||||
|
||||
let Content;
|
||||
if (contentLoading && items.length <= 0) {
|
||||
Content = (<ContentLoading />);
|
||||
} else if (contentError) {
|
||||
Content = (<ContentError />);
|
||||
} else if (items.length <= 0) {
|
||||
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
||||
} else {
|
||||
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
|
||||
}
|
||||
|
||||
if (items.length <= 0) {
|
||||
return (
|
||||
<Fragment>
|
||||
{emptyStateControls && (
|
||||
<EmptyStateControlsWrapper>
|
||||
{emptyStateControls}
|
||||
</EmptyStateControlsWrapper>
|
||||
)}
|
||||
{emptyStateControls && (
|
||||
<div css="border-bottom: 1px solid #d2d2d2" />
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={CubesIcon} />
|
||||
<Title size="lg">
|
||||
{i18n._(t`No ${ucFirst(itemNamePlural || pluralize(itemName))} Found `)}
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
{i18n._(t`Please add ${ucFirst(itemNamePlural || pluralize(itemName))} to populate this list `)}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{renderToolbar({
|
||||
sortedColumnKey: orderBy,
|
||||
sortOrder,
|
||||
columns,
|
||||
onSearch: () => { },
|
||||
onSort: this.handleSort,
|
||||
})}
|
||||
<DataList aria-label={i18n._(t`${ucFirst(pluralize(itemName))} List`)}>
|
||||
{items.map(item => (renderItem ? renderItem(item) : (
|
||||
<PaginatedDataListItem key={item.id} item={item} />
|
||||
)))}
|
||||
</DataList>
|
||||
<Pagination
|
||||
variant="bottom"
|
||||
itemCount={itemCount}
|
||||
page={queryParams.page || 1}
|
||||
perPage={queryParams.page_size}
|
||||
perPageOptions={showPageSizeOptions ? [
|
||||
{ title: '5', value: 5 },
|
||||
{ title: '10', value: 10 },
|
||||
{ title: '20', value: 20 },
|
||||
{ title: '50', value: 50 }
|
||||
] : []}
|
||||
onSetPage={this.handleSetPage}
|
||||
onPerPageSelect={this.handleSetPageSize}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
)}
|
||||
{Content}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderToolbar({
|
||||
sortedColumnKey: orderBy,
|
||||
sortOrder,
|
||||
columns,
|
||||
onSearch: () => { },
|
||||
onSort: this.handleSort,
|
||||
})}
|
||||
{Content}
|
||||
<Pagination
|
||||
variant="bottom"
|
||||
itemCount={itemCount}
|
||||
page={queryParams.page || 1}
|
||||
perPage={queryParams.page_size}
|
||||
perPageOptions={showPageSizeOptions ? [
|
||||
{ title: '5', value: 5 },
|
||||
{ title: '10', value: 10 },
|
||||
{ title: '20', value: 20 },
|
||||
{ title: '50', value: 50 }
|
||||
] : []}
|
||||
onSetPage={this.handleSetPage}
|
||||
onPerPageSelect={this.handleSetPageSize}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -178,14 +173,18 @@ PaginatedDataList.propTypes = {
|
||||
})),
|
||||
showPageSizeOptions: PropTypes.bool,
|
||||
renderToolbar: PropTypes.func,
|
||||
contentLoading: PropTypes.bool,
|
||||
contentError: PropTypes.bool,
|
||||
};
|
||||
|
||||
PaginatedDataList.defaultProps = {
|
||||
renderItem: null,
|
||||
contentLoading: false,
|
||||
contentError: false,
|
||||
toolbarColumns: [],
|
||||
itemName: 'item',
|
||||
itemNamePlural: '',
|
||||
showPageSizeOptions: true,
|
||||
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
|
||||
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,136 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { withNetwork } from './Network';
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const ConfigContext = React.createContext({});
|
||||
|
||||
import { ConfigAPI, MeAPI, RootAPI } from '../api';
|
||||
|
||||
const ConfigContext = React.createContext({});
|
||||
|
||||
class Provider extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: null,
|
||||
version: null,
|
||||
custom_logo: null,
|
||||
custom_login_info: null,
|
||||
me: {},
|
||||
...props.value
|
||||
}
|
||||
};
|
||||
|
||||
this.fetchConfig = this.fetchConfig.bind(this);
|
||||
this.fetchMe = this.fetchMe.bind(this);
|
||||
this.updateConfig = this.updateConfig.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { value } = this.props;
|
||||
if (!value) {
|
||||
this.fetchConfig();
|
||||
}
|
||||
}
|
||||
|
||||
updateConfig = config => {
|
||||
const { ansible_version, custom_virtualenvs, version } = config;
|
||||
|
||||
this.setState(prevState => ({
|
||||
value: {
|
||||
...prevState.value,
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
version
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
async fetchMe () {
|
||||
const { handleHttpError } = this.props;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
results: [me]
|
||||
}
|
||||
} = await MeAPI.read();
|
||||
this.setState(prevState => ({
|
||||
value: {
|
||||
...prevState.value,
|
||||
me
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
handleHttpError(err)
|
||||
|| this.setState({
|
||||
value: {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: null,
|
||||
version: null,
|
||||
custom_logo: null,
|
||||
custom_login_info: null,
|
||||
me: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fetchConfig () {
|
||||
const { handleHttpError } = this.props;
|
||||
|
||||
try {
|
||||
const [configRes, rootRes, meRes] = await Promise.all([
|
||||
ConfigAPI.read(),
|
||||
RootAPI.read(),
|
||||
MeAPI.read()
|
||||
]);
|
||||
this.setState({
|
||||
value: {
|
||||
ansible_version: configRes.data.ansible_version,
|
||||
custom_virtualenvs: configRes.data.custom_virtualenvs,
|
||||
version: configRes.data.version,
|
||||
custom_logo: rootRes.data.custom_logo,
|
||||
custom_login_info: rootRes.data.custom_login_info,
|
||||
me: meRes.data.results[0]
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
handleHttpError(err)
|
||||
|| this.setState({
|
||||
value: {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: null,
|
||||
version: null,
|
||||
custom_logo: null,
|
||||
custom_login_info: null,
|
||||
me: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.state;
|
||||
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
...value,
|
||||
fetchMe: this.fetchMe,
|
||||
updateConfig: this.updateConfig
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ConfigProvider = withNetwork(Provider);
|
||||
|
||||
export const Config = ({ children }) => (
|
||||
<ConfigContext.Consumer>{value => children(value)}</ConfigContext.Consumer>
|
||||
);
|
||||
export const ConfigProvider = ConfigContext.Provider;
|
||||
export const Config = ConfigContext.Consumer;
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { withRootDialog } from './RootDialog';
|
||||
|
||||
const NetworkContext = React.createContext({});
|
||||
|
||||
class Provider extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: {
|
||||
handleHttpError: err => {
|
||||
if (err.response.status === 401) {
|
||||
this.handle401();
|
||||
} else if (err.response.status === 404) {
|
||||
this.handle404();
|
||||
}
|
||||
return (err.response.status === 401 || err.response.status === 404);
|
||||
},
|
||||
...props.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handle401 () {
|
||||
const { handle401, history, setRootDialogMessage, i18n } = this.props;
|
||||
if (handle401) {
|
||||
handle401();
|
||||
return;
|
||||
}
|
||||
history.replace('/login');
|
||||
setRootDialogMessage({
|
||||
bodyText: i18n._(t`You have been logged out.`)
|
||||
});
|
||||
}
|
||||
|
||||
handle404 () {
|
||||
const { handle404, history, setRootDialogMessage, i18n } = this.props;
|
||||
if (handle404) {
|
||||
handle404();
|
||||
return;
|
||||
}
|
||||
history.replace('/home');
|
||||
setRootDialogMessage({
|
||||
title: i18n._(t`404`),
|
||||
bodyText: i18n._(t`Cannot find resource.`),
|
||||
variant: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.state;
|
||||
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<NetworkContext.Provider value={value}>
|
||||
{children}
|
||||
</NetworkContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Provider as _NetworkProvider };
|
||||
export const NetworkProvider = withI18n()(withRootDialog(withRouter(Provider)));
|
||||
|
||||
export function withNetwork (Child) {
|
||||
return (props) => (
|
||||
<NetworkContext.Consumer>
|
||||
{context => <Child {...props} {...context} />}
|
||||
</NetworkContext.Consumer>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
const RootDialogContext = React.createContext({});
|
||||
|
||||
export class RootDialogProvider extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: {
|
||||
title: null,
|
||||
setRootDialogMessage: ({ title, bodyText, variant }) => {
|
||||
const { value } = this.state;
|
||||
this.setState({ value: { ...value, title, bodyText, variant } });
|
||||
},
|
||||
clearRootDialogMessage: () => {
|
||||
const { value } = this.state;
|
||||
this.setState({ value: { ...value, title: null, bodyText: null, variant: null } });
|
||||
},
|
||||
...props.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
value
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<RootDialogContext.Provider value={value}>
|
||||
{children}
|
||||
</RootDialogContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const RootDialog = ({ children }) => (
|
||||
<RootDialogContext.Consumer>
|
||||
{value => children(value)}
|
||||
</RootDialogContext.Consumer>
|
||||
);
|
||||
|
||||
export function withRootDialog (Child) {
|
||||
return (props) => (
|
||||
<RootDialogContext.Consumer>
|
||||
{context => <Child {...props} {...context} />}
|
||||
</RootDialogContext.Consumer>
|
||||
);
|
||||
}
|
||||
373
src/index.jsx
373
src/index.jsx
@@ -13,15 +13,12 @@ import { t } from '@lingui/macro';
|
||||
import '@patternfly/react-core/dist/styles/base.css';
|
||||
import './app.scss';
|
||||
|
||||
import { Config } from './contexts/Config';
|
||||
|
||||
import { BrandName } from './variables';
|
||||
|
||||
import Background from './components/Background';
|
||||
import NotifyAndRedirect from './components/NotifyAndRedirect';
|
||||
|
||||
import RootProvider from './RootProvider';
|
||||
import App from './App';
|
||||
import { BrandName } from './variables';
|
||||
import { isAuthenticated } from './util/auth';
|
||||
|
||||
import Applications from './pages/Applications';
|
||||
import Credentials from './pages/Credentials';
|
||||
@@ -52,185 +49,188 @@ export function main (render) {
|
||||
const el = document.getElementById('app');
|
||||
document.title = `Ansible ${BrandName}`;
|
||||
|
||||
const defaultRedirect = () => (<Redirect to="/home" />);
|
||||
const removeTrailingSlash = (
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/*/"
|
||||
render={({ history: { location: { pathname, search, hash } } }) => (
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const loginRoutes = (
|
||||
<Switch>
|
||||
{removeTrailingSlash}
|
||||
<Route
|
||||
path="/login"
|
||||
render={() => (
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
)}
|
||||
/>
|
||||
<Redirect to="/login" />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
return render(
|
||||
<RootProvider>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Background>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path="/*/"
|
||||
render={({ history: { location: { pathname, search, hash } } }) => (
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
render={() => (
|
||||
<Config>
|
||||
{({ custom_logo, custom_login_info, fetchMe, updateConfig }) => (
|
||||
<Login
|
||||
logo={custom_logo}
|
||||
loginInfo={custom_login_info}
|
||||
fetchMe={fetchMe}
|
||||
updateConfig={updateConfig}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route exact path="/" render={() => <Redirect to="/home" />} />
|
||||
<Route
|
||||
render={() => (
|
||||
<App
|
||||
navLabel={i18n._(t`Primary Navigation`)}
|
||||
routeGroups={[
|
||||
{
|
||||
groupTitle: i18n._(t`Views`),
|
||||
groupId: 'views_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Dashboard`),
|
||||
path: '/home',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Jobs`),
|
||||
path: '/jobs',
|
||||
component: Jobs
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Schedules`),
|
||||
path: '/schedules',
|
||||
component: Schedules
|
||||
},
|
||||
{
|
||||
title: i18n._(t`My View`),
|
||||
path: '/portal',
|
||||
component: Portal
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Resources`),
|
||||
groupId: 'resources_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Templates`),
|
||||
path: '/templates',
|
||||
component: Templates
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Credentials`),
|
||||
path: '/credentials',
|
||||
component: Credentials
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Projects`),
|
||||
path: '/projects',
|
||||
component: Projects
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Inventories`),
|
||||
path: '/inventories',
|
||||
component: Inventories
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Inventory Scripts`),
|
||||
path: '/inventory_scripts',
|
||||
component: InventoryScripts
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Access`),
|
||||
groupId: 'access_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Organizations`),
|
||||
path: '/organizations',
|
||||
component: Organizations
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Users`),
|
||||
path: '/users',
|
||||
component: Users
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Teams`),
|
||||
path: '/teams',
|
||||
component: Teams
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Administration`),
|
||||
groupId: 'administration_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Credential Types`),
|
||||
path: '/credential_types',
|
||||
component: CredentialTypes
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Notifications`),
|
||||
path: '/notification_templates',
|
||||
component: NotificationTemplates
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Management Jobs`),
|
||||
path: '/management_jobs',
|
||||
component: ManagementJobs
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Instance Groups`),
|
||||
path: '/instance_groups',
|
||||
component: InstanceGroups
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Integrations`),
|
||||
path: '/applications',
|
||||
component: Applications
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Settings`),
|
||||
groupId: 'settings_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Authentication`),
|
||||
path: '/auth_settings',
|
||||
component: AuthSettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Jobs`),
|
||||
path: '/jobs_settings',
|
||||
component: JobsSettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`System`),
|
||||
path: '/system_settings',
|
||||
component: SystemSettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`User Interface`),
|
||||
path: '/ui_settings',
|
||||
component: UISettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`License`),
|
||||
path: '/license',
|
||||
component: License
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
render={({ routeGroups }) => (
|
||||
<Switch>
|
||||
{routeGroups
|
||||
{!isAuthenticated() ? loginRoutes : (
|
||||
<Switch>
|
||||
{removeTrailingSlash}
|
||||
<Route path="/login" render={defaultRedirect} />
|
||||
<Route exact path="/" render={defaultRedirect} />
|
||||
<Route
|
||||
render={() => (
|
||||
<App
|
||||
navLabel={i18n._(t`Primary Navigation`)}
|
||||
routeGroups={[
|
||||
{
|
||||
groupTitle: i18n._(t`Views`),
|
||||
groupId: 'views_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Dashboard`),
|
||||
path: '/home',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Jobs`),
|
||||
path: '/jobs',
|
||||
component: Jobs
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Schedules`),
|
||||
path: '/schedules',
|
||||
component: Schedules
|
||||
},
|
||||
{
|
||||
title: i18n._(t`My View`),
|
||||
path: '/portal',
|
||||
component: Portal
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Resources`),
|
||||
groupId: 'resources_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Templates`),
|
||||
path: '/templates',
|
||||
component: Templates
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Credentials`),
|
||||
path: '/credentials',
|
||||
component: Credentials
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Projects`),
|
||||
path: '/projects',
|
||||
component: Projects
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Inventories`),
|
||||
path: '/inventories',
|
||||
component: Inventories
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Inventory Scripts`),
|
||||
path: '/inventory_scripts',
|
||||
component: InventoryScripts
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Access`),
|
||||
groupId: 'access_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Organizations`),
|
||||
path: '/organizations',
|
||||
component: Organizations
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Users`),
|
||||
path: '/users',
|
||||
component: Users
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Teams`),
|
||||
path: '/teams',
|
||||
component: Teams
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Administration`),
|
||||
groupId: 'administration_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Credential Types`),
|
||||
path: '/credential_types',
|
||||
component: CredentialTypes
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Notifications`),
|
||||
path: '/notification_templates',
|
||||
component: NotificationTemplates
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Management Jobs`),
|
||||
path: '/management_jobs',
|
||||
component: ManagementJobs
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Instance Groups`),
|
||||
path: '/instance_groups',
|
||||
component: InstanceGroups
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Integrations`),
|
||||
path: '/applications',
|
||||
component: Applications
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupTitle: i18n._(t`Settings`),
|
||||
groupId: 'settings_group',
|
||||
routes: [
|
||||
{
|
||||
title: i18n._(t`Authentication`),
|
||||
path: '/auth_settings',
|
||||
component: AuthSettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`Jobs`),
|
||||
path: '/jobs_settings',
|
||||
component: JobsSettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`System`),
|
||||
path: '/system_settings',
|
||||
component: SystemSettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`User Interface`),
|
||||
path: '/ui_settings',
|
||||
component: UISettings
|
||||
},
|
||||
{
|
||||
title: i18n._(t`License`),
|
||||
path: '/license',
|
||||
component: License
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
render={({ routeGroups }) => (
|
||||
routeGroups
|
||||
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
||||
.map(({ component: PageComponent, path }) => (
|
||||
<Route
|
||||
@@ -241,15 +241,12 @@ export function main (render) {
|
||||
)}
|
||||
/>
|
||||
))
|
||||
.concat([
|
||||
<NotifyAndRedirect key="redirect" to="/" />
|
||||
])}
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
</Background>
|
||||
)}
|
||||
</I18n>
|
||||
|
||||
@@ -7,13 +7,10 @@ import {
|
||||
LoginForm,
|
||||
LoginPage as PFLoginPage,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { withRootDialog } from '../contexts/RootDialog';
|
||||
import { withNetwork } from '../contexts/Network';
|
||||
import { RootAPI } from '../api';
|
||||
import { BrandName } from '../variables';
|
||||
|
||||
import logoImg from '../../images/brand-logo.svg';
|
||||
import brandLogo from '../../images/brand-logo.svg';
|
||||
|
||||
const LoginPage = styled(PFLoginPage)`
|
||||
& .pf-c-brand {
|
||||
@@ -28,80 +25,122 @@ class AWXLogin extends Component {
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
isInputValid: true,
|
||||
isLoading: false,
|
||||
isAuthenticated: false
|
||||
authenticationError: false,
|
||||
validationError: false,
|
||||
isAuthenticating: false,
|
||||
isLoading: true,
|
||||
logo: null,
|
||||
loginInfo: null,
|
||||
};
|
||||
|
||||
this.onChangeUsername = this.onChangeUsername.bind(this);
|
||||
this.onChangePassword = this.onChangePassword.bind(this);
|
||||
this.onLoginButtonClick = this.onLoginButtonClick.bind(this);
|
||||
this.handleChangeUsername = this.handleChangeUsername.bind(this);
|
||||
this.handleChangePassword = this.handleChangePassword.bind(this);
|
||||
this.handleLoginButtonClick = this.handleLoginButtonClick.bind(this);
|
||||
this.loadCustomLoginInfo = this.loadCustomLoginInfo.bind(this);
|
||||
}
|
||||
|
||||
onChangeUsername (value) {
|
||||
this.setState({ username: value, isInputValid: true });
|
||||
async componentDidMount () {
|
||||
await this.loadCustomLoginInfo();
|
||||
}
|
||||
|
||||
onChangePassword (value) {
|
||||
this.setState({ password: value, isInputValid: true });
|
||||
async loadCustomLoginInfo () {
|
||||
this.setState({ isLoading: true });
|
||||
try {
|
||||
const { data: { custom_logo, custom_login_info } } = await RootAPI.read();
|
||||
const logo = custom_logo ? `data:image/jpeg;${custom_logo}` : brandLogo;
|
||||
|
||||
this.setState({ logo, loginInfo: custom_login_info });
|
||||
} catch (err) {
|
||||
this.setState({ logo: brandLogo });
|
||||
} finally {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
async onLoginButtonClick (event) {
|
||||
const { username, password, isLoading } = this.state;
|
||||
const { handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
|
||||
async handleLoginButtonClick (event) {
|
||||
const { username, password, isAuthenticating } = this.state;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (isLoading) {
|
||||
if (isAuthenticating) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRootDialogMessage();
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
this.setState({ authenticationError: false, isAuthenticating: true });
|
||||
try {
|
||||
const { data } = await RootAPI.login(username, password);
|
||||
updateConfig(data);
|
||||
await fetchMe();
|
||||
this.setState({ isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
||||
// note: if authentication is successful, the appropriate cookie will be set automatically
|
||||
// and isAuthenticated() (the source of truth) will start returning true.
|
||||
await RootAPI.login(username, password);
|
||||
} catch (err) {
|
||||
if (err && err.response && err.response.status === 401) {
|
||||
this.setState({ validationError: true });
|
||||
} else {
|
||||
this.setState({ authenticationError: true });
|
||||
}
|
||||
} finally {
|
||||
this.setState({ isAuthenticating: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeUsername (value) {
|
||||
this.setState({ username: value, validationError: false });
|
||||
}
|
||||
|
||||
handleChangePassword (value) {
|
||||
this.setState({ password: value, validationError: false });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { username, password, isInputValid, isAuthenticated } = this.state;
|
||||
const { alt, loginInfo, logo, bodyText: errorMessage, i18n } = this.props;
|
||||
const logoSrc = logo ? `data:image/jpeg;${logo}` : logoImg;
|
||||
const {
|
||||
authenticationError,
|
||||
validationError,
|
||||
username,
|
||||
password,
|
||||
isLoading,
|
||||
logo,
|
||||
loginInfo,
|
||||
} = this.state;
|
||||
const { alt, i18n, isAuthenticated } = this.props;
|
||||
// Setting BrandName to a variable here is necessary to get the jest tests
|
||||
// passing. Attempting to use BrandName in the template literal results
|
||||
// in failing tests.
|
||||
const brandName = BrandName;
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAuthenticated()) {
|
||||
return (<Redirect to="/" />);
|
||||
}
|
||||
|
||||
let helperText;
|
||||
if (validationError) {
|
||||
helperText = i18n._(t`Invalid username or password. Please try again.`);
|
||||
} else {
|
||||
helperText = i18n._(t`There was a problem signing in. Please try again.`);
|
||||
}
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
brandImgSrc={logoSrc}
|
||||
brandImgSrc={logo}
|
||||
brandImgAlt={alt || brandName}
|
||||
loginTitle={i18n._(t`Welcome to Ansible ${brandName}! Please Sign In.`)}
|
||||
textContent={loginInfo}
|
||||
>
|
||||
<LoginForm
|
||||
className={errorMessage && 'pf-m-error'}
|
||||
className={(authenticationError || validationError) ? 'pf-m-error' : ''}
|
||||
usernameLabel={i18n._(t`Username`)}
|
||||
passwordLabel={i18n._(t`Password`)}
|
||||
showHelperText={!isInputValid || !!errorMessage}
|
||||
helperText={errorMessage || i18n._(t`Invalid username or password. Please try again.`)}
|
||||
showHelperText={(authenticationError || validationError)}
|
||||
helperText={helperText}
|
||||
usernameValue={username}
|
||||
passwordValue={password}
|
||||
isValidUsername={isInputValid}
|
||||
isValidPassword={isInputValid}
|
||||
onChangeUsername={this.onChangeUsername}
|
||||
onChangePassword={this.onChangePassword}
|
||||
onLoginButtonClick={this.onLoginButtonClick}
|
||||
isValidUsername={!validationError}
|
||||
isValidPassword={!validationError}
|
||||
onChangeUsername={this.handleChangeUsername}
|
||||
onChangePassword={this.handleChangePassword}
|
||||
onLoginButtonClick={this.handleLoginButtonClick}
|
||||
/>
|
||||
</LoginPage>
|
||||
);
|
||||
@@ -109,4 +148,4 @@ class AWXLogin extends Component {
|
||||
}
|
||||
|
||||
export { AWXLogin as _AWXLogin };
|
||||
export default withI18n()(withNetwork(withRootDialog(withRouter(AWXLogin))));
|
||||
export default withI18n()(withRouter(AWXLogin));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Route, withRouter, Switch } from 'react-router-dom';
|
||||
import { NetworkProvider } from '../../contexts/Network';
|
||||
import { withRootDialog } from '../../contexts/RootDialog';
|
||||
|
||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||
import TemplatesList from './TemplatesList';
|
||||
@@ -21,32 +19,12 @@ class Templates extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { match, history, setRootDialogMessage, i18n } = this.props;
|
||||
const { match } = this.props;
|
||||
const { breadcrumbConfig } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${match.path}/:templateType/:id`}
|
||||
render={({ match: newRouteMatch }) => (
|
||||
<NetworkProvider
|
||||
handle404={() => {
|
||||
history.replace('/templates');
|
||||
setRootDialogMessage({
|
||||
title: '404',
|
||||
bodyText: (
|
||||
<Fragment>
|
||||
{i18n._(t`Cannot find template with ID`)}
|
||||
<strong>{` ${newRouteMatch.params.id}`}</strong>
|
||||
</Fragment>
|
||||
),
|
||||
variant: 'warning'
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}`}
|
||||
render={() => (
|
||||
@@ -60,4 +38,4 @@ class Templates extends Component {
|
||||
}
|
||||
|
||||
export { Templates as _Templates };
|
||||
export default withI18n()(withRootDialog(withRouter(Templates)));
|
||||
export default withI18n()(withRouter(Templates));
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { withNetwork } from '../../contexts/Network';
|
||||
import { UnifiedJobTemplatesAPI } from '../../api';
|
||||
|
||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||
@@ -29,25 +28,25 @@ class TemplatesList extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
isLoading: true,
|
||||
isInitialized: false,
|
||||
contentError: false,
|
||||
contentLoading: true,
|
||||
selected: [],
|
||||
templates: [],
|
||||
itemCount: 0,
|
||||
};
|
||||
this.readUnifiedJobTemplates = this.readUnifiedJobTemplates.bind(this);
|
||||
this.loadUnifiedJobTemplates = this.loadUnifiedJobTemplates.bind(this);
|
||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.readUnifiedJobTemplates();
|
||||
this.loadUnifiedJobTemplates();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { location } = this.props;
|
||||
if (location !== prevProps.location) {
|
||||
this.readUnifiedJobTemplates();
|
||||
this.loadUnifiedJobTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,33 +65,29 @@ class TemplatesList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async readUnifiedJobTemplates () {
|
||||
const { handleHttpError, location } = this.props;
|
||||
this.setState({ error: false, isLoading: true });
|
||||
async loadUnifiedJobTemplates () {
|
||||
const { location } = this.props;
|
||||
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
|
||||
|
||||
this.setState({ contentError: false, contentLoading: true });
|
||||
try {
|
||||
const { data } = await UnifiedJobTemplatesAPI.read(params);
|
||||
const { count, results } = data;
|
||||
|
||||
const stateToUpdate = {
|
||||
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params);
|
||||
this.setState({
|
||||
itemCount: count,
|
||||
templates: results,
|
||||
selected: [],
|
||||
isInitialized: true,
|
||||
isLoading: false,
|
||||
};
|
||||
this.setState(stateToUpdate);
|
||||
});
|
||||
} catch (err) {
|
||||
handleHttpError(err) || this.setState({ error: true, isLoading: false });
|
||||
this.setState({ contentError: true });
|
||||
} finally {
|
||||
this.setState({ contentLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
error,
|
||||
isInitialized,
|
||||
isLoading,
|
||||
contentError,
|
||||
contentLoading,
|
||||
templates,
|
||||
itemCount,
|
||||
selected,
|
||||
@@ -106,44 +101,43 @@ class TemplatesList extends Component {
|
||||
return (
|
||||
<PageSection variant={medium}>
|
||||
<Card>
|
||||
{isInitialized && (
|
||||
<PaginatedDataList
|
||||
items={templates}
|
||||
itemCount={itemCount}
|
||||
itemName={i18n._(t`Template`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true },
|
||||
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true },
|
||||
]}
|
||||
renderToolbar={(props) => (
|
||||
<DatalistToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
showExpandCollapse
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
/>
|
||||
)}
|
||||
renderItem={(template) => (
|
||||
<TemplateListItem
|
||||
key={template.id}
|
||||
value={template.name}
|
||||
template={template}
|
||||
detailUrl={`${match.url}/${template.type}/${template.id}`}
|
||||
onSelect={() => this.handleSelect(template)}
|
||||
isSelected={selected.some(row => row.id === template.id)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isLoading ? <div>loading....</div> : ''}
|
||||
{error ? <div>error</div> : '' }
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
contentLoading={contentLoading}
|
||||
items={templates}
|
||||
itemCount={itemCount}
|
||||
itemName={i18n._(t`Template`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarColumns={[
|
||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true },
|
||||
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true },
|
||||
]}
|
||||
renderToolbar={(props) => (
|
||||
<DatalistToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
showExpandCollapse
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
/>
|
||||
)}
|
||||
renderItem={(template) => (
|
||||
<TemplateListItem
|
||||
key={template.id}
|
||||
value={template.name}
|
||||
template={template}
|
||||
detailUrl={`${match.url}/${template.type}/${template.id}`}
|
||||
onSelect={() => this.handleSelect(template)}
|
||||
isSelected={selected.some(row => row.id === template.id)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { TemplatesList as _TemplatesList };
|
||||
export default withI18n()(withNetwork(withRouter(TemplatesList)));
|
||||
export default withI18n()(withRouter(TemplatesList));
|
||||
|
||||
8
src/util/auth.js
Normal file
8
src/util/auth.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function isAuthenticated () {
|
||||
const parsed = (`; ${document.cookie}`).split('; userLoggedIn=');
|
||||
if (parsed.length === 2) {
|
||||
return parsed.pop().split(';').shift() === 'true';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user