mirror of
https://github.com/ansible/awx.git
synced 2026-04-10 12:39:22 -02: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"
|
lookup_header="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => { }}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
|
selected={[]}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
@@ -215,6 +216,7 @@ describe('<Lookup />', () => {
|
|||||||
lookup_header="Foo Bar"
|
lookup_header="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => { }}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
|
selected={[]}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
getItems={() => { }}
|
getItems={() => { }}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ describe('<Notifications />', () => {
|
|||||||
<Notifications
|
<Notifications
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/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>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -25,6 +30,11 @@ describe('<Notifications />', () => {
|
|||||||
<Notifications
|
<Notifications
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/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>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -39,6 +49,11 @@ describe('<Notifications />', () => {
|
|||||||
<Notifications
|
<Notifications
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/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>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -54,6 +69,10 @@ describe('<Notifications />', () => {
|
|||||||
<Notifications
|
<Notifications
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
|
||||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||||
|
getError={jest.fn()}
|
||||||
|
getNotifications={jest.fn()}
|
||||||
|
getSuccess={jest.fn()}
|
||||||
|
postError={jest.fn()}
|
||||||
postSuccess={postSuccessFn}
|
postSuccess={postSuccessFn}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
@@ -75,6 +94,11 @@ describe('<Notifications />', () => {
|
|||||||
<Notifications
|
<Notifications
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications' }}
|
||||||
location={{ search: '', pathname: '/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>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -90,7 +114,11 @@ describe('<Notifications />', () => {
|
|||||||
<Notifications
|
<Notifications
|
||||||
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
|
match={{ path: '/organizations/:id/?tab=notifications', url: '/organizations/:id/?tab=notifications', params: { id: 1 } }}
|
||||||
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
location={{ search: '', pathname: '/organizations/:id/?tab=notifications' }}
|
||||||
|
getError={jest.fn()}
|
||||||
|
getNotifications={jest.fn()}
|
||||||
|
getSuccess={jest.fn()}
|
||||||
postError={postErrorFn}
|
postError={postErrorFn}
|
||||||
|
postSuccess={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@@ -141,6 +169,8 @@ describe('<Notifications />', () => {
|
|||||||
getNotifications={getNotificationsFn}
|
getNotifications={getNotificationsFn}
|
||||||
getSuccess={getSuccessFn}
|
getSuccess={getSuccessFn}
|
||||||
getError={getErrorFn}
|
getError={getErrorFn}
|
||||||
|
postError={jest.fn()}
|
||||||
|
postSuccess={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</MemoryRouter>
|
</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);
|
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) {
|
getOrganizationDetails (id) {
|
||||||
const endpoint = `${API_ORGANIZATIONS}${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,
|
SortNumericDownIcon,
|
||||||
SortNumericUpIcon,
|
SortNumericUpIcon,
|
||||||
TrashAltIcon,
|
TrashAltIcon,
|
||||||
PlusIcon
|
PlusIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
import {
|
import {
|
||||||
Link
|
Link
|
||||||
@@ -37,6 +37,12 @@ const flexGrowStyling = {
|
|||||||
flexGrow: '1'
|
flexGrow: '1'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ToolbarActiveStyle = {
|
||||||
|
backgroundColor: '#007bba',
|
||||||
|
color: 'white',
|
||||||
|
padding: '0 5px',
|
||||||
|
};
|
||||||
|
|
||||||
class DataListToolbar extends React.Component {
|
class DataListToolbar extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -56,6 +62,18 @@ class DataListToolbar extends React.Component {
|
|||||||
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
this.onSearchDropdownSelect = this.onSearchDropdownSelect.bind(this);
|
||||||
this.onSearch = this.onSearch.bind(this);
|
this.onSearch = this.onSearch.bind(this);
|
||||||
this.onSort = this.onSort.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) {
|
onSortDropdownToggle (isSortDropdownOpen) {
|
||||||
@@ -114,7 +132,8 @@ class DataListToolbar extends React.Component {
|
|||||||
showExpandCollapse,
|
showExpandCollapse,
|
||||||
showDelete,
|
showDelete,
|
||||||
showSelectAll,
|
showSelectAll,
|
||||||
isLookup
|
isLookup,
|
||||||
|
isCompact,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
isSearchDropdownOpen,
|
isSearchDropdownOpen,
|
||||||
@@ -245,7 +264,9 @@ class DataListToolbar extends React.Component {
|
|||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
aria-label={i18n._(t`Expand`)}
|
aria-label={i18n._(t`Collapse`)}
|
||||||
|
onClick={this.onCompact}
|
||||||
|
style={isCompact ? ToolbarActiveStyle : null}
|
||||||
>
|
>
|
||||||
<BarsIcon />
|
<BarsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -253,7 +274,9 @@ class DataListToolbar extends React.Component {
|
|||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
aria-label={i18n._(t`Collapse`)}
|
aria-label={i18n._(t`Expand`)}
|
||||||
|
onClick={this.onExpand}
|
||||||
|
style={!isCompact ? ToolbarActiveStyle : null}
|
||||||
>
|
>
|
||||||
<EqualsIcon />
|
<EqualsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -306,6 +329,7 @@ DataListToolbar.propTypes = {
|
|||||||
onSelectAll: PropTypes.func,
|
onSelectAll: PropTypes.func,
|
||||||
onSort: PropTypes.func,
|
onSort: PropTypes.func,
|
||||||
showDelete: PropTypes.bool,
|
showDelete: PropTypes.bool,
|
||||||
|
showExpandCollapse: PropTypes.bool,
|
||||||
showSelectAll: PropTypes.bool,
|
showSelectAll: PropTypes.bool,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
sortedColumnKey: PropTypes.string,
|
sortedColumnKey: PropTypes.string,
|
||||||
@@ -317,6 +341,7 @@ DataListToolbar.defaultProps = {
|
|||||||
onSelectAll: null,
|
onSelectAll: null,
|
||||||
onSort: null,
|
onSort: null,
|
||||||
showDelete: false,
|
showDelete: false,
|
||||||
|
showExpandCollapse: false,
|
||||||
showSelectAll: false,
|
showSelectAll: false,
|
||||||
sortOrder: 'ascending',
|
sortOrder: 'ascending',
|
||||||
sortedColumnKey: 'name',
|
sortedColumnKey: 'name',
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ class Notifications extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications.propType = {
|
Notifications.propTypes = {
|
||||||
getError: PropTypes.func.isRequired,
|
getError: PropTypes.func.isRequired,
|
||||||
getNotifications: PropTypes.func.isRequired,
|
getNotifications: PropTypes.func.isRequired,
|
||||||
getSuccess: PropTypes.func.isRequired,
|
getSuccess: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const toolTipContent = {
|
||||||
|
padding: '10px',
|
||||||
|
minWidth: '100px',
|
||||||
|
};
|
||||||
|
|
||||||
class Tooltip extends React.Component {
|
class Tooltip extends React.Component {
|
||||||
transforms = {
|
transforms = {
|
||||||
top: {
|
top: {
|
||||||
@@ -43,6 +48,9 @@ class Tooltip extends React.Component {
|
|||||||
isDisplayed
|
isDisplayed
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
if (message === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={{ position: 'relative' }}
|
style={{ position: 'relative' }}
|
||||||
@@ -57,7 +65,7 @@ class Tooltip extends React.Component {
|
|||||||
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">
|
<div className="pf-c-tooltip__content" style={toolTipContent}>
|
||||||
{ message }
|
{ message }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
PageSection
|
PageSection
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import OrganizationAccess from './OrganizationAccess';
|
||||||
import OrganizationDetail from './OrganizationDetail';
|
import OrganizationDetail from './OrganizationDetail';
|
||||||
import OrganizationEdit from './OrganizationEdit';
|
import OrganizationEdit from './OrganizationEdit';
|
||||||
import OrganizationNotifications from './OrganizationNotifications';
|
import OrganizationNotifications from './OrganizationNotifications';
|
||||||
@@ -145,7 +146,14 @@ class Organization extends Component {
|
|||||||
)}
|
)}
|
||||||
<Route
|
<Route
|
||||||
path="/organizations/:id/access"
|
path="/organizations/:id/access"
|
||||||
render={() => <CardBody><h1><Trans>Access</Trans></h1></CardBody>}
|
render={() => (
|
||||||
|
<OrganizationAccess
|
||||||
|
api={api}
|
||||||
|
match={match}
|
||||||
|
location={location}
|
||||||
|
history={history}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/organizations/:id/teams"
|
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