Merge pull request #5129 from mabashian/resource-access-tabs

Configures access tabs for job template, project, inventory and smart inventory details

Reviewed-by: Michael Abashian
             https://github.com/mabashian
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-10-31 00:42:37 +00:00 committed by GitHub
commit 51184ba20d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 111 additions and 105 deletions

View File

@ -4,6 +4,12 @@ class Inventories extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/inventories/';
this.readAccessList = this.readAccessList.bind(this);
}
readAccessList(id, params) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
}
}

View File

@ -12,6 +12,7 @@ class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
this.associateLabel = this.associateLabel.bind(this);
this.disassociateLabel = this.disassociateLabel.bind(this);
this.readCredentials = this.readCredentials.bind(this);
this.readAccessList = this.readAccessList.bind(this);
this.generateLabel = this.generateLabel.bind(this);
}
@ -57,6 +58,10 @@ class JobTemplates extends InstanceGroupsMixin(NotificationsMixin(Base)) {
disassociate: true,
});
}
readAccessList(id, params) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
}
}
export default JobTemplates;

View File

@ -7,11 +7,16 @@ class Projects extends LaunchUpdateMixin(NotificationsMixin(Base)) {
super(http);
this.baseUrl = '/api/v2/projects/';
this.readAccessList = this.readAccessList.bind(this);
this.readPlaybooks = this.readPlaybooks.bind(this);
this.readSync = this.readSync.bind(this);
this.sync = this.sync.bind(this);
}
readAccessList(id, params) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
}
readPlaybooks(id) {
return this.http.get(`${this.baseUrl}${id}/playbooks/`);
}

View File

