mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 10:27:37 -02:30
Add RBAC to org views
This commit is contained in:
@@ -3,6 +3,7 @@ const API_LOGIN = `${API_ROOT}login/`;
|
|||||||
const API_LOGOUT = `${API_ROOT}logout/`;
|
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||||
const API_V2 = `${API_ROOT}v2/`;
|
const API_V2 = `${API_ROOT}v2/`;
|
||||||
const API_CONFIG = `${API_V2}config/`;
|
const API_CONFIG = `${API_V2}config/`;
|
||||||
|
const API_ME = `${API_V2}me/`;
|
||||||
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
||||||
const API_INSTANCE_GROUPS = `${API_V2}instance_groups/`;
|
const API_INSTANCE_GROUPS = `${API_V2}instance_groups/`;
|
||||||
const API_USERS = `${API_V2}users/`;
|
const API_USERS = `${API_V2}users/`;
|
||||||
@@ -58,6 +59,10 @@ class APIClient {
|
|||||||
return this.http.get(API_CONFIG);
|
return this.http.get(API_CONFIG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMe () {
|
||||||
|
return this.http.get(API_ME);
|
||||||
|
}
|
||||||
|
|
||||||
destroyOrganization (id) {
|
destroyOrganization (id) {
|
||||||
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||||
return (this.http.delete(endpoint));
|
return (this.http.delete(endpoint));
|
||||||
@@ -71,6 +76,10 @@ class APIClient {
|
|||||||
return this.http.post(API_ORGANIZATIONS, data);
|
return this.http.post(API_ORGANIZATIONS, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callOrganizations () {
|
||||||
|
return this.http.options(API_ORGANIZATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
getOrganizationAccessList (id, params = {}) {
|
getOrganizationAccessList (id, params = {}) {
|
||||||
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
|
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { I18n } from '@lingui/react';
|
import { I18n, i18nMark } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -28,23 +28,27 @@ import VerticalSeparator from '../VerticalSeparator';
|
|||||||
class DataListToolbar extends React.Component {
|
class DataListToolbar extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
|
add,
|
||||||
addUrl,
|
addUrl,
|
||||||
columns,
|
columns,
|
||||||
|
deleteTooltip,
|
||||||
disableTrashCanIcon,
|
disableTrashCanIcon,
|
||||||
onSelectAll,
|
|
||||||
sortedColumnKey,
|
|
||||||
sortOrder,
|
|
||||||
showDelete,
|
|
||||||
showSelectAll,
|
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
isCompact,
|
isCompact,
|
||||||
noLeftMargin,
|
noLeftMargin,
|
||||||
onSort,
|
onSort,
|
||||||
onSearch,
|
onSearch
|
||||||
onCompact,
|
onCompact,
|
||||||
onExpand,
|
onExpand,
|
||||||
add,
|
onOpenDeleteModal,
|
||||||
onOpenDeleteModal
|
onSearch,
|
||||||
|
onSelectAll,
|
||||||
|
onSort,
|
||||||
|
showAdd,
|
||||||
|
showDelete,
|
||||||
|
showSelectAll,
|
||||||
|
sortOrder,
|
||||||
|
sortedColumnKey
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const showExpandCollapse = (onCompact && onExpand);
|
const showExpandCollapse = (onCompact && onExpand);
|
||||||
@@ -112,21 +116,23 @@ class DataListToolbar extends React.Component {
|
|||||||
<LevelItem>
|
<LevelItem>
|
||||||
{ showDelete && (
|
{ showDelete && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={i18n._(t`Delete`)}
|
content={deleteTooltip}
|
||||||
position="top"
|
position="left"
|
||||||
>
|
>
|
||||||
<Button
|
<span>
|
||||||
className="awx-ToolBarBtn"
|
<Button
|
||||||
variant="plain"
|
className="awx-ToolBarBtn"
|
||||||
aria-label={i18n._(t`Delete`)}
|
variant="plain"
|
||||||
onClick={onOpenDeleteModal}
|
aria-label={i18n._(t`Delete`)}
|
||||||
isDisabled={disableTrashCanIcon}
|
onClick={onOpenDeleteModal}
|
||||||
>
|
isDisabled={disableTrashCanIcon}
|
||||||
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
|
>
|
||||||
</Button>
|
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{addUrl && (
|
{showAdd && addUrl && (
|
||||||
<Link to={addUrl}>
|
<Link to={addUrl}>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -136,7 +142,7 @@ class DataListToolbar extends React.Component {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{add && (
|
{showAdd && add && (
|
||||||
<Fragment>{add}</Fragment>
|
<Fragment>{add}</Fragment>
|
||||||
)}
|
)}
|
||||||
</LevelItem>
|
</LevelItem>
|
||||||
@@ -149,13 +155,16 @@ class DataListToolbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DataListToolbar.propTypes = {
|
DataListToolbar.propTypes = {
|
||||||
|
add: PropTypes.node,
|
||||||
addUrl: PropTypes.string,
|
addUrl: PropTypes.string,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
deleteTooltip: PropTypes.node,
|
||||||
isAllSelected: PropTypes.bool,
|
isAllSelected: PropTypes.bool,
|
||||||
noLeftMargin: PropTypes.bool,
|
noLeftMargin: PropTypes.bool,
|
||||||
onSearch: PropTypes.func,
|
onSearch: PropTypes.func,
|
||||||
onSelectAll: PropTypes.func,
|
onSelectAll: PropTypes.func,
|
||||||
onSort: PropTypes.func,
|
onSort: PropTypes.func,
|
||||||
|
showAdd: PropTypes.bool,
|
||||||
showDelete: PropTypes.bool,
|
showDelete: PropTypes.bool,
|
||||||
showSelectAll: PropTypes.bool,
|
showSelectAll: PropTypes.bool,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
@@ -167,10 +176,17 @@ DataListToolbar.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
DataListToolbar.defaultProps = {
|
DataListToolbar.defaultProps = {
|
||||||
|
add: null,
|
||||||
addUrl: null,
|
addUrl: null,
|
||||||
|
deleteTooltip: i18nMark('Delete'),
|
||||||
|
isAllSelected: false,
|
||||||
|
isCompact: false,
|
||||||
|
onCompact: null,
|
||||||
|
onExpand: null,
|
||||||
onSearch: null,
|
onSearch: null,
|
||||||
onSelectAll: null,
|
onSelectAll: null,
|
||||||
onSort: null,
|
onSort: null,
|
||||||
|
showAdd: false,
|
||||||
showDelete: false,
|
showDelete: false,
|
||||||
showSelectAll: false,
|
showSelectAll: false,
|
||||||
sortOrder: 'ascending',
|
sortOrder: 'ascending',
|
||||||
|
|||||||
@@ -74,7 +74,10 @@
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-left: 20px;
|
}
|
||||||
|
|
||||||
|
.awx-toolbar .pf-c-button {
|
||||||
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
|
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
class NotificationListItem extends React.Component {
|
class NotificationListItem extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
|
canToggleNotifications,
|
||||||
itemId,
|
itemId,
|
||||||
name,
|
name,
|
||||||
notificationType,
|
notificationType,
|
||||||
@@ -49,12 +50,14 @@ class NotificationListItem extends React.Component {
|
|||||||
<Switch
|
<Switch
|
||||||
label={i18n._(t`Successful`)}
|
label={i18n._(t`Successful`)}
|
||||||
isChecked={successTurnedOn}
|
isChecked={successTurnedOn}
|
||||||
|
isDisabled={!canToggleNotifications}
|
||||||
onChange={() => toggleNotification(itemId, successTurnedOn, 'success')}
|
onChange={() => toggleNotification(itemId, successTurnedOn, 'success')}
|
||||||
aria-label={i18n._(t`Notification success toggle`)}
|
aria-label={i18n._(t`Notification success toggle`)}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
label={i18n._(t`Failure`)}
|
label={i18n._(t`Failure`)}
|
||||||
isChecked={errorTurnedOn}
|
isChecked={errorTurnedOn}
|
||||||
|
isDisabled={!canToggleNotifications}
|
||||||
onChange={() => toggleNotification(itemId, errorTurnedOn, 'error')}
|
onChange={() => toggleNotification(itemId, errorTurnedOn, 'error')}
|
||||||
aria-label={i18n._(t`Notification failure toggle`)}
|
aria-label={i18n._(t`Notification failure toggle`)}
|
||||||
/>
|
/>
|
||||||
@@ -67,6 +70,7 @@ class NotificationListItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationListItem.propTypes = {
|
NotificationListItem.propTypes = {
|
||||||
|
canToggleNotifications: PropTypes.bool.isRequired,
|
||||||
detailUrl: PropTypes.string.isRequired,
|
detailUrl: PropTypes.string.isRequired,
|
||||||
errorTurnedOn: PropTypes.bool,
|
errorTurnedOn: PropTypes.bool,
|
||||||
itemId: PropTypes.number.isRequired,
|
itemId: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ class Notifications extends Component {
|
|||||||
successTemplateIds,
|
successTemplateIds,
|
||||||
errorTemplateIds
|
errorTemplateIds
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
const { canToggleNotifications } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{noInitialResults && (
|
{noInitialResults && (
|
||||||
@@ -315,6 +316,7 @@ class Notifications extends Component {
|
|||||||
toggleNotification={this.toggleNotification}
|
toggleNotification={this.toggleNotification}
|
||||||
errorTurnedOn={errorTemplateIds.includes(o.id)}
|
errorTurnedOn={errorTemplateIds.includes(o.id)}
|
||||||
successTurnedOn={successTemplateIds.includes(o.id)}
|
successTurnedOn={successTemplateIds.includes(o.id)}
|
||||||
|
canToggleNotifications={canToggleNotifications}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -337,6 +339,7 @@ class Notifications extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Notifications.propTypes = {
|
Notifications.propTypes = {
|
||||||
|
canToggleNotifications: PropTypes.bool.isRequired,
|
||||||
onReadError: PropTypes.func.isRequired,
|
onReadError: PropTypes.func.isRequired,
|
||||||
onReadNotifications: PropTypes.func.isRequired,
|
onReadNotifications: PropTypes.func.isRequired,
|
||||||
onReadSuccess: PropTypes.func.isRequired,
|
onReadSuccess: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ class Provider extends Component {
|
|||||||
version: null,
|
version: null,
|
||||||
custom_logo: null,
|
custom_logo: null,
|
||||||
custom_login_info: null,
|
custom_login_info: null,
|
||||||
|
me: {},
|
||||||
...props.value
|
...props.value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fetchConfig = this.fetchConfig.bind(this);
|
this.fetchConfig = this.fetchConfig.bind(this);
|
||||||
|
this.fetchMe = this.fetchMe.bind(this);
|
||||||
|
this.updateConfig = this.updateConfig.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -30,6 +33,47 @@ class Provider extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateConfig = (config) => {
|
||||||
|
const {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
this.setState(prevState => ({
|
||||||
|
value: {
|
||||||
|
...prevState.value,
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMe () {
|
||||||
|
const { api, handleHttpError } = this.props;
|
||||||
|
try {
|
||||||
|
const { data: { results: [me] } } = await api.getMe();
|
||||||
|
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 () {
|
async fetchConfig () {
|
||||||
const { api, handleHttpError } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
|
|
||||||
@@ -47,13 +91,15 @@ class Provider extends Component {
|
|||||||
custom_login_info
|
custom_login_info
|
||||||
}
|
}
|
||||||
} = await api.getRoot();
|
} = await api.getRoot();
|
||||||
|
const { data: { results: [me] } } = await api.getMe();
|
||||||
this.setState({
|
this.setState({
|
||||||
value: {
|
value: {
|
||||||
ansible_version,
|
ansible_version,
|
||||||
custom_virtualenvs,
|
custom_virtualenvs,
|
||||||
version,
|
version,
|
||||||
custom_logo,
|
custom_logo,
|
||||||
custom_login_info
|
custom_login_info,
|
||||||
|
me
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -63,7 +109,8 @@ class Provider extends Component {
|
|||||||
custom_virtualenvs: null,
|
custom_virtualenvs: null,
|
||||||
version: null,
|
version: null,
|
||||||
custom_logo: null,
|
custom_logo: null,
|
||||||
custom_login_info: null
|
custom_login_info: null,
|
||||||
|
me: {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -75,7 +122,13 @@ class Provider extends Component {
|
|||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigContext.Provider value={value}>
|
<ConfigContext.Provider
|
||||||
|
value={{
|
||||||
|
...(propsValue || stateValue),
|
||||||
|
fetchMe: this.fetchMe,
|
||||||
|
updateConfig: this.updateConfig
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ export function main (render) {
|
|||||||
path="/login"
|
path="/login"
|
||||||
render={() => (
|
render={() => (
|
||||||
<Config>
|
<Config>
|
||||||
{({ custom_logo, custom_login_info }) => (
|
{({ custom_logo, custom_login_info, fetchMe, updateConfig }) => (
|
||||||
<Login
|
<Login
|
||||||
logo={custom_logo}
|
logo={custom_logo}
|
||||||
loginInfo={custom_login_info}
|
loginInfo={custom_login_info}
|
||||||
|
fetchMe={fetchMe}
|
||||||
|
updateConfig={updateConfig}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AWXLogin extends Component {
|
|||||||
|
|
||||||
async onLoginButtonClick (event) {
|
async onLoginButtonClick (event) {
|
||||||
const { username, password, isLoading } = this.state;
|
const { username, password, isLoading } = this.state;
|
||||||
const { api, handleHttpError, clearRootDialogMessage } = this.props;
|
const { api, handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -51,7 +51,9 @@ class AWXLogin extends Component {
|
|||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.login(username, password);
|
const { data } = await api.login(username, password);
|
||||||
|
updateConfig(data);
|
||||||
|
fetchMe();
|
||||||
this.setState({ isAuthenticated: true, isLoading: false });
|
this.setState({ isAuthenticated: true, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Route, withRouter, Switch } from 'react-router-dom';
|
|||||||
import { i18nMark } from '@lingui/react';
|
import { i18nMark } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { Config } from '../../contexts/Config';
|
||||||
import { NetworkProvider } from '../../contexts/Network';
|
import { NetworkProvider } from '../../contexts/Network';
|
||||||
import { withRootDialog } from '../../contexts/RootDialog';
|
import { withRootDialog } from '../../contexts/RootDialog';
|
||||||
|
|
||||||
@@ -74,11 +75,16 @@ class Organizations extends Component {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Organization
|
<Config>
|
||||||
history={history}
|
{({ me }) => (
|
||||||
location={location}
|
<Organization
|
||||||
setBreadcrumb={this.setBreadcrumbConfig}
|
history={history}
|
||||||
/>
|
location={location}
|
||||||
|
setBreadcrumb={this.setBreadcrumbConfig}
|
||||||
|
me={me || {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Config>
|
||||||
</NetworkProvider>
|
</NetworkProvider>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,9 +18,14 @@ import {
|
|||||||
withRouter
|
withRouter
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
import { withNetwork } from '../../../contexts/Network';
|
import { withNetwork } from '../../../contexts/Network';
|
||||||
|
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import BasicChip from '../../../components/BasicChip/BasicChip';
|
||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import AddResourceRole from '../../../components/AddRole/AddResourceRole';
|
import AddResourceRole from '../../../components/AddRole/AddResourceRole';
|
||||||
@@ -357,6 +362,7 @@ class OrganizationAccessList extends React.Component {
|
|||||||
columns={this.columns}
|
columns={this.columns}
|
||||||
onSearch={() => { }}
|
onSearch={() => { }}
|
||||||
onSort={this.onSort}
|
onSort={this.onSort}
|
||||||
|
showAdd={organization.summary_fields.user_capabilities.edit}
|
||||||
add={(
|
add={(
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
@@ -421,13 +427,21 @@ class OrganizationAccessList extends React.Component {
|
|||||||
<ul style={userRolesWrapperStyle}>
|
<ul style={userRolesWrapperStyle}>
|
||||||
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
|
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
|
||||||
{result.userRoles.map(role => (
|
{result.userRoles.map(role => (
|
||||||
<Chip
|
role.user_capabilities.unattach ? (
|
||||||
key={role.id}
|
<Chip
|
||||||
className="awx-c-chip"
|
key={role.id}
|
||||||
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
className="awx-c-chip"
|
||||||
>
|
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
||||||
{role.name}
|
>
|
||||||
</Chip>
|
{role.name}
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<BasicChip
|
||||||
|
key={role.id}
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</BasicChip>
|
||||||
|
)
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,13 +33,17 @@ class Organization extends Component {
|
|||||||
organization: null,
|
organization: null,
|
||||||
error: false,
|
error: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
isNotifAdmin: false,
|
||||||
|
isAuditorOfThisOrg: false,
|
||||||
|
isAdminOfThisOrg: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fetchOrganization = this.fetchOrganization.bind(this);
|
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||||
|
this.fetchOrganizationAndRoles = this.fetchOrganizationAndRoles.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.fetchOrganization();
|
this.fetchOrganizationAndRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate (prevProps) {
|
async componentDidUpdate (prevProps) {
|
||||||
@@ -49,6 +53,43 @@ class Organization extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchOrganizationAndRoles () {
|
||||||
|
const {
|
||||||
|
match,
|
||||||
|
setBreadcrumb,
|
||||||
|
api,
|
||||||
|
handleHttpError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [{ data }, notifAdminRest, auditorRes, adminRes] = await Promise.all([
|
||||||
|
api.getOrganizationDetails(parseInt(match.params.id, 10)),
|
||||||
|
api.getOrganizations({
|
||||||
|
role_level: 'notification_admin_role',
|
||||||
|
page_size: 1
|
||||||
|
}),
|
||||||
|
api.getOrganizations({
|
||||||
|
role_level: 'auditor_role',
|
||||||
|
id: parseInt(match.params.id, 10)
|
||||||
|
}),
|
||||||
|
api.getOrganizations({
|
||||||
|
role_level: 'admin_role',
|
||||||
|
id: parseInt(match.params.id, 10)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
setBreadcrumb(data);
|
||||||
|
this.setState({
|
||||||
|
organization: data,
|
||||||
|
loading: false,
|
||||||
|
isNotifAdmin: notifAdminRest.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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchOrganization () {
|
async fetchOrganization () {
|
||||||
const {
|
const {
|
||||||
match,
|
match,
|
||||||
@@ -70,19 +111,40 @@ class Organization extends Component {
|
|||||||
const {
|
const {
|
||||||
location,
|
location,
|
||||||
match,
|
match,
|
||||||
|
me,
|
||||||
history
|
history
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
organization,
|
organization,
|
||||||
error,
|
error,
|
||||||
loading
|
loading,
|
||||||
|
isNotifAdmin,
|
||||||
|
isAuditorOfThisOrg,
|
||||||
|
isAdminOfThisOrg
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const tabsPaddingOverride = {
|
const tabsPaddingOverride = {
|
||||||
padding: '0'
|
padding: '0'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg;
|
||||||
|
const canToggleNotifications = isNotifAdmin && (
|
||||||
|
me.is_system_auditor
|
||||||
|
|| isAuditorOfThisOrg
|
||||||
|
|| isAdminOfThisOrg
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabElements = [
|
||||||
|
{ name: i18nMark('Details'), link: `${match.url}/details` },
|
||||||
|
{ name: i18nMark('Access'), link: `${match.url}/access` },
|
||||||
|
{ name: i18nMark('Teams'), link: `${match.url}/teams` }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (canSeeNotificationsTab) {
|
||||||
|
tabElements.push({ name: i18nMark('Notifications'), link: `${match.url}/notifications` });
|
||||||
|
}
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
loading ? ''
|
loading ? ''
|
||||||
: (
|
: (
|
||||||
@@ -174,16 +236,19 @@ class Organization extends Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
{canSeeNotificationsTab && (
|
||||||
path="/organizations/:id/notifications"
|
<Route
|
||||||
render={() => (
|
path="/organizations/:id/notifications"
|
||||||
<OrganizationNotifications
|
render={() => (
|
||||||
match={match}
|
<OrganizationNotifications
|
||||||
location={location}
|
match={match}
|
||||||
history={history}
|
location={location}
|
||||||
/>
|
history={history}
|
||||||
)}
|
canToggleNotifications={canToggleNotifications}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
||||||
</Switch>
|
</Switch>
|
||||||
{error ? 'error!' : ''}
|
{error ? 'error!' : ''}
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ class OrganizationDetail extends Component {
|
|||||||
description,
|
description,
|
||||||
custom_virtualenv,
|
custom_virtualenv,
|
||||||
created,
|
created,
|
||||||
modified
|
modified,
|
||||||
|
summary_fields
|
||||||
},
|
},
|
||||||
match
|
match
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -165,11 +166,13 @@ class OrganizationDetail extends Component {
|
|||||||
</TextContent>
|
</TextContent>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
{summary_fields.user_capabilities.edit && (
|
||||||
<Link to={`/organizations/${match.params.id}/edit`}>
|
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
||||||
<Button><Trans>Edit</Trans></Button>
|
<Link to={`/organizations/${match.params.id}/edit`}>
|
||||||
</Link>
|
<Button><Trans>Edit</Trans></Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{error ? 'error!' : ''}
|
{error ? 'error!' : ''}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,13 +41,18 @@ class OrganizationNotifications extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const {
|
||||||
|
canToggleNotifications
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationsList
|
<NotificationsList
|
||||||
|
canToggleNotifications={canToggleNotifications}
|
||||||
|
onCreateError={this.createOrgNotificationError}
|
||||||
|
onCreateSuccess={this.createOrgNotificationSuccess}
|
||||||
|
onReadError={this.readOrgNotificationError}
|
||||||
onReadNotifications={this.readOrgNotifications}
|
onReadNotifications={this.readOrgNotifications}
|
||||||
onReadSuccess={this.readOrgNotificationSuccess}
|
onReadSuccess={this.readOrgNotificationSuccess}
|
||||||
onReadError={this.readOrgNotificationError}
|
|
||||||
onCreateSuccess={this.createOrgNotificationSuccess}
|
|
||||||
onCreateError={this.createOrgNotificationError}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ class OrganizationsList extends Component {
|
|||||||
loading: true,
|
loading: true,
|
||||||
results: [],
|
results: [],
|
||||||
selected: [],
|
selected: [],
|
||||||
isModalOpen: false,
|
isModalOpen: false
|
||||||
orgsToDelete: [],
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,14 +73,16 @@ class OrganizationsList extends Component {
|
|||||||
this.onSelectAll = this.onSelectAll.bind(this);
|
this.onSelectAll = this.onSelectAll.bind(this);
|
||||||
this.onSelect = this.onSelect.bind(this);
|
this.onSelect = this.onSelect.bind(this);
|
||||||
this.updateUrl = this.updateUrl.bind(this);
|
this.updateUrl = this.updateUrl.bind(this);
|
||||||
|
this.callOrganizations = this.callOrganizations.bind(this);
|
||||||
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||||
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
||||||
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
|
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
|
||||||
this.handleClearOrgsToDelete = this.handleClearOrgsToDelete.bind(this);
|
this.handleCloseOrgDeleteModal = this.handleCloseOrgDeleteModal.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const queryParams = this.getQueryParams();
|
const queryParams = this.getQueryParams();
|
||||||
|
this.callOrganizations();
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,20 +118,20 @@ class OrganizationsList extends Component {
|
|||||||
onSelectAll (isSelected) {
|
onSelectAll (isSelected) {
|
||||||
const { results } = this.state;
|
const { results } = this.state;
|
||||||
|
|
||||||
const selected = isSelected ? results.map(o => o.id) : [];
|
const selected = isSelected ? results : [];
|
||||||
|
|
||||||
this.setState({ selected });
|
this.setState({ selected });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect (id) {
|
onSelect (row) {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
|
|
||||||
const isSelected = selected.includes(id);
|
const isSelected = selected.some(s => s.id === row.id);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
this.setState({ selected: selected.filter(s => s !== id) });
|
this.setState({ selected: selected.filter(s => s.id !== row.id) });
|
||||||
} else {
|
} else {
|
||||||
this.setState({ selected: selected.concat(id) });
|
this.setState({ selected: selected.concat(row) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,43 +144,35 @@ class OrganizationsList extends Component {
|
|||||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearOrgsToDelete () {
|
handleCloseOrgDeleteModal () {
|
||||||
this.setState({
|
this.setState({
|
||||||
isModalOpen: false,
|
isModalOpen: false
|
||||||
orgsToDelete: []
|
|
||||||
});
|
});
|
||||||
this.onSelectAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenOrgDeleteModal () {
|
handleOpenOrgDeleteModal () {
|
||||||
const { results, selected } = this.state;
|
const { selected } = this.state;
|
||||||
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
||||||
const warningMsg = i18nMark('Are you sure you want to delete:');
|
const warningMsg = i18nMark('Are you sure you want to delete:');
|
||||||
|
|
||||||
const orgsToDelete = [];
|
|
||||||
results.forEach((result) => {
|
|
||||||
selected.forEach((selectedOrg) => {
|
|
||||||
if (result.id === selectedOrg) {
|
|
||||||
orgsToDelete.push({ name: result.name, id: selectedOrg });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.setState({
|
this.setState({
|
||||||
orgsToDelete,
|
|
||||||
isModalOpen: true,
|
isModalOpen: true,
|
||||||
warningTitle,
|
warningTitle,
|
||||||
warningMsg,
|
warningMsg,
|
||||||
loading: false });
|
loading: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleOrgDelete (event) {
|
async handleOrgDelete () {
|
||||||
const { orgsToDelete } = this.state;
|
const { selected } = this.state;
|
||||||
const { api, handleHttpError } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
let errorHandled;
|
let errorHandled;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(orgsToDelete.map((org) => api.destroyOrganization(org.id)));
|
await Promise.all(selected.map((org) => api.destroyOrganization(org.id)));
|
||||||
this.handleClearOrgsToDelete();
|
this.setState({
|
||||||
|
isModalOpen: false,
|
||||||
|
selected: []
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorHandled = handleHttpError(err);
|
errorHandled = handleHttpError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -188,7 +181,6 @@ class OrganizationsList extends Component {
|
|||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl (queryParams) {
|
updateUrl (queryParams) {
|
||||||
@@ -248,16 +240,35 @@ class OrganizationsList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async callOrganizations () {
|
||||||
|
const { api } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await api.callOrganizations();
|
||||||
|
const { actions } = data;
|
||||||
|
|
||||||
|
const stateToUpdate = {
|
||||||
|
canAdd: Object.prototype.hasOwnProperty.call(actions, 'POST')
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState(stateToUpdate);
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
medium,
|
medium,
|
||||||
} = PageSectionVariants;
|
} = PageSectionVariants;
|
||||||
const {
|
const {
|
||||||
|
canAdd,
|
||||||
count,
|
count,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
noInitialResults,
|
noInitialResults,
|
||||||
orgsToDelete,
|
|
||||||
page,
|
page,
|
||||||
pageCount,
|
pageCount,
|
||||||
page_size,
|
page_size,
|
||||||
@@ -270,6 +281,12 @@ class OrganizationsList extends Component {
|
|||||||
warningMsg,
|
warningMsg,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { match } = this.props;
|
const { match } = this.props;
|
||||||
|
|
||||||
|
const disableDelete = (
|
||||||
|
selected.length === 0
|
||||||
|
|| selected.some(row => !row.summary_fields.user_capabilities.delete)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
@@ -280,15 +297,15 @@ class OrganizationsList extends Component {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
title={warningTitle}
|
title={warningTitle}
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={this.handleClearOrgsToDelete}
|
onClose={this.handleCloseOrgDeleteModal}
|
||||||
actions={[
|
actions={[
|
||||||
<Button variant="danger" key="delete" aria-label="confirm-delete" onClick={this.handleOrgDelete}>{i18n._(t`Delete`)}</Button>,
|
<Button variant="danger" key="delete" aria-label="confirm-delete" onClick={this.handleOrgDelete}>{i18n._(t`Delete`)}</Button>,
|
||||||
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleClearOrgsToDelete}>{i18n._(t`Cancel`)}</Button>
|
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleCloseOrgDeleteModal}>{i18n._(t`Cancel`)}</Button>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{warningMsg}
|
{warningMsg}
|
||||||
<br />
|
<br />
|
||||||
{orgsToDelete.map((org) => (
|
{selected.map((org) => (
|
||||||
<span key={org.id}>
|
<span key={org.id}>
|
||||||
<strong>
|
<strong>
|
||||||
{org.name}
|
{org.name}
|
||||||
@@ -321,9 +338,24 @@ class OrganizationsList extends Component {
|
|||||||
onSort={this.onSort}
|
onSort={this.onSort}
|
||||||
onSelectAll={this.onSelectAll}
|
onSelectAll={this.onSelectAll}
|
||||||
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
||||||
disableTrashCanIcon={selected.length === 0}
|
disableTrashCanIcon={disableDelete}
|
||||||
|
deleteTooltip={
|
||||||
|
selected.some(row => !row.summary_fields.user_capabilities.delete) ? (
|
||||||
|
<div>
|
||||||
|
<Trans>
|
||||||
|
You dont have permission to delete the following Organizations:
|
||||||
|
</Trans>
|
||||||
|
{selected.map(row => (
|
||||||
|
<div key={row.id}>
|
||||||
|
{row.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
showDelete
|
showDelete
|
||||||
showSelectAll
|
showSelectAll
|
||||||
|
showAdd={canAdd}
|
||||||
/>
|
/>
|
||||||
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
||||||
{ results.map(o => (
|
{ results.map(o => (
|
||||||
@@ -334,8 +366,8 @@ class OrganizationsList extends Component {
|
|||||||
detailUrl={`${match.url}/${o.id}`}
|
detailUrl={`${match.url}/${o.id}`}
|
||||||
memberCount={o.summary_fields.related_field_counts.users}
|
memberCount={o.summary_fields.related_field_counts.users}
|
||||||
teamCount={o.summary_fields.related_field_counts.teams}
|
teamCount={o.summary_fields.related_field_counts.teams}
|
||||||
isSelected={selected.includes(o.id)}
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
onSelect={() => this.onSelect(o.id, o.name)}
|
onSelect={() => this.onSelect(o)}
|
||||||
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
|
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user