From 5ae7cbb43ad24080fb55923c995140728ff635e9 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 18 Apr 2019 13:31:03 -0400 Subject: [PATCH] Add RBAC to org views --- src/api.js | 9 ++ .../DataListToolbar/DataListToolbar.jsx | 60 ++++++---- src/components/DataListToolbar/styles.scss | 5 +- .../NotificationListItem.jsx | 4 + .../NotificationsList/Notifications.list.jsx | 3 + src/contexts/Config.jsx | 59 +++++++++- src/index.jsx | 4 +- src/pages/Login.jsx | 6 +- src/pages/Organizations/Organizations.jsx | 16 ++- .../components/OrganizationAccessList.jsx | 28 +++-- .../screens/Organization/Organization.jsx | 89 +++++++++++++-- .../Organization/OrganizationDetail.jsx | 15 ++- .../OrganizationNotifications.jsx | 11 +- .../screens/OrganizationsList.jsx | 104 ++++++++++++------ 14 files changed, 315 insertions(+), 98 deletions(-) diff --git a/src/api.js b/src/api.js index bda5fd5319..9767632c6a 100644 --- a/src/api.js +++ b/src/api.js @@ -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); } + callOrganizations () { + return this.http.options(API_ORGANIZATIONS); + } + getOrganizationAccessList (id, params = {}) { const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`; diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index 738f396b3f..a4e6428253 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -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,23 +28,27 @@ import VerticalSeparator from '../VerticalSeparator'; class DataListToolbar extends React.Component { render () { const { + add, addUrl, columns, + deleteTooltip, disableTrashCanIcon, - onSelectAll, - sortedColumnKey, - sortOrder, - showDelete, - showSelectAll, isAllSelected, isCompact, noLeftMargin, onSort, - onSearch, + onSearch onCompact, onExpand, - add, - onOpenDeleteModal + onOpenDeleteModal, + onSearch, + onSelectAll, + onSort, + showAdd, + showDelete, + showSelectAll, + sortOrder, + sortedColumnKey } = this.props; const showExpandCollapse = (onCompact && onExpand); @@ -112,21 +116,23 @@ class DataListToolbar extends React.Component { { showDelete && ( - + + + )} - {addUrl && ( + {showAdd && addUrl && ( - - + {summary_fields.user_capabilities.edit && ( +
+ + + +
+ )} {error ? 'error!' : ''} )} diff --git a/src/pages/Organizations/screens/Organization/OrganizationNotifications.jsx b/src/pages/Organizations/screens/Organization/OrganizationNotifications.jsx index 4f6cf13d6d..491071bfee 100644 --- a/src/pages/Organizations/screens/Organization/OrganizationNotifications.jsx +++ b/src/pages/Organizations/screens/Organization/OrganizationNotifications.jsx @@ -41,13 +41,18 @@ class OrganizationNotifications extends Component { } render () { + const { + canToggleNotifications + } = this.props; + return ( ); } diff --git a/src/pages/Organizations/screens/OrganizationsList.jsx b/src/pages/Organizations/screens/OrganizationsList.jsx index a3746ebaab..f0d0054715 100644 --- a/src/pages/Organizations/screens/OrganizationsList.jsx +++ b/src/pages/Organizations/screens/OrganizationsList.jsx @@ -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.callOrganizations = this.callOrganizations.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.callOrganizations(); 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 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 () { 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 }) => ( @@ -280,15 +297,15 @@ class OrganizationsList extends Component { variant="danger" title={warningTitle} isOpen={isModalOpen} - onClose={this.handleClearOrgsToDelete} + onClose={this.handleCloseOrgDeleteModal} actions={[ , - + ]} > {warningMsg}
- {orgsToDelete.map((org) => ( + {selected.map((org) => ( {org.name} @@ -321,9 +338,24 @@ 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) ? ( +
+ + You dont have permission to delete the following Organizations: + + {selected.map(row => ( +
+ {row.name} +
+ ))} +
+ ) : undefined + } showDelete showSelectAll + showAdd={canAdd} />
    { results.map(o => ( @@ -334,8 +366,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} /> ))}