mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 00:38:45 -03:30
Merge pull request #121 from ansible/org-access-list
Organization Access List
This commit is contained in:
160
__tests__/components/AccessList.test.jsx
Normal file
160
__tests__/components/AccessList.test.jsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import AccessList from '../../src/components/AccessList';
|
||||
|
||||
const mockResults = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'boo',
|
||||
url: '/foo/bar/',
|
||||
first_name: 'john',
|
||||
last_name: 'smith',
|
||||
summary_fields: {
|
||||
foo: [
|
||||
{
|
||||
role: {
|
||||
name: 'foo',
|
||||
id: 2,
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
describe('<AccessList />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
mount(
|
||||
<I18nProvider>
|
||||
<MemoryRouter>
|
||||
<AccessList
|
||||
match={{ path: '/organizations/:id', url: '/organizations/1', params: { id: '1' } }}
|
||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||
getAccessList={() => {}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
);
|
||||
});
|
||||
|
||||
test('api response data passed to component gets set to state properly', (done) => {
|
||||
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: mockResults } })}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
).find('AccessList');
|
||||
|
||||
setImmediate(() => {
|
||||
expect(wrapper.state().results).toEqual(mockResults);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('onExpand and onCompact methods called when user clicks on Expand and Compact icons respectively', async (done) => {
|
||||
const onExpand = jest.spyOn(AccessList.prototype, 'onExpand');
|
||||
const onCompact = jest.spyOn(AccessList.prototype, 'onCompact');
|
||||
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: mockResults } })}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
).find('AccessList');
|
||||
expect(onExpand).not.toHaveBeenCalled();
|
||||
expect(onCompact).not.toHaveBeenCalled();
|
||||
|
||||
setImmediate(() => {
|
||||
const rendered = wrapper.update();
|
||||
rendered.find('button[aria-label="Expand"]').simulate('click');
|
||||
rendered.find('button[aria-label="Collapse"]').simulate('click');
|
||||
expect(onExpand).toHaveBeenCalled();
|
||||
expect(onCompact).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('onSort being passed properly to DataListToolbar component', async (done) => {
|
||||
const onSort = jest.spyOn(AccessList.prototype, 'onSort');
|
||||
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: mockResults } })}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</I18nProvider>
|
||||
).find('AccessList');
|
||||
expect(onSort).not.toHaveBeenCalled();
|
||||
|
||||
setImmediate(() => {
|
||||
const rendered = wrapper.update();
|
||||
rendered.find('button[aria-label="Sort"]').simulate('click');
|
||||
expect(onSort).toHaveBeenCalled();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -196,6 +196,7 @@ describe('<Lookup />', () => {
|
||||
lookup_header="Foo Bar"
|
||||
onLookupSave={() => { }}
|
||||
value={mockData}
|
||||
selected={[]}
|
||||
columns={mockColumns}
|
||||
sortedColumnKey="name"
|
||||
getItems={() => { }}
|
||||
@@ -215,6 +216,7 @@ describe('<Lookup />', () => {
|
||||
lookup_header="Foo Bar"
|
||||
onLookupSave={() => { }}
|
||||
value={mockData}
|
||||
selected={[]}
|
||||
columns={mockColumns}
|
||||
sortedColumnKey="name"
|
||||
getItems={() => { }}
|
||||
|
||||
@@ -12,6 +12,11 @@ describe('<Notifications />', () => {
|
||||
<Notifications
|
||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||
getError={jest.fn()}
|
||||
getNotifications={jest.fn()}
|
||||
getSuccess={jest.fn()}
|
||||
postError={jest.fn()}
|
||||
postSuccess={jest.fn()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
@@ -25,6 +30,11 @@ describe('<Notifications />', () => {
|
||||
<Notifications
|
||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||
getError={jest.fn()}
|
||||
getNotifications={jest.fn()}
|
||||
getSuccess={jest.fn()}
|
||||
postError={jest.fn()}
|
||||
postSuccess={jest.fn()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
@@ -39,6 +49,11 @@ describe('<Notifications />', () => {
|
||||
<Notifications
|
||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||
getError={jest.fn()}
|
||||
getNotifications={jest.fn()}
|
||||
getSuccess={jest.fn()}
|
||||
postError={jest.fn()}
|
||||
postSuccess={jest.fn()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
@@ -54,6 +69,10 @@ describe('<Notifications />', () => {
|
||||
<Notifications
|
||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
|
||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||
getError={jest.fn()}
|
||||
getNotifications={jest.fn()}
|
||||
getSuccess={jest.fn()}
|
||||
postError={jest.fn()}
|
||||
postSuccess={postSuccessFn}
|
||||
/>
|
||||
</I18nProvider>
|
||||
@@ -75,6 +94,11 @@ describe('<Notifications />', () => {
|
||||
<Notifications
|
||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||
getError={jest.fn()}
|
||||
getNotifications={jest.fn()}
|
||||
getSuccess={jest.fn()}
|
||||
postError={jest.fn()}
|
||||
postSuccess={jest.fn()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
@@ -90,7 +114,11 @@ describe('<Notifications />', () => {
|
||||
<Notifications
|
||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
|
||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||
getError={jest.fn()}
|
||||
getNotifications={jest.fn()}
|
||||
getSuccess={jest.fn()}
|
||||
postError={postErrorFn}
|
||||
postSuccess={jest.fn()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
@@ -141,6 +169,8 @@ describe('<Notifications />', () => {
|
||||
getNotifications={getNotificationsFn}
|
||||
getSuccess={getSuccessFn}
|
||||
getError={getErrorFn}
|
||||
postError={jest.fn()}
|
||||
postSuccess={jest.fn()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
|
||||
|
||||
const mockAPIAccessList = {
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
const mockGetOrganzationAccessList = jest.fn(() => (
|
||||
Promise.resolve(mockAPIAccessList)
|
||||
));
|
||||
|
||||
describe('<OrganizationAccess />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
mount(
|
||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||
<OrganizationAccess
|
||||
match={{ path: '/organizations/:id/access', url: '/organizations/1/access', params: { id: 1 } }}
|
||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||
params={{}}
|
||||
api={{
|
||||
getOrganzationAccessList: jest.fn(),
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
test('passed methods as props are called appropriately', async () => {
|
||||
const wrapper = mount(
|
||||
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
|
||||
<OrganizationAccess
|
||||
match={{ path: '/organizations/:id/access', url: '/organizations/1/access', params: { id: 1 } }}
|
||||
location={{ search: '', pathname: '/organizations/1/access' }}
|
||||
params={{}}
|
||||
api={{
|
||||
getOrganzationAccessList: mockGetOrganzationAccessList,
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
).find('OrganizationAccess');
|
||||
const accessList = await wrapper.instance().getOrgAccessList();
|
||||
expect(accessList).toEqual(mockAPIAccessList);
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,12 @@ class APIClient {
|
||||
return this.http.post(API_ORGANIZATIONS, data);
|
||||
}
|
||||
|
||||
getOrganzationAccessList (id, params = {}) {
|
||||
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
|
||||
|
||||
return this.http.get(endpoint, { params });
|
||||
}
|
||||
|
||||
getOrganizationDetails (id) {
|
||||
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||
|
||||
|
||||
353
src/components/AccessList/Access.list.jsx
Normal file
353
src/components/AccessList/Access.list.jsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
DataList, DataListItem, DataListCell, Text,
|
||||
TextContent, TextVariants
|
||||
} 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';
|
||||
import DataListToolbar from '../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, customStyles }) => {
|
||||
let detail = null;
|
||||
if (value) {
|
||||
detail = (
|
||||
<TextContent style={{ ...detailWrapperStyle, ...customStyles }}>
|
||||
{url ? (
|
||||
<Link to={{ pathname: url }}>
|
||||
<Text component={TextVariants.h6} style={detailLabelStyle}>{label}</Text>
|
||||
</Link>) : (<Text component={TextVariants.h6} style={detailLabelStyle}>{label}</Text>
|
||||
)}
|
||||
<Text component={TextVariants.p} style={detailValueStyle}>{value}</Text>
|
||||
</TextContent>
|
||||
);
|
||||
}
|
||||
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 {
|
||||
columns = [
|
||||
{ name: i18nMark('Name'), key: 'first_name', isSortable: true },
|
||||
{ name: i18nMark('Username'), key: 'username', isSortable: true },
|
||||
{ name: i18nMark('Last Name'), key: 'last_name', isSortable: true },
|
||||
];
|
||||
|
||||
defaultParams = {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'first_name',
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const { page, page_size } = this.getQueryParams();
|
||||
|
||||
this.state = {
|
||||
page,
|
||||
page_size,
|
||||
count: null,
|
||||
sortOrder: 'ascending',
|
||||
sortedColumnKey: 'username',
|
||||
isCompact: false,
|
||||
};
|
||||
|
||||
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);
|
||||
this.getTeamRoles = this.getTeamRoles.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const queryParams = this.getQueryParams();
|
||||
try {
|
||||
this.fetchOrgAccessList(queryParams);
|
||||
} catch (error) {
|
||||
this.setState({ error });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Preserve sort order when paginating
|
||||
if (sortOrder === 'descending') {
|
||||
order_by = `-${order_by}`;
|
||||
}
|
||||
|
||||
const queryParams = this.getQueryParams({ page, page_size, order_by });
|
||||
|
||||
this.fetchOrgAccessList(queryParams);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
getQueryParams (overrides = {}) {
|
||||
const { location } = this.props;
|
||||
const { search } = location;
|
||||
|
||||
const searchParams = parseQueryString(search.substring(1));
|
||||
|
||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||
}
|
||||
|
||||
getRoles = roles => Object.values(roles)
|
||||
.reduce((val, role) => {
|
||||
if (role.length > 0) {
|
||||
val.push(role[0].role);
|
||||
}
|
||||
return val;
|
||||
}, []);
|
||||
|
||||
getTeamRoles = roles => roles
|
||||
.reduce((val, item) => {
|
||||
if (item.role.team_id) {
|
||||
const { role } = item;
|
||||
val.push(role);
|
||||
}
|
||||
return val;
|
||||
}, []);
|
||||
|
||||
async fetchOrgAccessList (queryParams) {
|
||||
const { match, getAccessList } = 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.forEach((result) => {
|
||||
if (result.summary_fields.direct_access) {
|
||||
result.teamRoles = this.getTeamRoles(result.summary_fields.direct_access);
|
||||
} else {
|
||||
result.teamRoles = [];
|
||||
}
|
||||
result.userRoles = this.getRoles(result.summary_fields) || [];
|
||||
});
|
||||
this.setState(stateToUpdate);
|
||||
} catch (error) {
|
||||
this.setState({ error });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
results,
|
||||
error,
|
||||
count,
|
||||
page_size,
|
||||
pageCount,
|
||||
page,
|
||||
sortedColumnKey,
|
||||
sortOrder,
|
||||
isCompact,
|
||||
} = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
{!error && !results && (
|
||||
<h1>Loading...</h1> // TODO: replace with proper loading state
|
||||
)}
|
||||
{error && !results && (
|
||||
<Fragment>
|
||||
<div>{error.message}</div>
|
||||
{error.response && (
|
||||
<div>{error.response.data.detail}</div>
|
||||
)}
|
||||
</Fragment> // TODO: replace with proper error handling
|
||||
)}
|
||||
{results && (
|
||||
<Fragment>
|
||||
<DataListToolbar
|
||||
sortedColumnKey={sortedColumnKey}
|
||||
sortOrder={sortOrder}
|
||||
columns={this.columns}
|
||||
onSearch={() => { }}
|
||||
onSort={this.onSort}
|
||||
onCompact={this.onCompact}
|
||||
onExpand={this.onExpand}
|
||||
isCompact={isCompact}
|
||||
showExpandCollapse
|
||||
/>
|
||||
<Fragment>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<DataList aria-label={i18n._(t`Access List`)}>
|
||||
{results.map(result => (
|
||||
<DataListItem aria-labelledby={i18n._(t`access-list-item`)} key={result.id}>
|
||||
<DataListCell>
|
||||
<UserName
|
||||
value={result.username}
|
||||
url={result.url}
|
||||
/>
|
||||
{result.first_name || result.last_name ? (
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
value={`${result.first_name} ${result.last_name}`}
|
||||
url={null}
|
||||
customStyles={isCompact ? hiddenStyle : null}
|
||||
/>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
</DataListCell>
|
||||
<DataListCell>
|
||||
<Detail
|
||||
label=" "
|
||||
value=" "
|
||||
url={null}
|
||||
customStyles={isCompact ? hiddenStyle : null}
|
||||
/>
|
||||
{result.userRoles.length > 0 && (
|
||||
<ul style={isCompact
|
||||
? { ...userRolesWrapperStyle, ...hiddenStyle }
|
||||
: userRolesWrapperStyle}
|
||||
>
|
||||
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`User Roles`)}</Text>
|
||||
{result.userRoles.map(role => (
|
||||
<BasicChip
|
||||
key={role.id}
|
||||
text={role.name}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{result.teamRoles.length > 0 && (
|
||||
<ul style={isCompact
|
||||
? { ...userRolesWrapperStyle, ...hiddenStyle }
|
||||
: userRolesWrapperStyle}
|
||||
>
|
||||
<Text component={TextVariants.h6} style={detailLabelStyle}>{i18n._(t`Team Roles`)}</Text>
|
||||
{result.teamRoles.map(role => (
|
||||
<BasicChip
|
||||
key={role.id}
|
||||
text={role.name}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</DataListCell>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
)}
|
||||
</I18n>
|
||||
</Fragment>
|
||||
<Pagination
|
||||
count={count}
|
||||
page={page}
|
||||
pageCount={pageCount}
|
||||
page_size={page_size}
|
||||
onSetPage={this.onSetPage}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccessList.propTypes = {
|
||||
getAccessList: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AccessList;
|
||||
3
src/components/AccessList/index.js
Normal file
3
src/components/AccessList/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import AccessList from './Access.list';
|
||||
|
||||
export default AccessList;
|
||||
@@ -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: '#007bba',
|
||||
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,
|
||||
@@ -245,7 +264,9 @@ class DataListToolbar extends React.Component {
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Expand`)}
|
||||
aria-label={i18n._(t`Collapse`)}
|
||||
onClick={this.onCompact}
|
||||
style={isCompact ? ToolbarActiveStyle : null}
|
||||
>
|
||||
<BarsIcon />
|
||||
</Button>
|
||||
@@ -253,7 +274,9 @@ class DataListToolbar extends React.Component {
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Collapse`)}
|
||||
aria-label={i18n._(t`Expand`)}
|
||||
onClick={this.onExpand}
|
||||
style={!isCompact ? ToolbarActiveStyle : null}
|
||||
>
|
||||
<EqualsIcon />
|
||||
</Button>
|
||||
@@ -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',
|
||||
|
||||
@@ -335,7 +335,7 @@ class Notifications extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.propType = {
|
||||
Notifications.propTypes = {
|
||||
getError: PropTypes.func.isRequired,
|
||||
getNotifications: PropTypes.func.isRequired,
|
||||
getSuccess: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const toolTipContent = {
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
};
|
||||
|
||||
class Tooltip extends React.Component {
|
||||
transforms = {
|
||||
top: {
|
||||
@@ -43,6 +48,9 @@ class Tooltip extends React.Component {
|
||||
isDisplayed
|
||||
} = this.state;
|
||||
|
||||
if (message === '') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
style={{ position: 'relative' }}
|
||||
@@ -57,7 +65,7 @@ class Tooltip extends React.Component {
|
||||
className={`pf-c-tooltip pf-m-${position}`}
|
||||
>
|
||||
<div className="pf-c-tooltip__arrow" />
|
||||
<div className="pf-c-tooltip__content">
|
||||
<div className="pf-c-tooltip__content" style={toolTipContent}>
|
||||
{ message }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PageSection
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import OrganizationAccess from './OrganizationAccess';
|
||||
import OrganizationDetail from './OrganizationDetail';
|
||||
import OrganizationEdit from './OrganizationEdit';
|
||||
import OrganizationNotifications from './OrganizationNotifications';
|
||||
@@ -145,7 +146,14 @@ class Organization extends Component {
|
||||
)}
|
||||
<Route
|
||||
path="/organizations/:id/access"
|
||||
render={() => <CardBody><h1><Trans>Access</Trans></h1></CardBody>}
|
||||
render={() => (
|
||||
<OrganizationAccess
|
||||
api={api}
|
||||
match={match}
|
||||
location={location}
|
||||
history={history}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/organizations/:id/teams"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import AccessList from '../../../../components/AccessList';
|
||||
|
||||
class OrganizationAccess extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.getOrgAccessList = this.getOrgAccessList.bind(this);
|
||||
}
|
||||
|
||||
getOrgAccessList (id, params) {
|
||||
const { api } = this.props;
|
||||
return api.getOrganzationAccessList(id, params);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
location,
|
||||
match,
|
||||
history,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AccessList
|
||||
getAccessList={this.getOrgAccessList}
|
||||
match={match}
|
||||
location={location}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrganizationAccess;
|
||||
Reference in New Issue
Block a user