mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 11:20:39 -03:30
Merge pull request #173 from mabashian/151-org-rbac
Add RBAC to org views (now with tests!)
This commit is contained in:
commit
3c06c97c32
@ -171,6 +171,7 @@ Here are the guidelines for how to name functions.
|
||||
|`replace<x>`| Use for methods that make API `PUT` requests |
|
||||
|`disassociate<x>`| Use for methods that pass `{ disassociate: true }` as a data param to an endpoint |
|
||||
|`associate<x>`| Use for methods that pass a resource id as a data param to an endpoint |
|
||||
|`can<x>`| Use for props dealing with RBAC to denote whether a user has access to something |
|
||||
|
||||
### Default State Initialization
|
||||
When declaring empty initial states, prefer the following instead of leaving them undefined:
|
||||
|
||||
@ -11,6 +11,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={() => {}}
|
||||
onCreateError={() => {}}
|
||||
onCreateSuccess={() => {}}
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -24,6 +25,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={() => {}}
|
||||
onCreateError={() => {}}
|
||||
onCreateSuccess={() => {}}
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
@ -38,6 +40,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={() => {}}
|
||||
onCreateError={() => {}}
|
||||
onCreateSuccess={() => {}}
|
||||
canToggleNotifications
|
||||
/>
|
||||
).find('Notifications');
|
||||
wrapper.instance().toggleNotification(1, true, 'success');
|
||||
@ -56,6 +59,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={() => {}}
|
||||
onCreateError={() => {}}
|
||||
onCreateSuccess={onCreateSuccess}
|
||||
canToggleNotifications
|
||||
/>
|
||||
).find('Notifications');
|
||||
wrapper.setState({ successTemplateIds: [44] });
|
||||
@ -76,6 +80,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={() => {}}
|
||||
onCreateError={() => {}}
|
||||
onCreateSuccess={() => {}}
|
||||
canToggleNotifications
|
||||
/>
|
||||
).find('Notifications');
|
||||
wrapper.instance().toggleNotification(1, true, 'error');
|
||||
@ -94,6 +99,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={() => {}}
|
||||
onCreateError={onCreateError}
|
||||
onCreateSuccess={() => {}}
|
||||
canToggleNotifications
|
||||
/>
|
||||
).find('Notifications');
|
||||
wrapper.setState({ errorTemplateIds: [44] });
|
||||
@ -144,6 +150,7 @@ describe('<Notifications />', () => {
|
||||
onReadSuccess={onReadSuccess}
|
||||
onCreateError={() => {}}
|
||||
onCreateSuccess={() => {}}
|
||||
canToggleNotifications
|
||||
/>
|
||||
).find('Notifications');
|
||||
wrapper.instance().updateUrl = jest.fn();
|
||||
|
||||
@ -20,6 +20,7 @@ describe('<NotificationListItem />', () => {
|
||||
toggleNotification={toggleNotification}
|
||||
detailUrl="/foo"
|
||||
notificationType="slack"
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
expect(wrapper.length).toBe(1);
|
||||
@ -33,6 +34,7 @@ describe('<NotificationListItem />', () => {
|
||||
toggleNotification={toggleNotification}
|
||||
detailUrl="/foo"
|
||||
notificationType="slack"
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
wrapper.find('Switch').first().find('input').simulate('change');
|
||||
@ -47,6 +49,7 @@ describe('<NotificationListItem />', () => {
|
||||
toggleNotification={toggleNotification}
|
||||
detailUrl="/foo"
|
||||
notificationType="slack"
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
wrapper.find('Switch').first().find('input').simulate('change');
|
||||
@ -61,6 +64,7 @@ describe('<NotificationListItem />', () => {
|
||||
toggleNotification={toggleNotification}
|
||||
detailUrl="/foo"
|
||||
notificationType="slack"
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
wrapper.find('Switch').at(1).find('input').simulate('change');
|
||||
@ -75,6 +79,7 @@ describe('<NotificationListItem />', () => {
|
||||
toggleNotification={toggleNotification}
|
||||
detailUrl="/foo"
|
||||
notificationType="slack"
|
||||
canToggleNotifications
|
||||
/>
|
||||
);
|
||||
wrapper.find('Switch').at(1).find('input').simulate('change');
|
||||
|
||||
@ -16,13 +16,31 @@ const mockData = [
|
||||
role: {
|
||||
name: 'foo',
|
||||
id: 2,
|
||||
user_capabilities: {
|
||||
unattach: true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const organization = {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
summary_fields: {
|
||||
object_roles: {},
|
||||
user_capabilities: {
|
||||
edit: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const api = {
|
||||
foo: () => {}
|
||||
};
|
||||
|
||||
describe('<OrganizationAccessList />', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
@ -33,7 +51,8 @@ describe('<OrganizationAccessList />', () => {
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => {}}
|
||||
removeRole={() => {}}
|
||||
/>
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
);
|
||||
});
|
||||
|
||||
@ -42,7 +61,8 @@ describe('<OrganizationAccessList />', () => {
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
/>
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
|
||||
setImmediate(() => {
|
||||
@ -57,7 +77,8 @@ describe('<OrganizationAccessList />', () => {
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
/>
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
expect(onSort).not.toHaveBeenCalled();
|
||||
|
||||
@ -74,7 +95,8 @@ describe('<OrganizationAccessList />', () => {
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
/>
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
|
||||
setImmediate(() => {
|
||||
@ -94,7 +116,8 @@ describe('<OrganizationAccessList />', () => {
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
/>
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
expect(handleWarning).not.toHaveBeenCalled();
|
||||
expect(confirmDelete).not.toHaveBeenCalled();
|
||||
@ -117,7 +140,8 @@ describe('<OrganizationAccessList />', () => {
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
/>
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
|
||||
setImmediate(() => {
|
||||
@ -146,4 +170,36 @@ describe('<OrganizationAccessList />', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('add role button visible for user that can edit org', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
organization={organization}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
|
||||
setImmediate(() => {
|
||||
const addRole = wrapper.update().find('DataListToolbar').find('PlusIcon');
|
||||
expect(addRole.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('add role button hidden for user that cannot edit org', () => {
|
||||
const readOnlyOrg = { ...organization };
|
||||
readOnlyOrg.summary_fields.user_capabilities.edit = false;
|
||||
const wrapper = mountWithContexts(
|
||||
<OrganizationAccessList
|
||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||
removeRole={() => {}}
|
||||
organization={readOnlyOrg}
|
||||
/>, { context: { network: { api } } }
|
||||
).find('OrganizationAccessList');
|
||||
|
||||
setImmediate(() => {
|
||||
const addRole = wrapper.update().find('DataListToolbar').find('PlusIcon');
|
||||
expect(addRole.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,7 +3,21 @@ import { mountWithContexts } from '../../../../enzymeHelpers';
|
||||
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
|
||||
|
||||
describe('<OrganizationView />', () => {
|
||||
const me = {
|
||||
is_super_user: true,
|
||||
is_system_auditor: false
|
||||
};
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(<Organization />);
|
||||
mountWithContexts(<Organization me={me} />);
|
||||
});
|
||||
test('notifications tab shown/hidden based on permissions', () => {
|
||||
const wrapper = mountWithContexts(<Organization me={me} />);
|
||||
expect(wrapper.find('.pf-c-tabs__item').length).toBe(3);
|
||||
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(0);
|
||||
wrapper.find('Organization').setState({
|
||||
isNotifAdmin: true
|
||||
});
|
||||
expect(wrapper.find('.pf-c-tabs__item').length).toBe(4);
|
||||
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,8 +3,12 @@ import { mountWithContexts } from '../../../../enzymeHelpers';
|
||||
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
|
||||
|
||||
describe('<OrganizationAccess />', () => {
|
||||
const organization = {
|
||||
id: 1,
|
||||
name: 'Default'
|
||||
};
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(<OrganizationAccess />);
|
||||
mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||
});
|
||||
|
||||
test('passed methods as props are called appropriately', async () => {
|
||||
@ -14,13 +18,14 @@ describe('<OrganizationAccess />', () => {
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
};
|
||||
const wrapper = mountWithContexts(<OrganizationAccess />, { context: { network: {
|
||||
api: {
|
||||
getOrganizationAccessList: () => Promise.resolve(mockAPIAccessList),
|
||||
disassociate: () => Promise.resolve(mockResponse)
|
||||
},
|
||||
handleHttpError: () => {}
|
||||
} } }).find('OrganizationAccess');
|
||||
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />,
|
||||
{ context: { network: {
|
||||
api: {
|
||||
getOrganizationAccessList: () => Promise.resolve(mockAPIAccessList),
|
||||
disassociate: () => Promise.resolve(mockResponse)
|
||||
},
|
||||
handleHttpError: () => {}
|
||||
} } }).find('OrganizationAccess');
|
||||
const accessList = await wrapper.instance().getOrgAccessList();
|
||||
expect(accessList).toEqual(mockAPIAccessList);
|
||||
const resp = await wrapper.instance().removeRole(2, 3, 'users');
|
||||
|
||||
@ -8,7 +8,12 @@ describe('<OrganizationDetail />', () => {
|
||||
description: 'Bar',
|
||||
custom_virtualenv: 'Fizz',
|
||||
created: 'Bat',
|
||||
modified: 'Boo'
|
||||
modified: 'Boo',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
@ -83,4 +88,28 @@ describe('<OrganizationDetail />', () => {
|
||||
expect(modifiedDetail.find('h6').text()).toBe('Last Modified');
|
||||
expect(modifiedDetail.find('p').text()).toBe('Boo');
|
||||
});
|
||||
|
||||
test('should show edit button for users with edit permission', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<OrganizationDetail
|
||||
organization={mockDetails}
|
||||
/>
|
||||
).find('OrganizationDetail');
|
||||
|
||||
const editLink = wrapper.findWhere(node => node.props().to === '/organizations/undefined/edit');
|
||||
expect(editLink.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should hide edit button for users without edit permission', () => {
|
||||
const readOnlyOrg = { ...mockDetails };
|
||||
readOnlyOrg.summary_fields.user_capabilities.edit = false;
|
||||
const wrapper = mountWithContexts(
|
||||
<OrganizationDetail
|
||||
organization={readOnlyOrg}
|
||||
/>
|
||||
).find('OrganizationDetail');
|
||||
|
||||
const editLink = wrapper.findWhere(node => node.props().to === '/organizations/undefined/edit');
|
||||
expect(editLink.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ describe('<OrganizationNotifications />', () => {
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(
|
||||
<OrganizationNotifications />, { context: { network: {
|
||||
<OrganizationNotifications canToggleNotifications />, { context: { network: {
|
||||
api,
|
||||
handleHttpError: () => {}
|
||||
} } }
|
||||
@ -26,7 +26,7 @@ describe('<OrganizationNotifications />', () => {
|
||||
});
|
||||
test('handles api requests', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<OrganizationNotifications />, { context: { network: {
|
||||
<OrganizationNotifications canToggleNotifications />, { context: { network: {
|
||||
api,
|
||||
handleHttpError: () => {}
|
||||
} } }
|
||||
|
||||
@ -3,6 +3,7 @@ const API_LOGIN = `${API_ROOT}login/`;
|
||||
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||
const API_V2 = `${API_ROOT}v2/`;
|
||||
const API_CONFIG = `${API_V2}config/`;
|
||||
const API_ME = `${API_V2}me/`;
|
||||
const API_ORGANIZATIONS = `${API_V2}organizations/`;
|
||||
const API_INSTANCE_GROUPS = `${API_V2}instance_groups/`;
|
||||
const API_USERS = `${API_V2}users/`;
|
||||
@ -58,6 +59,10 @@ class APIClient {
|
||||
return this.http.get(API_CONFIG);
|
||||
}
|
||||
|
||||
getMe () {
|
||||
return this.http.get(API_ME);
|
||||
}
|
||||
|
||||
destroyOrganization (id) {
|
||||
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||
return (this.http.delete(endpoint));
|
||||
@ -71,6 +76,10 @@ class APIClient {
|
||||
return this.http.post(API_ORGANIZATIONS, data);
|
||||
}
|
||||
|
||||
optionsOrganizations () {
|
||||
return this.http.options(API_ORGANIZATIONS);
|
||||
}
|
||||
|
||||
getOrganizationAccessList (id, params = {}) {
|
||||
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { I18n, i18nMark } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
@ -28,14 +28,11 @@ import VerticalSeparator from '../VerticalSeparator';
|
||||
class DataListToolbar extends React.Component {
|
||||
render () {
|
||||
const {
|
||||
add,
|
||||
addUrl,
|
||||
columns,
|
||||
deleteTooltip,
|
||||
disableTrashCanIcon,
|
||||
onSelectAll,
|
||||
sortedColumnKey,
|
||||
sortOrder,
|
||||
showDelete,
|
||||
showSelectAll,
|
||||
isAllSelected,
|
||||
isCompact,
|
||||
noLeftMargin,
|
||||
@ -43,8 +40,13 @@ class DataListToolbar extends React.Component {
|
||||
onSearch,
|
||||
onCompact,
|
||||
onExpand,
|
||||
add,
|
||||
onOpenDeleteModal
|
||||
onOpenDeleteModal,
|
||||
onSelectAll,
|
||||
showAdd,
|
||||
showDelete,
|
||||
showSelectAll,
|
||||
sortOrder,
|
||||
sortedColumnKey
|
||||
} = this.props;
|
||||
|
||||
const showExpandCollapse = (onCompact && onExpand);
|
||||
@ -112,21 +114,23 @@ class DataListToolbar extends React.Component {
|
||||
<LevelItem>
|
||||
{ showDelete && (
|
||||
<Tooltip
|
||||
content={i18n._(t`Delete`)}
|
||||
position="top"
|
||||
content={deleteTooltip}
|
||||
position="left"
|
||||
>
|
||||
<Button
|
||||
className="awx-ToolBarBtn"
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={onOpenDeleteModal}
|
||||
isDisabled={disableTrashCanIcon}
|
||||
>
|
||||
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
|
||||
</Button>
|
||||
<span>
|
||||
<Button
|
||||
className="awx-ToolBarBtn"
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={onOpenDeleteModal}
|
||||
isDisabled={disableTrashCanIcon}
|
||||
>
|
||||
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{addUrl && (
|
||||
{showAdd && addUrl && (
|
||||
<Link to={addUrl}>
|
||||
<Button
|
||||
variant="primary"
|
||||
@ -136,7 +140,7 @@ class DataListToolbar extends React.Component {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{add && (
|
||||
{showAdd && add && (
|
||||
<Fragment>{add}</Fragment>
|
||||
)}
|
||||
</LevelItem>
|
||||
@ -149,38 +153,42 @@ class DataListToolbar extends React.Component {
|
||||
}
|
||||
|
||||
DataListToolbar.propTypes = {
|
||||
add: PropTypes.node,
|
||||
addUrl: PropTypes.string,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
deleteTooltip: PropTypes.node,
|
||||
isAllSelected: PropTypes.bool,
|
||||
isCompact: PropTypes.bool,
|
||||
noLeftMargin: PropTypes.bool,
|
||||
onCompact: PropTypes.func,
|
||||
onExpand: PropTypes.func,
|
||||
onSearch: PropTypes.func,
|
||||
onSelectAll: PropTypes.func,
|
||||
onSort: PropTypes.func,
|
||||
showAdd: PropTypes.bool,
|
||||
showDelete: PropTypes.bool,
|
||||
showSelectAll: PropTypes.bool,
|
||||
sortOrder: PropTypes.string,
|
||||
sortedColumnKey: PropTypes.string,
|
||||
onCompact: PropTypes.func,
|
||||
onExpand: PropTypes.func,
|
||||
isCompact: PropTypes.bool,
|
||||
add: PropTypes.node
|
||||
sortedColumnKey: PropTypes.string
|
||||
};
|
||||
|
||||
DataListToolbar.defaultProps = {
|
||||
add: null,
|
||||
addUrl: null,
|
||||
deleteTooltip: i18nMark('Delete'),
|
||||
isAllSelected: false,
|
||||
isCompact: false,
|
||||
noLeftMargin: false,
|
||||
onCompact: null,
|
||||
onExpand: null,
|
||||
onSearch: null,
|
||||
onSelectAll: null,
|
||||
onSort: null,
|
||||
showAdd: false,
|
||||
showDelete: false,
|
||||
showSelectAll: false,
|
||||
sortOrder: 'ascending',
|
||||
sortedColumnKey: 'name',
|
||||
isAllSelected: false,
|
||||
onCompact: null,
|
||||
onExpand: null,
|
||||
isCompact: false,
|
||||
add: null,
|
||||
noLeftMargin: false
|
||||
sortedColumnKey: 'name'
|
||||
};
|
||||
|
||||
export default DataListToolbar;
|
||||
|
||||
@ -74,7 +74,10 @@
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
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 {
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
class NotificationListItem extends React.Component {
|
||||
render () {
|
||||
const {
|
||||
canToggleNotifications,
|
||||
itemId,
|
||||
name,
|
||||
notificationType,
|
||||
@ -49,12 +50,14 @@ class NotificationListItem extends React.Component {
|
||||
<Switch
|
||||
label={i18n._(t`Successful`)}
|
||||
isChecked={successTurnedOn}
|
||||
isDisabled={!canToggleNotifications}
|
||||
onChange={() => toggleNotification(itemId, successTurnedOn, 'success')}
|
||||
aria-label={i18n._(t`Notification success toggle`)}
|
||||
/>
|
||||
<Switch
|
||||
label={i18n._(t`Failure`)}
|
||||
isChecked={errorTurnedOn}
|
||||
isDisabled={!canToggleNotifications}
|
||||
onChange={() => toggleNotification(itemId, errorTurnedOn, 'error')}
|
||||
aria-label={i18n._(t`Notification failure toggle`)}
|
||||
/>
|
||||
@ -67,6 +70,7 @@ class NotificationListItem extends React.Component {
|
||||
}
|
||||
|
||||
NotificationListItem.propTypes = {
|
||||
canToggleNotifications: PropTypes.bool.isRequired,
|
||||
detailUrl: PropTypes.string.isRequired,
|
||||
errorTurnedOn: PropTypes.bool,
|
||||
itemId: PropTypes.number.isRequired,
|
||||
|
||||
@ -273,6 +273,7 @@ class Notifications extends Component {
|
||||
successTemplateIds,
|
||||
errorTemplateIds
|
||||
} = this.state;
|
||||
const { canToggleNotifications } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
{noInitialResults && (
|
||||
@ -315,6 +316,7 @@ class Notifications extends Component {
|
||||
toggleNotification={this.toggleNotification}
|
||||
errorTurnedOn={errorTemplateIds.includes(o.id)}
|
||||
successTurnedOn={successTemplateIds.includes(o.id)}
|
||||
canToggleNotifications={canToggleNotifications}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@ -337,6 +339,7 @@ class Notifications extends Component {
|
||||
}
|
||||
|
||||
Notifications.propTypes = {
|
||||
canToggleNotifications: PropTypes.bool.isRequired,
|
||||
onReadError: PropTypes.func.isRequired,
|
||||
onReadNotifications: PropTypes.func.isRequired,
|
||||
onReadSuccess: PropTypes.func.isRequired,
|
||||
|
||||
@ -16,11 +16,14 @@ class Provider extends Component {
|
||||
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 () {
|
||||
@ -30,30 +33,64 @@ 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 () {
|
||||
const { api, handleHttpError } = this.props;
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
version
|
||||
}
|
||||
} = await api.getConfig();
|
||||
const {
|
||||
data: {
|
||||
custom_logo,
|
||||
custom_login_info
|
||||
}
|
||||
} = await api.getRoot();
|
||||
const [configRes, rootRes, meRes] = await Promise.all([
|
||||
api.getConfig(),
|
||||
api.getRoot(),
|
||||
api.getMe()
|
||||
]);
|
||||
this.setState({
|
||||
value: {
|
||||
ansible_version,
|
||||
custom_virtualenvs,
|
||||
version,
|
||||
custom_logo,
|
||||
custom_login_info
|
||||
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
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@ -63,7 +100,8 @@ class Provider extends Component {
|
||||
custom_virtualenvs: null,
|
||||
version: null,
|
||||
custom_logo: null,
|
||||
custom_login_info: null
|
||||
custom_login_info: null,
|
||||
me: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -75,7 +113,13 @@ class Provider extends Component {
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={value}>
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
...value,
|
||||
fetchMe: this.fetchMe,
|
||||
updateConfig: this.updateConfig
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
|
||||
@ -63,10 +63,12 @@ export function main (render) {
|
||||
path="/login"
|
||||
render={() => (
|
||||
<Config>
|
||||
{({ custom_logo, custom_login_info }) => (
|
||||
{({ custom_logo, custom_login_info, fetchMe, updateConfig }) => (
|
||||
<Login
|
||||
logo={custom_logo}
|
||||
loginInfo={custom_login_info}
|
||||
fetchMe={fetchMe}
|
||||
updateConfig={updateConfig}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
|
||||
@ -39,7 +39,7 @@ class AWXLogin extends Component {
|
||||
|
||||
async onLoginButtonClick (event) {
|
||||
const { username, password, isLoading } = this.state;
|
||||
const { api, handleHttpError, clearRootDialogMessage } = this.props;
|
||||
const { api, handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@ -51,7 +51,9 @@ class AWXLogin extends Component {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
try {
|
||||
await api.login(username, password);
|
||||
const { data } = await api.login(username, password);
|
||||
updateConfig(data);
|
||||
await fetchMe();
|
||||
this.setState({ isAuthenticated: true, isLoading: false });
|
||||
} catch (error) {
|
||||
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 { Trans } from '@lingui/macro';
|
||||
|
||||
import { Config } from '../../contexts/Config';
|
||||
import { NetworkProvider } from '../../contexts/Network';
|
||||
import { withRootDialog } from '../../contexts/RootDialog';
|
||||
|
||||
@ -74,11 +75,16 @@ class Organizations extends Component {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Organization
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadcrumbConfig}
|
||||
/>
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Organization
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadcrumbConfig}
|
||||
me={me || {}}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
</NetworkProvider>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
import { withNetwork } from '../../../contexts/Network';
|
||||
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import BasicChip from '../../../components/BasicChip/BasicChip';
|
||||
import Pagination from '../../../components/Pagination';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import AddResourceRole from '../../../components/AddRole/AddResourceRole';
|
||||
@ -357,6 +358,7 @@ class OrganizationAccessList extends React.Component {
|
||||
columns={this.columns}
|
||||
onSearch={() => { }}
|
||||
onSort={this.onSort}
|
||||
showAdd={organization.summary_fields.user_capabilities.edit}
|
||||
add={(
|
||||
<Fragment>
|
||||
<Button
|
||||
@ -421,13 +423,21 @@ class OrganizationAccessList extends React.Component {
|
||||
<ul style={userRolesWrapperStyle}>
|
||||
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
|
||||
{result.userRoles.map(role => (
|
||||
<Chip
|
||||
key={role.id}
|
||||
className="awx-c-chip"
|
||||
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
||||
>
|
||||
{role.name}
|
||||
</Chip>
|
||||
role.user_capabilities.unattach ? (
|
||||
<Chip
|
||||
key={role.id}
|
||||
className="awx-c-chip"
|
||||
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
||||
>
|
||||
{role.name}
|
||||
</Chip>
|
||||
) : (
|
||||
<BasicChip
|
||||
key={role.id}
|
||||
>
|
||||
{role.name}
|
||||
</BasicChip>
|
||||
)
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
@ -466,7 +476,9 @@ class OrganizationAccessList extends React.Component {
|
||||
}
|
||||
|
||||
OrganizationAccessList.propTypes = {
|
||||
api: PropTypes.shape().isRequired,
|
||||
getAccessList: PropTypes.func.isRequired,
|
||||
organization: PropTypes.shape().isRequired,
|
||||
removeRole: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@ -33,13 +33,17 @@ class Organization extends Component {
|
||||
organization: null,
|
||||
error: false,
|
||||
loading: true,
|
||||
isNotifAdmin: false,
|
||||
isAuditorOfThisOrg: false,
|
||||
isAdminOfThisOrg: false
|
||||
};
|
||||
|
||||
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||
this.fetchOrganizationAndRoles = this.fetchOrganizationAndRoles.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.fetchOrganization();
|
||||
this.fetchOrganizationAndRoles();
|
||||
}
|
||||
|
||||
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 () {
|
||||
const {
|
||||
match,
|
||||
@ -70,19 +111,44 @@ class Organization extends Component {
|
||||
const {
|
||||
location,
|
||||
match,
|
||||
me,
|
||||
history
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
organization,
|
||||
error,
|
||||
loading
|
||||
loading,
|
||||
isNotifAdmin,
|
||||
isAuditorOfThisOrg,
|
||||
isAdminOfThisOrg
|
||||
} = this.state;
|
||||
|
||||
const tabsPaddingOverride = {
|
||||
padding: '0'
|
||||
};
|
||||
|
||||
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg;
|
||||
const canToggleNotifications = isNotifAdmin && (
|
||||
me.is_system_auditor
|
||||
|| isAuditorOfThisOrg
|
||||
|| isAdminOfThisOrg
|
||||
);
|
||||
|
||||
const tabsArray = [
|
||||
{ name: i18nMark('Details'), link: `${match.url}/details`, id: 0 },
|
||||
{ name: i18nMark('Access'), link: `${match.url}/access`, id: 1 },
|
||||
{ name: i18nMark('Teams'), link: `${match.url}/teams`, id: 2 }
|
||||
];
|
||||
|
||||
if (canSeeNotificationsTab) {
|
||||
tabsArray.push({
|
||||
name: i18nMark('Notifications'),
|
||||
link: `${match.url}/notifications`,
|
||||
id: 3
|
||||
});
|
||||
}
|
||||
|
||||
let cardHeader = (
|
||||
loading ? ''
|
||||
: (
|
||||
@ -96,12 +162,7 @@ class Organization extends Component {
|
||||
match={match}
|
||||
history={history}
|
||||
labeltext={i18n._(t`Organization detail tabs`)}
|
||||
tabsArray={[
|
||||
{ name: i18nMark('Details'), link: `${match.url}/details`, id: 0 },
|
||||
{ name: i18nMark('Access'), link: `${match.url}/access`, id: 1 },
|
||||
{ name: i18nMark('Teams'), link: `${match.url}/teams`, id: 2 },
|
||||
{ name: i18nMark('Notifications'), link: `${match.url}/notifications`, id: 3 },
|
||||
]}
|
||||
tabsArray={tabsArray}
|
||||
/>
|
||||
<Link
|
||||
aria-label="Close"
|
||||
@ -174,16 +235,16 @@ class Organization extends Component {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/organizations/:id/notifications"
|
||||
render={() => (
|
||||
<OrganizationNotifications
|
||||
match={match}
|
||||
location={location}
|
||||
history={history}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{canSeeNotificationsTab && (
|
||||
<Route
|
||||
path="/organizations/:id/notifications"
|
||||
render={() => (
|
||||
<OrganizationNotifications
|
||||
canToggleNotifications={canToggleNotifications}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
||||
</Switch>
|
||||
{error ? 'error!' : ''}
|
||||
|
||||
@ -100,7 +100,8 @@ class OrganizationDetail extends Component {
|
||||
description,
|
||||
custom_virtualenv,
|
||||
created,
|
||||
modified
|
||||
modified,
|
||||
summary_fields
|
||||
},
|
||||
match
|
||||
} = this.props;
|
||||
@ -165,11 +166,13 @@ class OrganizationDetail extends Component {
|
||||
</TextContent>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
||||
<Link to={`/organizations/${match.params.id}/edit`}>
|
||||
<Button><Trans>Edit</Trans></Button>
|
||||
</Link>
|
||||
</div>
|
||||
{summary_fields.user_capabilities.edit && (
|
||||
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
||||
<Link to={`/organizations/${match.params.id}/edit`}>
|
||||
<Button><Trans>Edit</Trans></Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{error ? 'error!' : ''}
|
||||
</CardBody>
|
||||
)}
|
||||
|
||||
@ -41,13 +41,18 @@ class OrganizationNotifications extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
canToggleNotifications
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<NotificationsList
|
||||
canToggleNotifications={canToggleNotifications}
|
||||
onCreateError={this.createOrgNotificationError}
|
||||
onCreateSuccess={this.createOrgNotificationSuccess}
|
||||
onReadError={this.readOrgNotificationError}
|
||||
onReadNotifications={this.readOrgNotifications}
|
||||
onReadSuccess={this.readOrgNotificationSuccess}
|
||||
onReadError={this.readOrgNotificationError}
|
||||
onCreateSuccess={this.createOrgNotificationSuccess}
|
||||
onCreateError={this.createOrgNotificationError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,8 +62,7 @@ class OrganizationsList extends Component {
|
||||
loading: true,
|
||||
results: [],
|
||||
selected: [],
|
||||
isModalOpen: false,
|
||||
orgsToDelete: [],
|
||||
isModalOpen: false
|
||||
|
||||
};
|
||||
|
||||
@ -74,14 +73,16 @@ class OrganizationsList extends Component {
|
||||
this.onSelectAll = this.onSelectAll.bind(this);
|
||||
this.onSelect = this.onSelect.bind(this);
|
||||
this.updateUrl = this.updateUrl.bind(this);
|
||||
this.fetchOptionsOrganizations = this.fetchOptionsOrganizations.bind(this);
|
||||
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
||||
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
|
||||
this.handleClearOrgsToDelete = this.handleClearOrgsToDelete.bind(this);
|
||||
this.handleCloseOrgDeleteModal = this.handleCloseOrgDeleteModal.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const queryParams = this.getQueryParams();
|
||||
this.fetchOptionsOrganizations();
|
||||
this.fetchOrganizations(queryParams);
|
||||
}
|
||||
|
||||
@ -117,20 +118,20 @@ class OrganizationsList extends Component {
|
||||
onSelectAll (isSelected) {
|
||||
const { results } = this.state;
|
||||
|
||||
const selected = isSelected ? results.map(o => o.id) : [];
|
||||
const selected = isSelected ? results : [];
|
||||
|
||||
this.setState({ selected });
|
||||
}
|
||||
|
||||
onSelect (id) {
|
||||
onSelect (row) {
|
||||
const { selected } = this.state;
|
||||
|
||||
const isSelected = selected.includes(id);
|
||||
const isSelected = selected.some(s => s.id === row.id);
|
||||
|
||||
if (isSelected) {
|
||||
this.setState({ selected: selected.filter(s => s !== id) });
|
||||
this.setState({ selected: selected.filter(s => s.id !== row.id) });
|
||||
} 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);
|
||||
}
|
||||
|
||||
handleClearOrgsToDelete () {
|
||||
handleCloseOrgDeleteModal () {
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
orgsToDelete: []
|
||||
isModalOpen: false
|
||||
});
|
||||
this.onSelectAll();
|
||||
}
|
||||
|
||||
handleOpenOrgDeleteModal () {
|
||||
const { results, selected } = this.state;
|
||||
const { selected } = this.state;
|
||||
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
||||
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({
|
||||
orgsToDelete,
|
||||
isModalOpen: true,
|
||||
warningTitle,
|
||||
warningMsg,
|
||||
loading: false });
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
async handleOrgDelete (event) {
|
||||
const { orgsToDelete } = this.state;
|
||||
async handleOrgDelete () {
|
||||
const { selected } = this.state;
|
||||
const { api, handleHttpError } = this.props;
|
||||
let errorHandled;
|
||||
|
||||
try {
|
||||
await Promise.all(orgsToDelete.map((org) => api.destroyOrganization(org.id)));
|
||||
this.handleClearOrgsToDelete();
|
||||
await Promise.all(selected.map((org) => api.destroyOrganization(org.id)));
|
||||
this.setState({
|
||||
isModalOpen: false,
|
||||
selected: []
|
||||
});
|
||||
} catch (err) {
|
||||
errorHandled = handleHttpError(err);
|
||||
} finally {
|
||||
@ -188,7 +181,6 @@ class OrganizationsList extends Component {
|
||||
this.fetchOrganizations(queryParams);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
updateUrl (queryParams) {
|
||||
@ -248,16 +240,35 @@ class OrganizationsList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchOptionsOrganizations () {
|
||||
const { api } = this.props;
|
||||
|
||||
try {
|
||||
const { data } = await api.optionsOrganizations();
|
||||
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 () {
|
||||
const {
|
||||
medium,
|
||||
} = PageSectionVariants;
|
||||
const {
|
||||
canAdd,
|
||||
count,
|
||||
error,
|
||||
loading,
|
||||
noInitialResults,
|
||||
orgsToDelete,
|
||||
page,
|
||||
pageCount,
|
||||
page_size,
|
||||
@ -270,6 +281,12 @@ class OrganizationsList extends Component {
|
||||
warningMsg,
|
||||
} = this.state;
|
||||
const { match } = this.props;
|
||||
|
||||
const disableDelete = (
|
||||
selected.length === 0
|
||||
|| selected.some(row => !row.summary_fields.user_capabilities.delete)
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@ -280,15 +297,15 @@ class OrganizationsList extends Component {
|
||||
variant="danger"
|
||||
title={warningTitle}
|
||||
isOpen={isModalOpen}
|
||||
onClose={this.handleClearOrgsToDelete}
|
||||
onClose={this.handleCloseOrgDeleteModal}
|
||||
actions={[
|
||||
<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}
|
||||
<br />
|
||||
{orgsToDelete.map((org) => (
|
||||
{selected.map((org) => (
|
||||
<span key={org.id}>
|
||||
<strong>
|
||||
{org.name}
|
||||
@ -321,9 +338,27 @@ class OrganizationsList extends Component {
|
||||
onSort={this.onSort}
|
||||
onSelectAll={this.onSelectAll}
|
||||
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
|
||||
.filter(row => !row.summary_fields.user_capabilities.delete)
|
||||
.map(row => (
|
||||
<div key={row.id}>
|
||||
{row.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
showDelete
|
||||
showSelectAll
|
||||
showAdd={canAdd}
|
||||
/>
|
||||
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
||||
{ results.map(o => (
|
||||
@ -334,8 +369,8 @@ class OrganizationsList extends Component {
|
||||
detailUrl={`${match.url}/${o.id}`}
|
||||
memberCount={o.summary_fields.related_field_counts.users}
|
||||
teamCount={o.summary_fields.related_field_counts.teams}
|
||||
isSelected={selected.includes(o.id)}
|
||||
onSelect={() => this.onSelect(o.id, o.name)}
|
||||
isSelected={selected.some(row => row.id === o.id)}
|
||||
onSelect={() => this.onSelect(o)}
|
||||
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
|
||||
/>
|
||||
))}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user