Respond to PR feedback.

This commit is contained in:
Kia Lam
2019-03-05 12:01:06 -05:00
parent 35ecd83214
commit 9f86fc2def
7 changed files with 101 additions and 135 deletions

View File

@@ -17,20 +17,14 @@ const mockResults = [
{ {
role: { role: {
name: 'foo', name: 'foo',
id: 2,
} }
} }
] ],
} }
} }
]; ];
const mockUserRoles = [
{
id: 2,
name: 'bar',
}
];
describe('<AccessList />', () => { describe('<AccessList />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
mount( mount(
@@ -40,7 +34,6 @@ describe('<AccessList />', () => {
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '1' } }} match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '1' } }}
location={{ search: '', pathname: '/organizations/1/access' }} location={{ search: '', pathname: '/organizations/1/access' }}
getAccessList={() => {}} getAccessList={() => {}}
getUserRoles={() => {}}
/> />
</MemoryRouter> </MemoryRouter>
</I18nProvider> </I18nProvider>
@@ -55,7 +48,6 @@ describe('<AccessList />', () => {
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }} match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
location={{ search: '', pathname: '/organizations/1/access' }} location={{ search: '', pathname: '/organizations/1/access' }}
getAccessList={() => ({ data: { count: 1, results: mockResults } })} getAccessList={() => ({ data: { count: 1, results: mockResults } })}
getUserRoles={() => ({ data: { results: mockUserRoles } })}
/> />
</MemoryRouter> </MemoryRouter>
</I18nProvider> </I18nProvider>
@@ -77,7 +69,6 @@ describe('<AccessList />', () => {
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }} match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
location={{ search: '', pathname: '/organizations/1/access' }} location={{ search: '', pathname: '/organizations/1/access' }}
getAccessList={() => ({ data: { count: 1, results: mockResults } })} getAccessList={() => ({ data: { count: 1, results: mockResults } })}
getUserRoles={() => ({ data: { results: mockUserRoles } })}
/> />
</MemoryRouter> </MemoryRouter>
</I18nProvider> </I18nProvider>
@@ -104,7 +95,6 @@ describe('<AccessList />', () => {
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }} match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
location={{ search: '', pathname: '/organizations/1/access' }} location={{ search: '', pathname: '/organizations/1/access' }}
getAccessList={() => ({ data: { count: 1, results: mockResults } })} getAccessList={() => ({ data: { count: 1, results: mockResults } })}
getUserRoles={() => ({ data: { results: mockUserRoles } })}
/> />
</MemoryRouter> </MemoryRouter>
</I18nProvider> </I18nProvider>
@@ -118,4 +108,53 @@ describe('<AccessList />', () => {
done(); done();
}); });
}); });
test('getTeamRoles returns empty array if dataset is missing team_id attribute', (done) => {
const mockData = [
{
id: 1,
username: 'boo',
url: '/foo/bar/',
first_name: 'john',
last_name: 'smith',
summary_fields: {
foo: [
{
role: {
name: 'foo',
id: 2,
}
}
],
direct_access: [
{
role: {
name: 'team user',
id: 3,
}
}
]
}
}
];
const wrapper = mount(
<I18nProvider>
<MemoryRouter>
<AccessList
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '0' } }}
location={{ search: '', pathname: '/organizations/1/access' }}
getAccessList={() => ({ data: { count: 1, results: mockData } })}
/>
</MemoryRouter>
</I18nProvider>
).find('AccessList');
setImmediate(() => {
const { results } = wrapper.state();
results.forEach(result => {
expect(result.teamRoles).toEqual([]);
});
done();
});
});
}); });

View File