@ -39,7 +39,7 @@ class DeleteRoleConfirmationModal extends React.Component {
<Button
key="delete"
variant="danger"
aria-label="Confirm delete"
aria-label={i18n._(t`Confirm delete`)}
onClick={onConfirm}
>
{i18n._(t`Delete`)}
@ -57,11 +57,7 @@ class DeleteRoleConfirmationModal extends React.Component {
<br />
<br />
{i18n._(
t`If you ${(
<b>
<i>only</i>
</b>
)} want to remove access for this particular user, please remove them from the team.`
t`If you only want to remove access for this particular user, please remove them from the team.`
)}
</Fragment>
) : (

View File

@ -3,7 +3,7 @@ import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '@api';
import { TeamsAPI, UsersAPI } from '@api';
import AddResourceRole from '@components/AddRole/AddResourceRole';
import AlertModal from '@components/AlertModal';
import DataListToolbar from '@components/DataListToolbar';
@ -11,10 +11,9 @@ import PaginatedDataList, {
ToolbarAddButton,
} from '@components/PaginatedDataList';
import { getQSConfig, encodeQueryString, parseQueryString } from '@util/qs';
import { Organization } from '@types';
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
import OrganizationAccessItem from './OrganizationAccessItem';
import ResourceAccessListItem from './ResourceAccessListItem';
const QS_CONFIG = getQSConfig('access', {
page: 1,
@ -22,11 +21,7 @@ const QS_CONFIG = getQSConfig('access', {
order_by: 'first_name',
});
class OrganizationAccess extends React.Component {
static propTypes = {
organization: Organization.isRequired,
};
class ResourceAccessList extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -65,14 +60,14 @@ class OrganizationAccess extends React.Component {
}
async loadAccessList() {
const { organization, location } = this.props;
const { apiModel, resource, location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ contentError: null, hasContentLoading: true });
try {
const {
data: { results: accessRecords = [], count: itemCount = 0 },
} = await OrganizationsAPI.readAccessList(organization.id, params);
} = await apiModel.readAccessList(resource.id, params);
this.setState({ itemCount, accessRecords });
} catch (err) {
this.setState({ contentError: err });
@ -143,7 +138,7 @@ class OrganizationAccess extends React.Component {
}
render() {
const { organization, i18n } = this.props;
const { resource, i18n } = this.props;
const {
accessRecords,
contentError,
@ -154,7 +149,7 @@ class OrganizationAccess extends React.Component {
itemCount,
isAddModalOpen,
} = this.state;
const canEdit = organization.summary_fields.user_capabilities.edit;
const canEdit = resource.summary_fields.user_capabilities.edit;
const isDeleteModalOpen =
!hasContentLoading && !hasDeletionError && deletionRole;
@ -204,7 +199,7 @@ class OrganizationAccess extends React.Component {
/>
)}
renderItem={accessRecord => (
<OrganizationAccessItem
<ResourceAccessListItem
key={accessRecord.id}
accessRecord={accessRecord}
onRoleDelete={this.handleDeleteOpen}
@ -215,7 +210,7 @@ class OrganizationAccess extends React.Component {
<AddResourceRole
onClose={this.handleAddClose}
onSave={this.handleAddSuccess}
roles={organization.summary_fields.object_roles}
roles={resource.summary_fields.object_roles}
/>
)}
{isDeleteModalOpen && (
@ -239,5 +234,5 @@ class OrganizationAccess extends React.Component {
}
}
export { OrganizationAccess as _OrganizationAccess };
export default withI18n()(withRouter(OrganizationAccess));
export { ResourceAccessList as _ResourceAccessList };
export default withI18n()(withRouter(ResourceAccessList));

View File

@ -5,11 +5,11 @@ import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '@api';
import OrganizationAccess from './OrganizationAccess';
import ResourceAccessList from './ResourceAccessList';
jest.mock('@api');
describe('<OrganizationAccess />', () => {
describe('<ResourceAccessList />', () => {
const organization = {
id: 1,
name: 'Default',
@ -83,33 +83,33 @@ describe('<OrganizationAccess />', () => {
test('initially renders succesfully', () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
expect(wrapper.find('PaginatedDataList')).toHaveLength(1);
});
test('should fetch and display access records on mount', async done => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
await waitForElement(
wrapper,
'OrganizationAccessItem',
'ResourceAccessListItem',
el => el.length === 2
);
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(
data.results
);
expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe(
expect(wrapper.find('ResourceAccessList').state('hasContentLoading')).toBe(
false
);
expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(null);
expect(wrapper.find('ResourceAccessList').state('contentError')).toBe(null);
done();
});
test('should open confirmation dialog when deleting role', async done => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
await sleep(0);
wrapper.update();
@ -118,7 +118,7 @@ describe('<OrganizationAccess />', () => {
button.prop('onClick')();
wrapper.update();
const component = wrapper.find('OrganizationAccess');
const component = wrapper.find('ResourceAccessList');
expect(component.state('deletionRole')).toEqual(
data.results[0].summary_fields.direct_access[0].role
);
@ -129,7 +129,7 @@ describe('<OrganizationAccess />', () => {
it('should close dialog when cancel button clicked', async done => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
await sleep(0);
wrapper.update();
@ -138,7 +138,7 @@ describe('<OrganizationAccess />', () => {
wrapper.update();
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
const component = wrapper.find('OrganizationAccess');
const component = wrapper.find('ResourceAccessList');
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
@ -148,7 +148,7 @@ describe('<OrganizationAccess />', () => {
it('should delete user role', async done => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
const button = await waitForElement(
wrapper,
@ -170,7 +170,7 @@ describe('<OrganizationAccess />', () => {
await sleep(0);
wrapper.update();
const component = wrapper.find('OrganizationAccess');
const component = wrapper.find('ResourceAccessList');
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
@ -181,7 +181,7 @@ describe('<OrganizationAccess />', () => {
it('should delete team role', async done => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
);
const button = await waitForElement(
wrapper,
@ -203,7 +203,7 @@ describe('<OrganizationAccess />', () => {
await sleep(0);
wrapper.update();
const component = wrapper.find('OrganizationAccess');
const component = wrapper.find('ResourceAccessList');
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);

View File

@ -22,7 +22,7 @@ const DataListItemCells = styled(PFDataListItemCells)`
align-items: start;
`;
class OrganizationAccessItem extends React.Component {
class ResourceAccessListItem extends React.Component {
static propTypes = {
accessRecord: AccessRecord.isRequired,
onRoleDelete: func.isRequired,
@ -132,4 +132,4 @@ class OrganizationAccessItem extends React.Component {
}
}
export default withI18n()(OrganizationAccessItem);
export default withI18n()(ResourceAccessListItem);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import OrganizationAccessItem from './OrganizationAccessItem';
import ResourceAccessListItem from './ResourceAccessListItem';
const accessRecord = {
id: 2,
@ -28,14 +28,14 @@ const accessRecord = {
},
};
describe('<OrganizationAccessItem />', () => {
describe('<ResourceAccessListItem />', () => {
test('initially renders succesfully', () => {
const wrapper = mountWithContexts(
<OrganizationAccessItem
<ResourceAccessListItem
accessRecord={accessRecord}
onRoleDelete={() => {}}
/>
);
expect(wrapper.find('OrganizationAccessItem')).toMatchSnapshot();
expect(wrapper.find('ResourceAccessListItem')).toMatchSnapshot();
});
});

View File

@ -110,7 +110,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.
<br />
<br />
If you {0} want to remove access for this particular user, please remove them from the team.
If you only want to remove access for this particular user, please remove them from the team.
<svg
aria-hidden="true"
class="at-c-alertModal__icon"
@ -211,7 +211,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.
<br />
<br />
If you {0} want to remove access for this particular user, please remove them from the team.
If you only want to remove access for this particular user, please remove them from the team.
<svg
aria-hidden="true"
class="at-c-alertModal__icon"
@ -422,7 +422,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.
<br />
<br />
If you {0} want to remove access for this particular user, please remove them from the team.
If you only want to remove access for this particular user, please remove them from the team.
<ExclamationCircleIcon
className="at-c-alertModal__icon"
color="currentColor"

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
<OrganizationAccessItem
exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
<ResourceAccessListItem
accessRecord={
Object {
"first_name": "jane",
@ -47,7 +47,7 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
<div
className="pf-c-data-list__item-row"
>
<OrganizationAccessItem__DataListItemCells
<ResourceAccessListItem__DataListItemCells
dataListCells={
Array [
<DataListCell>
@ -157,17 +157,17 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
"$$typeof": Symbol(react.forward_ref),
"attrs": Array [],
"componentStyle": ComponentStyle {
"componentId": "OrganizationAccessItem__DataListItemCells-sc-1b9e0ad-0",
"componentId": "ResourceAccessListItem__DataListItemCells-a3r9sq-0",
"isStatic": true,
"lastClassName": "QeteT",
"lastClassName": "WzRoT",
"rules": Array [
"align-items:start;",
],
},
"displayName": "OrganizationAccessItem__DataListItemCells",
"displayName": "ResourceAccessListItem__DataListItemCells",
"foldedComponentIds": Array [],
"render": [Function],
"styledComponentId": "OrganizationAccessItem__DataListItemCells-sc-1b9e0ad-0",
"styledComponentId": "ResourceAccessListItem__DataListItemCells-a3r9sq-0",
"target": [Function],
"toString": [Function],
"warnTooManyClasses": [Function],
@ -178,7 +178,7 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
rowid="access-list-item"
>
<DataListItemCells
className="OrganizationAccessItem__DataListItemCells-sc-1b9e0ad-0 QeteT"
className="ResourceAccessListItem__DataListItemCells-a3r9sq-0 WzRoT"
dataListCells={
Array [
<DataListCell>
@ -232,7 +232,7 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
rowid="access-list-item"
>
<div
className="pf-c-data-list__item-content OrganizationAccessItem__DataListItemCells-sc-1b9e0ad-0 QeteT"
className="pf-c-data-list__item-content ResourceAccessListItem__DataListItemCells-a3r9sq-0 WzRoT"
>
<DataListCell
key="name"
@ -887,10 +887,10 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
</div>
</DataListItemCells>
</StyledComponent>
</OrganizationAccessItem__DataListItemCells>
</ResourceAccessListItem__DataListItemCells>
</div>
</DataListItemRow>
</li>
</DataListItem>
</OrganizationAccessItem>
</ResourceAccessListItem>
`;

View File

@ -0,0 +1,5 @@
export { default as ResourceAccessList } from './ResourceAccessList';
export { default as ResourceAccessListItem } from './ResourceAccessListItem';
export {
default as DeleteRoleConfirmationModal,
} from './DeleteRoleConfirmationModal';

View File

@ -6,12 +6,12 @@ import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
import InventoryDetail from './InventoryDetail';
import InventoryAccess from './InventoryAccess';
import InventoryHosts from './InventoryHosts';
import InventoryGroups from './InventoryGroups';
import InventorySources from './InventorySources';
import InventoryCompletedJobs from './InventoryCompletedJobs';
import InventorySources from './InventorySources';
import { InventoriesAPI } from '@api';
import InventoryEdit from './InventoryEdit';
@ -137,7 +137,12 @@ class Inventory extends Component {
<Route
key="access"
path="/inventories/inventory/:id/access"
render={() => <InventoryAccess inventory={inventory} />}
render={() => (
<ResourceAccessList
resource={inventory}
apiModel={InventoriesAPI}
/>
)}
/>,
<Route
key="groups"

View File

@ -1,10 +0,0 @@
import React, { Component } from 'react';
import { CardBody } from '@patternfly/react-core';
class InventoryAccess extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
}
}
export default InventoryAccess;

View File

@ -1 +0,0 @@
export { default } from './InventoryAccess';

View File

@ -6,8 +6,8 @@ import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
import SmartInventoryDetail from './SmartInventoryDetail';
import SmartInventoryAccess from './SmartInventoryAccess';
import SmartInventoryHosts from './SmartInventoryHosts';
import SmartInventoryCompletedJobs from './SmartInventoryCompletedJobs';
import { InventoriesAPI } from '@api';
@ -132,7 +132,12 @@ class SmartInventory extends Component {
<Route
key="access"
path="/inventories/smart_inventory/:id/access"
render={() => <SmartInventoryAccess inventory={inventory} />}
render={() => (
<ResourceAccessList
resource={inventory}
apiModel={InventoriesAPI}
/>
)}
/>,
<Route
key="hosts"

View File

@ -1,10 +0,0 @@
import React, { Component } from 'react';
import { CardBody } from '@patternfly/react-core';
class SmartInventoryAccess extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
}
}
export default SmartInventoryAccess;

View File

@ -1 +0,0 @@
export { default } from './SmartInventoryAccess';

View File

@ -12,7 +12,7 @@ import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList/NotificationList';
import { OrganizationAccess } from './OrganizationAccess';
import { ResourceAccessList } from '@components/ResourceAccessList';
import OrganizationDetail from './OrganizationDetail';
import OrganizationEdit from './OrganizationEdit';
import OrganizationTeams from './OrganizationTeams';
@ -216,7 +216,10 @@ class Organization extends Component {
<Route
path="/organizations/:id/access"
render={() => (
<OrganizationAccess organization={organization} />
<ResourceAccessList
resource={organization}
apiModel={OrganizationsAPI}
/>
)}
/>
)}

View File

@ -1,5 +0,0 @@
export { default as OrganizationAccess } from './OrganizationAccess';
export { default as OrganizationAccessItem } from './OrganizationAccessItem';
export {
default as DeleteRoleConfirmationModal,
} from './DeleteRoleConfirmationModal';

View File

@ -12,7 +12,7 @@ import CardCloseButton from '@components/CardCloseButton';
import RoutedTabs from '@components/RoutedTabs';
import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList';
import ProjectAccess from './ProjectAccess';
import { ResourceAccessList } from '@components/ResourceAccessList';
import ProjectDetail from './ProjectDetail';
import ProjectEdit from './ProjectEdit';
import ProjectJobTemplates from './ProjectJobTemplates';
@ -222,7 +222,12 @@ class Project extends Component {
{project && (
<Route
path="/projects/:id/access"
render={() => <ProjectAccess project={project} />}
render={() => (
<ResourceAccessList
resource={project}
apiModel={ProjectsAPI}
/>
)}
/>
)}
{canSeeNotificationsTab && (

View File

@ -1,10 +0,0 @@
import React, { Component } from 'react';
import { CardBody } from '@patternfly/react-core';
class ProjectAccess extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
}
}
export default ProjectAccess;

View File

@ -1 +0,0 @@
export { default } from './ProjectAccess';

View File

@ -7,6 +7,7 @@ import CardCloseButton from '@components/CardCloseButton';
import ContentError from '@components/ContentError';
import NotificationList from '@components/NotificationList';
import RoutedTabs from '@components/RoutedTabs';
import { ResourceAccessList } from '@components/ResourceAccessList';
import JobTemplateDetail from './JobTemplateDetail';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import JobTemplateEdit from './JobTemplateEdit';
@ -90,7 +91,7 @@ class Template extends Component {
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: '/home' },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
if (canSeeNotificationsTab) {
@ -177,6 +178,18 @@ class Template extends Component {
render={() => <JobTemplateEdit template={template} />}
/>
)}
{template && (
<Route
key="access"
path="/templates/:templateType/:id/access"
render={() => (
<ResourceAccessList
resource={template}
apiModel={JobTemplatesAPI}
/>
)}
/>
)}
{canSeeNotificationsTab && (
<Route
path="/templates/:templateType/:id/notifications"

View File

@ -40,6 +40,7 @@ class Templates extends Component {
[`/templates/${template.type}/${template.id}/notifications`]: i18n._(
t`Notifications`
),
[`/templates/${template.type}/${template.id}/access`]: i18n._(t`Access`),
};
this.setState({ breadcrumbConfig });
};