diff --git a/src/api.js b/src/api.js index 1c9c03ff31..95e4520541 100644 --- a/src/api.js +++ b/src/api.js @@ -66,10 +66,10 @@ class APIClient { return this.http.post(API_ORGANIZATIONS, data); } - getOrganzationAccessList (id) { + getOrganzationAccessList (id, params = {}) { const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`; - return this.http.get(endpoint); + return this.http.get(endpoint, { params }); } getOrganizationDetails (id) { @@ -84,24 +84,6 @@ class APIClient { return this.http.get(endpoint, { params }); } - getOrganizationUserRoles (id) { - const endpoint = `${API_USERS}${id}/roles/`; - - return this.http.get(endpoint); - } - - getUserTeams (id) { - const endpoint = `${API_USERS}${id}/teams/`; - - return this.http.get(endpoint); - } - - getTeamRoles (id) { - const endpoint = `${API_TEAMS}${id}/roles/`; - - return this.http.get(endpoint); - } - getOrganizationNotifications (id, params = {}) { const endpoint = `${API_ORGANIZATIONS}${id}/notification_templates/`; @@ -139,6 +121,24 @@ class APIClient { createInstanceGroups (url, id) { return this.http.post(url, { id }); } + + getUserRoles (id) { + const endpoint = `${API_USERS}${id}/roles/`; + + return this.http.get(endpoint); + } + + getUserTeams (id) { + const endpoint = `${API_USERS}${id}/teams/`; + + return this.http.get(endpoint); + } + + getTeamRoles (id) { + const endpoint = `${API_TEAMS}${id}/roles/`; + + return this.http.get(endpoint); + } } export default APIClient; diff --git a/src/components/AccessList/Access.list.jsx b/src/components/AccessList/Access.list.jsx new file mode 100644 index 0000000000..dc9a53a592 --- /dev/null +++ b/src/components/AccessList/Access.list.jsx @@ -0,0 +1,343 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { + DataList, DataListItem, DataListCell, Text, + TextContent, TextVariants, Badge +} from '@patternfly/react-core'; + +import { I18n, i18nMark } from '@lingui/react'; +import { t } from '@lingui/macro'; + +import { + Link +} from 'react-router-dom'; + +import BasicChip from '../BasicChip/BasicChip'; +import Pagination from '../Pagination/Pagination'; +import DataListToolbar from '../DataListToolbar/DataListToolbar'; + +import { + parseQueryString, +} from '../../qs'; + +const userRolesWrapperStyle = { + display: 'flex', + flexWrap: 'wrap', +}; + +const detailWrapperStyle = { + display: 'grid', + gridTemplateColumns: 'minmax(70px, max-content) minmax(60px, max-content)', +}; + +const detailLabelStyle = { + fontWeight: '700', + lineHeight: '24px', + marginRight: '20px', +}; + +const detailValueStyle = { + lineHeight: '28px', + overflow: 'visible', +}; + +const hiddenStyle = { + display: 'none', +}; + +const Detail = ({ label, value, url, isBadge, customStyles }) => { + let detail = null; + if (value) { + detail = ( + + {url ? ( + + {label} + ) : ({label} + )} + {isBadge ? ( + + {value} + + ) : ( + {value} + )} + + ); + } + return detail; +}; + +class AccessList extends React.Component { + columns = [ + { name: i18nMark('Username'), key: 'username', isSortable: true }, + { name: i18nMark('First Name'), key: 'first_name', isSortable: true, isNumeric: true }, + { name: i18nMark('Last Name'), key: 'last_name', isSortable: true, isNumeric: true }, + ]; + + defaultParams = { + page: 1, + page_size: 5, + order_by: 'username', + }; + + constructor (props) { + super(props); + + const { page, page_size } = this.getQueryParams(); + + this.state = { + page, + page_size, + count: null, + results: [], + sortOrder: 'ascending', + sortedColumnKey: 'username', + isCompact: true, + }; + + this.fetchOrgAccessList = this.fetchOrgAccessList.bind(this); + this.onSetPage = this.onSetPage.bind(this); + this.onExpand = this.onExpand.bind(this); + this.onCompact = this.onCompact.bind(this); + this.onSort = this.onSort.bind(this); + this.getQueryParams = this.getQueryParams.bind(this); + } + + componentDidMount () { + const queryParams = this.getQueryParams(); + + this.fetchOrgAccessList(queryParams); + } + + onExpand = () => { + this.setState({ isCompact: false }); + } + + onCompact = () => { + this.setState({ isCompact: true }); + } + + onSetPage = (pageNumber, pageSize) => { + const { sortOrder, sortedColumnKey } = this.state; + const page = parseInt(pageNumber, 10); + const page_size = parseInt(pageSize, 10); + let order_by = sortedColumnKey; + + if (sortOrder === 'descending') { + order_by = `-${order_by}`; + } + + const queryParams = this.getQueryParams({ page, page_size, order_by }); + + this.fetchOrgAccessList(queryParams); + }; + + getQueryParams (overrides = {}) { + const { location } = this.props; + const { search } = location; + + const searchParams = parseQueryString(search.substring(1)); + + return Object.assign({}, this.defaultParams, searchParams, overrides); + } + + onSort = (sortedColumnKey, sortOrder) => { + const { page_size } = this.state; + + let order_by = sortedColumnKey; + + if (sortOrder === 'descending') { + order_by = `-${order_by}`; + } + + const queryParams = this.getQueryParams({ order_by, page_size }); + + this.fetchOrgAccessList(queryParams); + }; + + async fetchOrgAccessList (queryParams) { + const { match, getAccessList, getUserRoles, getTeamRoles, getUserTeams } = this.props; + + const { page, page_size, order_by } = queryParams; + + let sortOrder = 'ascending'; + let sortedColumnKey = order_by; + + if (order_by.startsWith('-')) { + sortOrder = 'descending'; + sortedColumnKey = order_by.substring(1); + } + + try { + const { data: + { count = null, results = null } + } = await getAccessList(match.params.id, queryParams); + const pageCount = Math.ceil(count / page_size); + + const stateToUpdate = { + count, + page, + pageCount, + page_size, + sortOrder, + sortedColumnKey, + results, + }; + + results.map(async result => { + result.user_roles = []; + result.team_roles = []; + // Grab each Role Type and set as a top-level value + Object.values(result.summary_fields).filter(value => value.length > 0).forEach(roleType => { + result.roleType = roleType[0].role.name; + }); + + // Grab User Roles and set as a top-level value + try { + const resp = await getUserRoles(result.id); + const roles = resp.data.results; + roles.forEach(role => { + result.user_roles.push(role); + }); + this.setState(stateToUpdate); + } catch (error) { + this.setState({ error }); + } + + // Grab Team Roles and set as a top-level value + try { + const team_data = await getUserTeams(result.id); + const teams = team_data.data.results; + teams.forEach(async team => { + let team_roles = await getTeamRoles(team.id); + team_roles = team_roles.data.results; + team_roles.forEach(role => { + result.team_roles.push(role); + }); + this.setState(stateToUpdate); + }); + } catch (error) { + this.setState({ error }); + } + }); + } catch (error) { + this.setState({ error }); + } + } + + render () { + const { + results, + error, + count, + page_size, + pageCount, + page, + sortedColumnKey, + sortOrder, + isCompact, + } = this.state; + + if (!results) { + return null; + } + return ( + + {({ i18n }) => ( + + {}} + onSort={this.onSort} + onCompact={this.onCompact} + onExpand={this.onExpand} + isCompact={isCompact} + showExpandCollapse + /> + + {results.map(result => ( + + + + {result.first_name || result.last_name ? ( + + ) : ( + null + )} + + + + {result.user_roles.length > 0 && ( +
    + {i18n._(t`User Roles`)} + {result.user_roles.map(role => ( + + ))} +
+ )} + {result.team_roles.length > 0 && ( +
    + {i18n._(t`Team Roles`)} + {result.team_roles.map(role => ( + + ))} +
+ )} +
+
+ ))} +
+ + { error ?
{error}
: '' } +
+ )} +
+ ); + } +} + +AccessList.propTypes = { + getAccessList: PropTypes.func.isRequired, + getUserRoles: PropTypes.func.isRequired, + getUserTeams: PropTypes.func.isRequired, + getTeamRoles: PropTypes.func.isRequired, +}; + +export default AccessList; diff --git a/src/components/AccessList/index.js b/src/components/AccessList/index.js new file mode 100644 index 0000000000..f435e8bb1f --- /dev/null +++ b/src/components/AccessList/index.js @@ -0,0 +1,3 @@ +import AccessList from './Access.list'; + +export default AccessList; diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index a84f086a00..074b804e58 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -24,7 +24,7 @@ import { SortNumericDownIcon, SortNumericUpIcon, TrashAltIcon, - PlusIcon + PlusIcon, } from '@patternfly/react-icons'; import { Link @@ -37,6 +37,12 @@ const flexGrowStyling = { flexGrow: '1' }; +const ToolbarActiveStyle = { + backgroundColor: 'rgb(0, 123, 186)', + color: 'white', + padding: '0 5px', +}; + class DataListToolbar extends React.Component { constructor (props) { super(props); @@ -56,6 +62,18 @@ class DataListToolbar extends React.Component { this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this); this.onSearch = this.onSearch.bind(this); this.onSort = this.onSort.bind(this); + this.onExpand = this.onExpand.bind(this); + this.onCompact = this.onCompact.bind(this); + } + + onExpand () { + const { onExpand } = this.props; + onExpand(); + } + + onCompact () { + const { onCompact } = this.props; + onCompact(); } onSortDropdownToggle (isSortDropdownOpen) { @@ -114,7 +132,8 @@ class DataListToolbar extends React.Component { showExpandCollapse, showDelete, showSelectAll, - isLookup + isLookup, + isCompact, } = this.props; const { isSearchDropdownOpen, @@ -246,16 +265,20 @@ class DataListToolbar extends React.Component { { (showDelete || addUrl) && ( @@ -306,6 +329,7 @@ DataListToolbar.propTypes = { onSelectAll: PropTypes.func, onSort: PropTypes.func, showDelete: PropTypes.bool, + showExpandCollapse: PropTypes.bool, showSelectAll: PropTypes.bool, sortOrder: PropTypes.string, sortedColumnKey: PropTypes.string, @@ -317,6 +341,7 @@ DataListToolbar.defaultProps = { onSelectAll: null, onSort: null, showDelete: false, + showExpandCollapse: false, showSelectAll: false, sortOrder: 'ascending', sortedColumnKey: 'name', diff --git a/src/components/NotificationsList/Notifications.list.jsx b/src/components/NotificationsList/Notifications.list.jsx index 38f37044ce..1fe385823d 100644 --- a/src/components/NotificationsList/Notifications.list.jsx +++ b/src/components/NotificationsList/Notifications.list.jsx @@ -335,7 +335,7 @@ class Notifications extends Component { } } -Notifications.propType = { +Notifications.propTypes = { getError: PropTypes.func.isRequired, getNotifications: PropTypes.func.isRequired, getSuccess: PropTypes.func.isRequired, diff --git a/src/pages/Organizations/screens/Organization/OrganizationAccess.jsx b/src/pages/Organizations/screens/Organization/OrganizationAccess.jsx new file mode 100644 index 0000000000..969bb4a370 --- /dev/null +++ b/src/pages/Organizations/screens/Organization/OrganizationAccess.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import AccessList from '../../../../components/AccessList/Access.list'; + +class OrganizationAccess extends React.Component { + constructor (props) { + super(props); + + this.getOrgAccessList = this.getOrgAccessList.bind(this); + this.getUserRoles = this.getUserRoles.bind(this); + this.getUserTeams = this.getUserTeams.bind(this); + this.getTeamRoles = this.getTeamRoles.bind(this); + } + + getOrgAccessList (id, params) { + const { api } = this.props; + return api.getOrganzationAccessList(id, params); + } + + getUserRoles (id) { + const { api } = this.props; + return api.getUserRoles(id); + } + + getUserTeams (id) { + const { api } = this.props; + return api.getUserTeams(id); + } + + getTeamRoles (id) { + const { api } = this.props; + return api.getTeamRoles(id); + } + + render () { + const { + location, + match, + history, + } = this.props; + + return ( + + ); + } +} + +export default OrganizationAccess;