@@ -7,18 +7,11 @@ import OrganizationAccess from '../../../../../src/pages/Organizations/screens/O
const mockAPIAccessList = { const mockAPIAccessList = {
foo: 'bar', foo: 'bar',
}; };
const mockAPIRoles = {
bar: 'baz',
};
const mockGetOrganzationAccessList = jest.fn(() => ( const mockGetOrganzationAccessList = jest.fn(() => (
Promise.resolve(mockAPIAccessList) Promise.resolve(mockAPIAccessList)
)); ));
const mockGetUserRoles = jest.fn(() => (
Promise.resolve(mockAPIRoles)
));
describe('<OrganizationAccess />', () => { describe('<OrganizationAccess />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
mount( mount(
@@ -29,7 +22,6 @@ describe('<OrganizationAccess />', () => {
params={{}} params={{}}
api={{ api={{
getOrganzationAccessList: jest.fn(), getOrganzationAccessList: jest.fn(),
getUserRoles: jest.fn(),
}} }}
/> />
</MemoryRouter> </MemoryRouter>
@@ -45,14 +37,11 @@ describe('<OrganizationAccess />', () => {
params={{}} params={{}}
api={{ api={{
getOrganzationAccessList: mockGetOrganzationAccessList, getOrganzationAccessList: mockGetOrganzationAccessList,
getUserRoles: mockGetUserRoles,
}} }}
/> />
</MemoryRouter> </MemoryRouter>
).find('OrganizationAccess'); ).find('OrganizationAccess');
const accessList = await wrapper.instance().getOrgAccessList(); const accessList = await wrapper.instance().getOrgAccessList();
expect(accessList).toEqual(mockAPIAccessList); expect(accessList).toEqual(mockAPIAccessList);
const userRoles = await wrapper.instance().getUserRoles();
expect(userRoles).toEqual(mockAPIRoles);
}); });
}); });

View File

@@ -5,7 +5,6 @@ const API_V2 = `${API_ROOT}v2/`;
const API_CONFIG = `${API_V2}config/`; const API_CONFIG = `${API_V2}config/`;
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 LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded'; const LOGIN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
@@ -130,12 +129,6 @@ class APIClient {
disassociateInstanceGroup (url, id) { disassociateInstanceGroup (url, id) {
return this.http.post(url, { id, disassociate: true }); return this.http.post(url, { id, disassociate: true });
} }
getUserRoles (id) {
const endpoint = `${API_USERS}${id}/roles/`;
return this.http.get(endpoint);
}
} }
export default APIClient; export default APIClient;

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { import {
DataList, DataListItem, DataListCell, Text, DataList, DataListItem, DataListCell, Text,
TextContent, TextVariants, Badge TextContent, TextVariants
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { I18n, i18nMark } from '@lingui/react'; import { I18n, i18nMark } from '@lingui/react';
@@ -16,7 +16,6 @@ import {
import BasicChip from '../BasicChip/BasicChip'; import BasicChip from '../BasicChip/BasicChip';
import Pagination from '../Pagination'; import Pagination from '../Pagination';
import DataListToolbar from '../DataListToolbar'; import DataListToolbar from '../DataListToolbar';
import Tooltip from '../Tooltip';
import { import {
parseQueryString, parseQueryString,
@@ -47,16 +46,7 @@ const hiddenStyle = {
display: 'none', display: 'none',
}; };
const badgeStyle = { const Detail = ({ label, value, url, customStyles }) => {
borderRadius: 'var(--pf-global--BorderRadius--sm)',
height: '24px',
}
const badgeTextStyle = {
lineHeight: '24px',
}
const Detail = ({ label, value, url, isBadge, customStyles }) => {
let detail = null; let detail = null;
if (value) { if (value) {
detail = ( detail = (
@@ -66,19 +56,29 @@ const Detail = ({ label, value, url, isBadge, customStyles }) => {
<Text component={TextVariants.h6} style={detailLabelStyle}>{label}</Text> <Text component={TextVariants.h6} style={detailLabelStyle}>{label}</Text>
</Link>) : (<Text component={TextVariants.h6} style={detailLabelStyle}>{label}</Text> </Link>) : (<Text component={TextVariants.h6} style={detailLabelStyle}>{label}</Text>
)} )}
{isBadge ? ( <Text component={TextVariants.p} style={detailValueStyle}>{value}</Text>
<Badge style={badgeStyle} isRead>
<Text component={TextVariants.p} style={{...detailValueStyle, ...badgeTextStyle}}>{value}</Text>
</Badge>
) : (
<Text component={TextVariants.p} style={detailValueStyle}>{value}</Text>
)}
</TextContent> </TextContent>
); );
} }
return detail; return detail;
}; };
const UserName = ({ value, url }) => {
let username = null;
if (value) {
username = (
<TextContent style={detailWrapperStyle}>
{url ? (
<Link to={{ pathname: url }}>
<Text component={TextVariants.h6} style={detailLabelStyle}>{value}</Text>
</Link>) : (<Text component={TextVariants.h6} style={detailLabelStyle}>{value}</Text>
)}
</TextContent>
);
}
return username;
};
class AccessList extends React.Component { class AccessList extends React.Component {
columns = [ columns = [
{ name: i18nMark('Name'), key: 'first_name', isSortable: true }, { name: i18nMark('Name'), key: 'first_name', isSortable: true },
@@ -112,8 +112,6 @@ class AccessList extends React.Component {
this.onCompact = this.onCompact.bind(this); this.onCompact = this.onCompact.bind(this);
this.onSort = this.onSort.bind(this); this.onSort = this.onSort.bind(this);
this.getQueryParams = this.getQueryParams.bind(this); this.getQueryParams = this.getQueryParams.bind(this);
this.getRoleType = this.getRoleType.bind(this);
this.fetchUserRoles = this.fetchUserRoles.bind(this);
this.getTeamRoles = this.getTeamRoles.bind(this); this.getTeamRoles = this.getTeamRoles.bind(this);
} }
@@ -173,36 +171,22 @@ class AccessList extends React.Component {
return Object.assign({}, this.defaultParams, searchParams, overrides); return Object.assign({}, this.defaultParams, searchParams, overrides);
} }
getTeamRoles (arr) { getRoles = roles => Object.values(roles)
this.arr = arr; .reduce((val, role) => {
const filtered = this.arr.filter(entry => entry.role.team_id); if (role.length > 0) {
return filtered.reduce((val, item) => { val.push(role[0].role);
if (item.role.team_id) {
const { role } = item;
val = role;
} }
return val; return val;
}, {}); }, []);
}
getRoleType (arr, index, type) { getTeamRoles = roles => roles
return Object.values(arr).filter(value => value.length > 0).map(roleType => { .reduce((val, item) => {
if (type === 'user') { if (item.role.team_id) {
return roleType[index].role.name; const { role } = item;
val.push(role);
} }
if (type === 'team') { return val;
return this.getTeamRoles(roleType); }, []);
}
return null;
});
}
async fetchUserRoles (id) {
const { getUserRoles } = this.props;
const { data: { results: userRoles = [] } } = await getUserRoles(id);
return userRoles;
}
async fetchOrgAccessList (queryParams) { async fetchOrgAccessList (queryParams) {
const { match, getAccessList } = this.props; const { match, getAccessList } = this.props;
@@ -232,25 +216,15 @@ class AccessList extends React.Component {
sortedColumnKey, sortedColumnKey,
results, results,
}; };
results.forEach((result) => {
results.forEach(async result => { if (result.summary_fields.direct_access) {
result.userRoles = []; result.teamRoles = this.getTeamRoles(result.summary_fields.direct_access);
result.teamRoles = []; } else {
result.directRole = null; result.teamRoles = [];
// Grab each Role Type and set as a top-level value
result.directRole = this.getRoleType(result.summary_fields, 0, 'user') || null;
result.teamRoles = this.getRoleType(result.summary_fields, 1, 'team').filter(teamRole => teamRole.id);
// Grab User Roles and set as a top-level value
try {
const roles = await this.fetchUserRoles(result.id);
roles.map(role => result.userRoles.push(role));
this.setState(stateToUpdate);
} catch (error) {
this.setState({ error });
} }
result.userRoles = this.getRoles(result.summary_fields) || [];
}); });
this.setState(stateToUpdate);
} catch (error) { } catch (error) {
this.setState({ error }); this.setState({ error });
} }
@@ -301,11 +275,9 @@ class AccessList extends React.Component {
{results.map(result => ( {results.map(result => (
<DataListItem aria-labelledby={i18n._(t`access-list-item`)} key={result.id}> <DataListItem aria-labelledby={i18n._(t`access-list-item`)} key={result.id}>
<DataListCell> <DataListCell>
<Detail <UserName
label={result.username} value={result.username}
value={result.directRole}
url={result.url} url={result.url}
isBadge
/> />
{result.first_name || result.last_name ? ( {result.first_name || result.last_name ? (
<Detail <Detail
@@ -331,30 +303,12 @@ class AccessList extends React.Component {
: userRolesWrapperStyle} : 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 => (
// Show tooltips for associated Org/Team of role name displayed <BasicChip
if (role.summary_fields.resource_name) { key={role.id}
return ( text={role.name}
<Tooltip />
message={role.summary_fields.resource_name} ))}
position="top"
key={role.id}
>
<BasicChip
key={role.id}
text={role.name}
/>
</Tooltip>
)
} else {
return (
<BasicChip
key={role.id}
text={role.name}
/>
)
}
})}
</ul> </ul>
)} )}
{result.teamRoles.length > 0 && ( {result.teamRoles.length > 0 && (
@@ -388,14 +342,12 @@ class AccessList extends React.Component {
</Fragment> </Fragment>
)} )}
</Fragment> </Fragment>
); );
} }
} }
AccessList.propTypes = { AccessList.propTypes = {
getAccessList: PropTypes.func.isRequired, getAccessList: PropTypes.func.isRequired,
getUserRoles: PropTypes.func.isRequired,
}; };
export default AccessList; export default AccessList;

View File

@@ -38,7 +38,7 @@ const flexGrowStyling = {
}; };
const ToolbarActiveStyle = { const ToolbarActiveStyle = {
backgroundColor: 'rgb(0, 123, 186)', backgroundColor: '#007bba',
color: 'white', color: 'white',
padding: '0 5px', padding: '0 5px',
}; };

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const toolTipContent = { const toolTipContent = {
padding: '10px', padding: '10px',
minWidth: '100px', minWidth: '100px',
} };
class Tooltip extends React.Component { class Tooltip extends React.Component {
transforms = { transforms = {
@@ -64,7 +64,7 @@ class Tooltip extends React.Component {
style={{ position: 'absolute', zIndex: '10', ...this.transforms[position] }} style={{ position: 'absolute', zIndex: '10', ...this.transforms[position] }}
className={`pf-c-tooltip pf-m-${position}`} className={`pf-c-tooltip pf-m-${position}`}
> >
<div className="pf-c-tooltip__arrow"/> <div className="pf-c-tooltip__arrow" />
<div className="pf-c-tooltip__content" style={toolTipContent}> <div className="pf-c-tooltip__content" style={toolTipContent}>
{ message } { message }
</div> </div>

View File

@@ -6,7 +6,6 @@ class OrganizationAccess extends React.Component {
super(props); super(props);
this.getOrgAccessList = this.getOrgAccessList.bind(this); this.getOrgAccessList = this.getOrgAccessList.bind(this);
this.getUserRoles = this.getUserRoles.bind(this);
} }
getOrgAccessList (id, params) { getOrgAccessList (id, params) {
@@ -14,11 +13,6 @@ class OrganizationAccess extends React.Component {
return api.getOrganzationAccessList(id, params); return api.getOrganzationAccessList(id, params);
} }
getUserRoles (id) {
const { api } = this.props;
return api.getUserRoles(id);
}
render () { render () {
const { const {
location, location,
@@ -29,7 +23,6 @@ class OrganizationAccess extends React.Component {
return ( return (
<AccessList <AccessList
getAccessList={this.getOrgAccessList} getAccessList={this.getOrgAccessList}
getUserRoles={this.getUserRoles}
match={match} match={match}
location={location} location={location}
history={history} history={history}