mirror of
https://github.com/ansible/awx.git
synced 2026-03-26 21:35:01 -02:30
Merge pull request #173 from mabashian/151-org-rbac
Add RBAC to org views (now with tests!)
This commit is contained in:
@@ -171,6 +171,7 @@ Here are the guidelines for how to name functions.
|
|||||||
|`replace<x>`| Use for methods that make API `PUT` requests |
|
|`replace<x>`| Use for methods that make API `PUT` requests |
|
||||||
|`disassociate<x>`| Use for methods that pass `{ disassociate: true }` as a data param to an endpoint |
|
|`disassociate<x>`| Use for methods that pass `{ disassociate: true }` as a data param to an endpoint |
|
||||||
|`associate<x>`| Use for methods that pass a resource id as a data param to an endpoint |
|
|`associate<x>`| Use for methods that pass a resource id as a data param to an endpoint |
|
||||||
|
|`can<x>`| Use for props dealing with RBAC to denote whether a user has access to something |
|
||||||
|
|
||||||
### Default State Initialization
|
### Default State Initialization
|
||||||
When declaring empty initial states, prefer the following instead of leaving them undefined:
|
When declaring empty initial states, prefer the following instead of leaving them undefined:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={() => {}}
|
onReadSuccess={() => {}}
|
||||||
onCreateError={() => {}}
|
onCreateError={() => {}}
|
||||||
onCreateSuccess={() => {}}
|
onCreateSuccess={() => {}}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -24,6 +25,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={() => {}}
|
onReadSuccess={() => {}}
|
||||||
onCreateError={() => {}}
|
onCreateError={() => {}}
|
||||||
onCreateSuccess={() => {}}
|
onCreateSuccess={() => {}}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
@@ -38,6 +40,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={() => {}}
|
onReadSuccess={() => {}}
|
||||||
onCreateError={() => {}}
|
onCreateError={() => {}}
|
||||||
onCreateSuccess={() => {}}
|
onCreateSuccess={() => {}}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.instance().toggleNotification(1, true, 'success');
|
wrapper.instance().toggleNotification(1, true, 'success');
|
||||||
@@ -56,6 +59,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={() => {}}
|
onReadSuccess={() => {}}
|
||||||
onCreateError={() => {}}
|
onCreateError={() => {}}
|
||||||
onCreateSuccess={onCreateSuccess}
|
onCreateSuccess={onCreateSuccess}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.setState({ successTemplateIds: [44] });
|
wrapper.setState({ successTemplateIds: [44] });
|
||||||
@@ -76,6 +80,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={() => {}}
|
onReadSuccess={() => {}}
|
||||||
onCreateError={() => {}}
|
onCreateError={() => {}}
|
||||||
onCreateSuccess={() => {}}
|
onCreateSuccess={() => {}}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.instance().toggleNotification(1, true, 'error');
|
wrapper.instance().toggleNotification(1, true, 'error');
|
||||||
@@ -94,6 +99,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={() => {}}
|
onReadSuccess={() => {}}
|
||||||
onCreateError={onCreateError}
|
onCreateError={onCreateError}
|
||||||
onCreateSuccess={() => {}}
|
onCreateSuccess={() => {}}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.setState({ errorTemplateIds: [44] });
|
wrapper.setState({ errorTemplateIds: [44] });
|
||||||
@@ -144,6 +150,7 @@ describe('<Notifications />', () => {
|
|||||||
onReadSuccess={onReadSuccess}
|
onReadSuccess={onReadSuccess}
|
||||||
onCreateError={() => {}}
|
onCreateError={() => {}}
|
||||||
onCreateSuccess={() => {}}
|
onCreateSuccess={() => {}}
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
).find('Notifications');
|
).find('Notifications');
|
||||||
wrapper.instance().updateUrl = jest.fn();
|
wrapper.instance().updateUrl = jest.fn();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe('<NotificationListItem />', () => {
|
|||||||
toggleNotification={toggleNotification}
|
toggleNotification={toggleNotification}
|
||||||
detailUrl="/foo"
|
detailUrl="/foo"
|
||||||
notificationType="slack"
|
notificationType="slack"
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.length).toBe(1);
|
expect(wrapper.length).toBe(1);
|
||||||
@@ -33,6 +34,7 @@ describe('<NotificationListItem />', () => {
|
|||||||
toggleNotification={toggleNotification}
|
toggleNotification={toggleNotification}
|
||||||
detailUrl="/foo"
|
detailUrl="/foo"
|
||||||
notificationType="slack"
|
notificationType="slack"
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Switch').first().find('input').simulate('change');
|
wrapper.find('Switch').first().find('input').simulate('change');
|
||||||
@@ -47,6 +49,7 @@ describe('<NotificationListItem />', () => {
|
|||||||
toggleNotification={toggleNotification}
|
toggleNotification={toggleNotification}
|
||||||
detailUrl="/foo"
|
detailUrl="/foo"
|
||||||
notificationType="slack"
|
notificationType="slack"
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Switch').first().find('input').simulate('change');
|
wrapper.find('Switch').first().find('input').simulate('change');
|
||||||
@@ -61,6 +64,7 @@ describe('<NotificationListItem />', () => {
|
|||||||
toggleNotification={toggleNotification}
|
toggleNotification={toggleNotification}
|
||||||
detailUrl="/foo"
|
detailUrl="/foo"
|
||||||
notificationType="slack"
|
notificationType="slack"
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Switch').at(1).find('input').simulate('change');
|
wrapper.find('Switch').at(1).find('input').simulate('change');
|
||||||
@@ -75,6 +79,7 @@ describe('<NotificationListItem />', () => {
|
|||||||
toggleNotification={toggleNotification}
|
toggleNotification={toggleNotification}
|
||||||
detailUrl="/foo"
|
detailUrl="/foo"
|
||||||
notificationType="slack"
|
notificationType="slack"
|
||||||
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Switch').at(1).find('input').simulate('change');
|
wrapper.find('Switch').at(1).find('input').simulate('change');
|
||||||
|
|||||||
@@ -16,13 +16,31 @@ const mockData = [
|
|||||||
role: {
|
role: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 2,
|
id: 2,
|
||||||
|
user_capabilities: {
|
||||||
|
unattach: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const organization = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Default',
|
||||||
|
summary_fields: {
|
||||||
|
object_roles: {},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
foo: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
describe('<OrganizationAccessList />', () => {
|
describe('<OrganizationAccessList />', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
@@ -33,7 +51,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
<OrganizationAccessList
|
<OrganizationAccessList
|
||||||
getAccessList={() => {}}
|
getAccessList={() => {}}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
/>
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,7 +61,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
<OrganizationAccessList
|
<OrganizationAccessList
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
/>
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
).find('OrganizationAccessList');
|
).find('OrganizationAccessList');
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
@@ -57,7 +77,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
<OrganizationAccessList
|
<OrganizationAccessList
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
/>
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
).find('OrganizationAccessList');
|
).find('OrganizationAccessList');
|
||||||
expect(onSort).not.toHaveBeenCalled();
|
expect(onSort).not.toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -74,7 +95,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
<OrganizationAccessList
|
<OrganizationAccessList
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
/>
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
).find('OrganizationAccessList');
|
).find('OrganizationAccessList');
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
@@ -94,7 +116,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
<OrganizationAccessList
|
<OrganizationAccessList
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
/>
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
).find('OrganizationAccessList');
|
).find('OrganizationAccessList');
|
||||||
expect(handleWarning).not.toHaveBeenCalled();
|
expect(handleWarning).not.toHaveBeenCalled();
|
||||||
expect(confirmDelete).not.toHaveBeenCalled();
|
expect(confirmDelete).not.toHaveBeenCalled();
|
||||||
@@ -117,7 +140,8 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
<OrganizationAccessList
|
<OrganizationAccessList
|
||||||
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
removeRole={() => {}}
|
removeRole={() => {}}
|
||||||
/>
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
).find('OrganizationAccessList');
|
).find('OrganizationAccessList');
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
@@ -146,4 +170,36 @@ describe('<OrganizationAccessList />', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('add role button visible for user that can edit org', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<OrganizationAccessList
|
||||||
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
|
removeRole={() => {}}
|
||||||
|
organization={organization}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
|
).find('OrganizationAccessList');
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
const addRole = wrapper.update().find('DataListToolbar').find('PlusIcon');
|
||||||
|
expect(addRole.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add role button hidden for user that cannot edit org', () => {
|
||||||
|
const readOnlyOrg = { ...organization };
|
||||||
|
readOnlyOrg.summary_fields.user_capabilities.edit = false;
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<OrganizationAccessList
|
||||||
|
getAccessList={() => ({ data: { count: 1, results: mockData } })}
|
||||||
|
removeRole={() => {}}
|
||||||
|
organization={readOnlyOrg}
|
||||||
|
/>, { context: { network: { api } } }
|
||||||
|
).find('OrganizationAccessList');
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
const addRole = wrapper.update().find('DataListToolbar').find('PlusIcon');
|
||||||
|
expect(addRole.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,21 @@ import { mountWithContexts } from '../../../../enzymeHelpers';
|
|||||||
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
|
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
|
||||||
|
|
||||||
describe('<OrganizationView />', () => {
|
describe('<OrganizationView />', () => {
|
||||||
|
const me = {
|
||||||
|
is_super_user: true,
|
||||||
|
is_system_auditor: false
|
||||||
|
};
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<Organization />);
|
mountWithContexts(<Organization me={me} />);
|
||||||
|
});
|
||||||
|
test('notifications tab shown/hidden based on permissions', () => {
|
||||||
|
const wrapper = mountWithContexts(<Organization me={me} />);
|
||||||
|
expect(wrapper.find('.pf-c-tabs__item').length).toBe(3);
|
||||||
|
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(0);
|
||||||
|
wrapper.find('Organization').setState({
|
||||||
|
isNotifAdmin: true
|
||||||
|
});
|
||||||
|
expect(wrapper.find('.pf-c-tabs__item').length).toBe(4);
|
||||||
|
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import { mountWithContexts } from '../../../../enzymeHelpers';
|
|||||||
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
|
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
|
||||||
|
|
||||||
describe('<OrganizationAccess />', () => {
|
describe('<OrganizationAccess />', () => {
|
||||||
|
const organization = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Default'
|
||||||
|
};
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<OrganizationAccess />);
|
mountWithContexts(<OrganizationAccess organization={organization} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('passed methods as props are called appropriately', async () => {
|
test('passed methods as props are called appropriately', async () => {
|
||||||
@@ -14,13 +18,14 @@ describe('<OrganizationAccess />', () => {
|
|||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(<OrganizationAccess />, { context: { network: {
|
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />,
|
||||||
api: {
|
{ context: { network: {
|
||||||
getOrganizationAccessList: () => Promise.resolve(mockAPIAccessList),
|
api: {
|
||||||
disassociate: () => Promise.resolve(mockResponse)
|
getOrganizationAccessList: () => Promise.resolve(mockAPIAccessList),
|
||||||
},
|
disassociate: () => Promise.resolve(mockResponse)
|
||||||
handleHttpError: () => {}
|
},
|
||||||
} } }).find('OrganizationAccess');
|
handleHttpError: () => {}
|
||||||
|
} } }).find('OrganizationAccess');
|
||||||
const accessList = await wrapper.instance().getOrgAccessList();
|
const accessList = await wrapper.instance().getOrgAccessList();
|
||||||
expect(accessList).toEqual(mockAPIAccessList);
|
expect(accessList).toEqual(mockAPIAccessList);
|
||||||
const resp = await wrapper.instance().removeRole(2, 3, 'users');
|
const resp = await wrapper.instance().removeRole(2, 3, 'users');
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ describe('<OrganizationDetail />', () => {
|
|||||||
description: 'Bar',
|
description: 'Bar',
|
||||||
custom_virtualenv: 'Fizz',
|
custom_virtualenv: 'Fizz',
|
||||||
created: 'Bat',
|
created: 'Bat',
|
||||||
modified: 'Boo'
|
modified: 'Boo',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
@@ -83,4 +88,28 @@ describe('<OrganizationDetail />', () => {
|
|||||||
expect(modifiedDetail.find('h6').text()).toBe('Last Modified');
|
expect(modifiedDetail.find('h6').text()).toBe('Last Modified');
|
||||||
expect(modifiedDetail.find('p').text()).toBe('Boo');
|
expect(modifiedDetail.find('p').text()).toBe('Boo');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show edit button for users with edit permission', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<OrganizationDetail
|
||||||
|
organization={mockDetails}
|
||||||
|
/>
|
||||||
|
).find('OrganizationDetail');
|
||||||
|
|
||||||
|
const editLink = wrapper.findWhere(node => node.props().to === '/organizations/undefined/edit');
|
||||||
|
expect(editLink.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button for users without edit permission', () => {
|
||||||
|
const readOnlyOrg = { ...mockDetails };
|
||||||
|
readOnlyOrg.summary_fields.user_capabilities.edit = false;
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<OrganizationDetail
|
||||||
|
organization={readOnlyOrg}
|
||||||
|
/>
|
||||||
|
).find('OrganizationDetail');
|
||||||
|
|
||||||
|
const editLink = wrapper.findWhere(node => node.props().to === '/organizations/undefined/edit');
|
||||||
|
expect(editLink.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
<OrganizationNotifications />, { context: { network: {
|
<OrganizationNotifications canToggleNotifications />, { context: { network: {
|
||||||
api,
|
api,
|
||||||
handleHttpError: () => {}
|
handleHttpError: () => {}
|
||||||
} } }
|
} } }
|
||||||
@@ -26,7 +26,7 @@ describe('<OrganizationNotifications />', () => {
|
|||||||
});
|
});
|
||||||
test('handles api requests', () => {
|
test('handles api requests', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<OrganizationNotifications />, { context: { network: {
|
<OrganizationNotifications canToggleNotifications />, { context: { network: {
|
||||||
api,
|
api,
|
||||||
handleHttpError: () => {}
|
handleHttpError: () => {}
|
||||||
} } }
|
} } }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const API_LOGIN = `${API_ROOT}login/`;
|
|||||||
const API_LOGOUT = `${API_ROOT}logout/`;
|
const API_LOGOUT = `${API_ROOT}logout/`;
|
||||||
const API_V2 = `${API_ROOT}v2/`;
|
const API_V2 = `${API_ROOT}v2/`;
|
||||||
const API_CONFIG = `${API_V2}config/`;
|
const API_CONFIG = `${API_V2}config/`;
|
||||||
|
const API_ME = `${API_V2}me/`;
|
||||||
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 API_USERS = `${API_V2}users/`;
|
||||||
@@ -58,6 +59,10 @@ class APIClient {
|
|||||||
return this.http.get(API_CONFIG);
|
return this.http.get(API_CONFIG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMe () {
|
||||||
|
return this.http.get(API_ME);
|
||||||
|
}
|
||||||
|
|
||||||
destroyOrganization (id) {
|
destroyOrganization (id) {
|
||||||
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
const endpoint = `${API_ORGANIZATIONS}${id}/`;
|
||||||
return (this.http.delete(endpoint));
|
return (this.http.delete(endpoint));
|
||||||
@@ -71,6 +76,10 @@ class APIClient {
|
|||||||
return this.http.post(API_ORGANIZATIONS, data);
|
return this.http.post(API_ORGANIZATIONS, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optionsOrganizations () {
|
||||||
|
return this.http.options(API_ORGANIZATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
getOrganizationAccessList (id, params = {}) {
|
getOrganizationAccessList (id, params = {}) {
|
||||||
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
|
const endpoint = `${API_ORGANIZATIONS}${id}/access_list/`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { I18n } from '@lingui/react';
|
import { I18n, i18nMark } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -28,14 +28,11 @@ import VerticalSeparator from '../VerticalSeparator';
|
|||||||
class DataListToolbar extends React.Component {
|
class DataListToolbar extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
|
add,
|
||||||
addUrl,
|
addUrl,
|
||||||
columns,
|
columns,
|
||||||
|
deleteTooltip,
|
||||||
disableTrashCanIcon,
|
disableTrashCanIcon,
|
||||||
onSelectAll,
|
|
||||||
sortedColumnKey,
|
|
||||||
sortOrder,
|
|
||||||
showDelete,
|
|
||||||
showSelectAll,
|
|
||||||
isAllSelected,
|
isAllSelected,
|
||||||
isCompact,
|
isCompact,
|
||||||
noLeftMargin,
|
noLeftMargin,
|
||||||
@@ -43,8 +40,13 @@ class DataListToolbar extends React.Component {
|
|||||||
onSearch,
|
onSearch,
|
||||||
onCompact,
|
onCompact,
|
||||||
onExpand,
|
onExpand,
|
||||||
add,
|
onOpenDeleteModal,
|
||||||
onOpenDeleteModal
|
onSelectAll,
|
||||||
|
showAdd,
|
||||||
|
showDelete,
|
||||||
|
showSelectAll,
|
||||||
|
sortOrder,
|
||||||
|
sortedColumnKey
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const showExpandCollapse = (onCompact && onExpand);
|
const showExpandCollapse = (onCompact && onExpand);
|
||||||
@@ -112,21 +114,23 @@ class DataListToolbar extends React.Component {
|
|||||||
<LevelItem>
|
<LevelItem>
|
||||||
{ showDelete && (
|
{ showDelete && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={i18n._(t`Delete`)}
|
content={deleteTooltip}
|
||||||
position="top"
|
position="left"
|
||||||
>
|
>
|
||||||
<Button
|
<span>
|
||||||
className="awx-ToolBarBtn"
|
<Button
|
||||||
variant="plain"
|
className="awx-ToolBarBtn"
|
||||||
aria-label={i18n._(t`Delete`)}
|
variant="plain"
|
||||||
onClick={onOpenDeleteModal}
|
aria-label={i18n._(t`Delete`)}
|
||||||
isDisabled={disableTrashCanIcon}
|
onClick={onOpenDeleteModal}
|
||||||
>
|
isDisabled={disableTrashCanIcon}
|
||||||
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
|
>
|
||||||
</Button>
|
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{addUrl && (
|
{showAdd && addUrl && (
|
||||||
<Link to={addUrl}>
|
<Link to={addUrl}>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -136,7 +140,7 @@ class DataListToolbar extends React.Component {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{add && (
|
{showAdd && add && (
|
||||||
<Fragment>{add}</Fragment>
|
<Fragment>{add}</Fragment>
|
||||||
)}
|
)}
|
||||||
</LevelItem>
|
</LevelItem>
|
||||||
@@ -149,38 +153,42 @@ class DataListToolbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DataListToolbar.propTypes = {
|
DataListToolbar.propTypes = {
|
||||||
|
add: PropTypes.node,
|
||||||
addUrl: PropTypes.string,
|
addUrl: PropTypes.string,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
deleteTooltip: PropTypes.node,
|
||||||
isAllSelected: PropTypes.bool,
|
isAllSelected: PropTypes.bool,
|
||||||
|
isCompact: PropTypes.bool,
|
||||||
noLeftMargin: PropTypes.bool,
|
noLeftMargin: PropTypes.bool,
|
||||||
|
onCompact: PropTypes.func,
|
||||||
|
onExpand: PropTypes.func,
|
||||||
onSearch: PropTypes.func,
|
onSearch: PropTypes.func,
|
||||||
onSelectAll: PropTypes.func,
|
onSelectAll: PropTypes.func,
|
||||||
onSort: PropTypes.func,
|
onSort: PropTypes.func,
|
||||||
|
showAdd: PropTypes.bool,
|
||||||
showDelete: PropTypes.bool,
|
showDelete: PropTypes.bool,
|
||||||
showSelectAll: PropTypes.bool,
|
showSelectAll: PropTypes.bool,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
sortedColumnKey: PropTypes.string,
|
sortedColumnKey: PropTypes.string
|
||||||
onCompact: PropTypes.func,
|
|
||||||
onExpand: PropTypes.func,
|
|
||||||
isCompact: PropTypes.bool,
|
|
||||||
add: PropTypes.node
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DataListToolbar.defaultProps = {
|
DataListToolbar.defaultProps = {
|
||||||
|
add: null,
|
||||||
addUrl: null,
|
addUrl: null,
|
||||||
|
deleteTooltip: i18nMark('Delete'),
|
||||||
|
isAllSelected: false,
|
||||||
|
isCompact: false,
|
||||||
|
noLeftMargin: false,
|
||||||
|
onCompact: null,
|
||||||
|
onExpand: null,
|
||||||
onSearch: null,
|
onSearch: null,
|
||||||
onSelectAll: null,
|
onSelectAll: null,
|
||||||
onSort: null,
|
onSort: null,
|
||||||
|
showAdd: false,
|
||||||
showDelete: false,
|
showDelete: false,
|
||||||
showSelectAll: false,
|
showSelectAll: false,
|
||||||
sortOrder: 'ascending',
|
sortOrder: 'ascending',
|
||||||
sortedColumnKey: 'name',
|
sortedColumnKey: 'name'
|
||||||
isAllSelected: false,
|
|
||||||
onCompact: null,
|
|
||||||
onExpand: null,
|
|
||||||
isCompact: false,
|
|
||||||
add: null,
|
|
||||||
noLeftMargin: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DataListToolbar;
|
export default DataListToolbar;
|
||||||
|
|||||||
@@ -74,7 +74,10 @@
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-left: 20px;
|
}
|
||||||
|
|
||||||
|
.awx-toolbar .pf-c-button {
|
||||||
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
|
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
class NotificationListItem extends React.Component {
|
class NotificationListItem extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
|
canToggleNotifications,
|
||||||
itemId,
|
itemId,
|
||||||
name,
|
name,
|
||||||
notificationType,
|
notificationType,
|
||||||
@@ -49,12 +50,14 @@ class NotificationListItem extends React.Component {
|
|||||||
<Switch
|
<Switch
|
||||||
label={i18n._(t`Successful`)}
|
label={i18n._(t`Successful`)}
|
||||||
isChecked={successTurnedOn}
|
isChecked={successTurnedOn}
|
||||||
|
isDisabled={!canToggleNotifications}
|
||||||
onChange={() => toggleNotification(itemId, successTurnedOn, 'success')}
|
onChange={() => toggleNotification(itemId, successTurnedOn, 'success')}
|
||||||
aria-label={i18n._(t`Notification success toggle`)}
|
aria-label={i18n._(t`Notification success toggle`)}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
label={i18n._(t`Failure`)}
|
label={i18n._(t`Failure`)}
|
||||||
isChecked={errorTurnedOn}
|
isChecked={errorTurnedOn}
|
||||||
|
isDisabled={!canToggleNotifications}
|
||||||
onChange={() => toggleNotification(itemId, errorTurnedOn, 'error')}
|
onChange={() => toggleNotification(itemId, errorTurnedOn, 'error')}
|
||||||
aria-label={i18n._(t`Notification failure toggle`)}
|
aria-label={i18n._(t`Notification failure toggle`)}
|
||||||
/>
|
/>
|
||||||
@@ -67,6 +70,7 @@ class NotificationListItem extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationListItem.propTypes = {
|
NotificationListItem.propTypes = {
|
||||||
|
canToggleNotifications: PropTypes.bool.isRequired,
|
||||||
detailUrl: PropTypes.string.isRequired,
|
detailUrl: PropTypes.string.isRequired,
|
||||||
errorTurnedOn: PropTypes.bool,
|
errorTurnedOn: PropTypes.bool,
|
||||||
itemId: PropTypes.number.isRequired,
|
itemId: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ class Notifications extends Component {
|
|||||||
successTemplateIds,
|
successTemplateIds,
|
||||||
errorTemplateIds
|
errorTemplateIds
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
const { canToggleNotifications } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{noInitialResults && (
|
{noInitialResults && (
|
||||||
@@ -315,6 +316,7 @@ class Notifications extends Component {
|
|||||||
toggleNotification={this.toggleNotification}
|
toggleNotification={this.toggleNotification}
|
||||||
errorTurnedOn={errorTemplateIds.includes(o.id)}
|
errorTurnedOn={errorTemplateIds.includes(o.id)}
|
||||||
successTurnedOn={successTemplateIds.includes(o.id)}
|
successTurnedOn={successTemplateIds.includes(o.id)}
|
||||||
|
canToggleNotifications={canToggleNotifications}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -337,6 +339,7 @@ class Notifications extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Notifications.propTypes = {
|
Notifications.propTypes = {
|
||||||
|
canToggleNotifications: PropTypes.bool.isRequired,
|
||||||
onReadError: PropTypes.func.isRequired,
|
onReadError: PropTypes.func.isRequired,
|
||||||
onReadNotifications: PropTypes.func.isRequired,
|
onReadNotifications: PropTypes.func.isRequired,
|
||||||
onReadSuccess: PropTypes.func.isRequired,
|
onReadSuccess: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ class Provider extends Component {
|
|||||||
version: null,
|
version: null,
|
||||||
custom_logo: null,
|
custom_logo: null,
|
||||||
custom_login_info: null,
|
custom_login_info: null,
|
||||||
|
me: {},
|
||||||
...props.value
|
...props.value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fetchConfig = this.fetchConfig.bind(this);
|
this.fetchConfig = this.fetchConfig.bind(this);
|
||||||
|
this.fetchMe = this.fetchMe.bind(this);
|
||||||
|
this.updateConfig = this.updateConfig.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -30,30 +33,64 @@ class Provider extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateConfig = (config) => {
|
||||||
|
const {
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
this.setState(prevState => ({
|
||||||
|
value: {
|
||||||
|
...prevState.value,
|
||||||
|
ansible_version,
|
||||||
|
custom_virtualenvs,
|
||||||
|
version
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMe () {
|
||||||
|
const { api, handleHttpError } = this.props;
|
||||||
|
try {
|
||||||
|
const { data: { results: [me] } } = await api.getMe();
|
||||||
|
this.setState(prevState => ({
|
||||||
|
value: {
|
||||||
|
...prevState.value,
|
||||||
|
me
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
handleHttpError(err) || this.setState({
|
||||||
|
value: {
|
||||||
|
ansible_version: null,
|
||||||
|
custom_virtualenvs: null,
|
||||||
|
version: null,
|
||||||
|
custom_logo: null,
|
||||||
|
custom_login_info: null,
|
||||||
|
me: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchConfig () {
|
async fetchConfig () {
|
||||||
const { api, handleHttpError } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const [configRes, rootRes, meRes] = await Promise.all([
|
||||||
data: {
|
api.getConfig(),
|
||||||
ansible_version,
|
api.getRoot(),
|
||||||
custom_virtualenvs,
|
api.getMe()
|
||||||
version
|
]);
|
||||||
}
|
|
||||||
} = await api.getConfig();
|
|
||||||
const {
|
|
||||||
data: {
|
|
||||||
custom_logo,
|
|
||||||
custom_login_info
|
|
||||||
}
|
|
||||||
} = await api.getRoot();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
value: {
|
value: {
|
||||||
ansible_version,
|
ansible_version: configRes.data.ansible_version,
|
||||||
custom_virtualenvs,
|
custom_virtualenvs: configRes.data.custom_virtualenvs,
|
||||||
version,
|
version: configRes.data.version,
|
||||||
custom_logo,
|
custom_logo: rootRes.data.custom_logo,
|
||||||
custom_login_info
|
custom_login_info: rootRes.data.custom_login_info,
|
||||||
|
me: meRes.data.results
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -63,7 +100,8 @@ class Provider extends Component {
|
|||||||
custom_virtualenvs: null,
|
custom_virtualenvs: null,
|
||||||
version: null,
|
version: null,
|
||||||
custom_logo: null,
|
custom_logo: null,
|
||||||
custom_login_info: null
|
custom_login_info: null,
|
||||||
|
me: {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -75,7 +113,13 @@ class Provider extends Component {
|
|||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigContext.Provider value={value}>
|
<ConfigContext.Provider
|
||||||
|
value={{
|
||||||
|
...value,
|
||||||
|
fetchMe: this.fetchMe,
|
||||||
|
updateConfig: this.updateConfig
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ export function main (render) {
|
|||||||
path="/login"
|
path="/login"
|
||||||
render={() => (
|
render={() => (
|
||||||
<Config>
|
<Config>
|
||||||
{({ custom_logo, custom_login_info }) => (
|
{({ custom_logo, custom_login_info, fetchMe, updateConfig }) => (
|
||||||
<Login
|
<Login
|
||||||
logo={custom_logo}
|
logo={custom_logo}
|
||||||
loginInfo={custom_login_info}
|
loginInfo={custom_login_info}
|
||||||
|
fetchMe={fetchMe}
|
||||||
|
updateConfig={updateConfig}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AWXLogin extends Component {
|
|||||||
|
|
||||||
async onLoginButtonClick (event) {
|
async onLoginButtonClick (event) {
|
||||||
const { username, password, isLoading } = this.state;
|
const { username, password, isLoading } = this.state;
|
||||||
const { api, handleHttpError, clearRootDialogMessage } = this.props;
|
const { api, handleHttpError, clearRootDialogMessage, fetchMe, updateConfig } = this.props;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -51,7 +51,9 @@ class AWXLogin extends Component {
|
|||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.login(username, password);
|
const { data } = await api.login(username, password);
|
||||||
|
updateConfig(data);
|
||||||
|
await fetchMe();
|
||||||
this.setState({ isAuthenticated: true, isLoading: false });
|
this.setState({ isAuthenticated: true, isLoading: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
handleHttpError(error) || this.setState({ isInputValid: false, isLoading: false });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Route, withRouter, Switch } from 'react-router-dom';
|
|||||||
import { i18nMark } from '@lingui/react';
|
import { i18nMark } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { Config } from '../../contexts/Config';
|
||||||
import { NetworkProvider } from '../../contexts/Network';
|
import { NetworkProvider } from '../../contexts/Network';
|
||||||
import { withRootDialog } from '../../contexts/RootDialog';
|
import { withRootDialog } from '../../contexts/RootDialog';
|
||||||
|
|
||||||
@@ -74,11 +75,16 @@ class Organizations extends Component {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Organization
|
<Config>
|
||||||
history={history}
|
{({ me }) => (
|
||||||
location={location}
|
<Organization
|
||||||
setBreadcrumb={this.setBreadcrumbConfig}
|
history={history}
|
||||||
/>
|
location={location}
|
||||||
|
setBreadcrumb={this.setBreadcrumbConfig}
|
||||||
|
me={me || {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Config>
|
||||||
</NetworkProvider>
|
</NetworkProvider>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { withNetwork } from '../../../contexts/Network';
|
import { withNetwork } from '../../../contexts/Network';
|
||||||
|
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import BasicChip from '../../../components/BasicChip/BasicChip';
|
||||||
import Pagination from '../../../components/Pagination';
|
import Pagination from '../../../components/Pagination';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import AddResourceRole from '../../../components/AddRole/AddResourceRole';
|
import AddResourceRole from '../../../components/AddRole/AddResourceRole';
|
||||||
@@ -357,6 +358,7 @@ class OrganizationAccessList extends React.Component {
|
|||||||
columns={this.columns}
|
columns={this.columns}
|
||||||
onSearch={() => { }}
|
onSearch={() => { }}
|
||||||
onSort={this.onSort}
|
onSort={this.onSort}
|
||||||
|
showAdd={organization.summary_fields.user_capabilities.edit}
|
||||||
add={(
|
add={(
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
@@ -421,13 +423,21 @@ class OrganizationAccessList extends React.Component {
|
|||||||
<ul style={userRolesWrapperStyle}>
|
<ul style={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 => (
|
||||||
<Chip
|
role.user_capabilities.unattach ? (
|
||||||
key={role.id}
|
<Chip
|
||||||
className="awx-c-chip"
|
key={role.id}
|
||||||
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
className="awx-c-chip"
|
||||||
>
|
onClick={() => this.handleWarning(role.name, role.id, result.username, result.id, 'users')}
|
||||||
{role.name}
|
>
|
||||||
</Chip>
|
{role.name}
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<BasicChip
|
||||||
|
key={role.id}
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</BasicChip>
|
||||||
|
)
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
@@ -466,7 +476,9 @@ class OrganizationAccessList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OrganizationAccessList.propTypes = {
|
OrganizationAccessList.propTypes = {
|
||||||
|
api: PropTypes.shape().isRequired,
|
||||||
getAccessList: PropTypes.func.isRequired,
|
getAccessList: PropTypes.func.isRequired,
|
||||||
|
organization: PropTypes.shape().isRequired,
|
||||||
removeRole: PropTypes.func.isRequired
|
removeRole: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,17 @@ class Organization extends Component {
|
|||||||
organization: null,
|
organization: null,
|
||||||
error: false,
|
error: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
isNotifAdmin: false,
|
||||||
|
isAuditorOfThisOrg: false,
|
||||||
|
isAdminOfThisOrg: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fetchOrganization = this.fetchOrganization.bind(this);
|
this.fetchOrganization = this.fetchOrganization.bind(this);
|
||||||
|
this.fetchOrganizationAndRoles = this.fetchOrganizationAndRoles.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.fetchOrganization();
|
this.fetchOrganizationAndRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate (prevProps) {
|
async componentDidUpdate (prevProps) {
|
||||||
@@ -49,6 +53,43 @@ class Organization extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchOrganizationAndRoles () {
|
||||||
|
const {
|
||||||
|
match,
|
||||||
|
setBreadcrumb,
|
||||||
|
api,
|
||||||
|
handleHttpError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [{ data }, notifAdminRest, auditorRes, adminRes] = await Promise.all([
|
||||||
|
api.getOrganizationDetails(parseInt(match.params.id, 10)),
|
||||||
|
api.getOrganizations({
|
||||||
|
role_level: 'notification_admin_role',
|
||||||
|
page_size: 1
|
||||||
|
}),
|
||||||
|
api.getOrganizations({
|
||||||
|
role_level: 'auditor_role',
|
||||||
|
id: parseInt(match.params.id, 10)
|
||||||
|
}),
|
||||||
|
api.getOrganizations({
|
||||||
|
role_level: 'admin_role',
|
||||||
|
id: parseInt(match.params.id, 10)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
setBreadcrumb(data);
|
||||||
|
this.setState({
|
||||||
|
organization: data,
|
||||||
|
loading: false,
|
||||||
|
isNotifAdmin: notifAdminRest.data.results.length > 0,
|
||||||
|
isAuditorOfThisOrg: auditorRes.data.results.length > 0,
|
||||||
|
isAdminOfThisOrg: adminRes.data.results.length > 0
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleHttpError(error) || this.setState({ error: true, loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchOrganization () {
|
async fetchOrganization () {
|
||||||
const {
|
const {
|
||||||
match,
|
match,
|
||||||
@@ -70,19 +111,44 @@ class Organization extends Component {
|
|||||||
const {
|
const {
|
||||||
location,
|
location,
|
||||||
match,
|
match,
|
||||||
|
me,
|
||||||
history
|
history
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
organization,
|
organization,
|
||||||
error,
|
error,
|
||||||
loading
|
loading,
|
||||||
|
isNotifAdmin,
|
||||||
|
isAuditorOfThisOrg,
|
||||||
|
isAdminOfThisOrg
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const tabsPaddingOverride = {
|
const tabsPaddingOverride = {
|
||||||
padding: '0'
|
padding: '0'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin || isAuditorOfThisOrg;
|
||||||
|
const canToggleNotifications = isNotifAdmin && (
|
||||||
|
me.is_system_auditor
|
||||||
|
|| isAuditorOfThisOrg
|
||||||
|
|| isAdminOfThisOrg
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{ name: i18nMark('Details'), link: `${match.url}/details`, id: 0 },
|
||||||
|
{ name: i18nMark('Access'), link: `${match.url}/access`, id: 1 },
|
||||||
|
{ name: i18nMark('Teams'), link: `${match.url}/teams`, id: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (canSeeNotificationsTab) {
|
||||||
|
tabsArray.push({
|
||||||
|
name: i18nMark('Notifications'),
|
||||||
|
link: `${match.url}/notifications`,
|
||||||
|
id: 3
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
loading ? ''
|
loading ? ''
|
||||||
: (
|
: (
|
||||||
@@ -96,12 +162,7 @@ class Organization extends Component {
|
|||||||
match={match}
|
match={match}
|
||||||
history={history}
|
history={history}
|
||||||
labeltext={i18n._(t`Organization detail tabs`)}
|
labeltext={i18n._(t`Organization detail tabs`)}
|
||||||
tabsArray={[
|
tabsArray={tabsArray}
|
||||||
{ name: i18nMark('Details'), link: `${match.url}/details`, id: 0 },
|
|
||||||
{ name: i18nMark('Access'), link: `${match.url}/access`, id: 1 },
|
|
||||||
{ name: i18nMark('Teams'), link: `${match.url}/teams`, id: 2 },
|
|
||||||
{ name: i18nMark('Notifications'), link: `${match.url}/notifications`, id: 3 },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
@@ -174,16 +235,16 @@ class Organization extends Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
{canSeeNotificationsTab && (
|
||||||
path="/organizations/:id/notifications"
|
<Route
|
||||||
render={() => (
|
path="/organizations/:id/notifications"
|
||||||
<OrganizationNotifications
|
render={() => (
|
||||||
match={match}
|
<OrganizationNotifications
|
||||||
location={location}
|
canToggleNotifications={canToggleNotifications}
|
||||||
history={history}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
{organization && <NotifyAndRedirect to={`/organizations/${match.params.id}/details`} />}
|
||||||
</Switch>
|
</Switch>
|
||||||
{error ? 'error!' : ''}
|
{error ? 'error!' : ''}
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ class OrganizationDetail extends Component {
|
|||||||
description,
|
description,
|
||||||
custom_virtualenv,
|
custom_virtualenv,
|
||||||
created,
|
created,
|
||||||
modified
|
modified,
|
||||||
|
summary_fields
|
||||||
},
|
},
|
||||||
match
|
match
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -165,11 +166,13 @@ class OrganizationDetail extends Component {
|
|||||||
</TextContent>
|
</TextContent>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
{summary_fields.user_capabilities.edit && (
|
||||||
<Link to={`/organizations/${match.params.id}/edit`}>
|
<div style={{ display: 'flex', flexDirection: 'row-reverse', marginTop: '20px' }}>
|
||||||
<Button><Trans>Edit</Trans></Button>
|
<Link to={`/organizations/${match.params.id}/edit`}>
|
||||||
</Link>
|
<Button><Trans>Edit</Trans></Button>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{error ? 'error!' : ''}
|
{error ? 'error!' : ''}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,13 +41,18 @@ class OrganizationNotifications extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const {
|
||||||
|
canToggleNotifications
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationsList
|
<NotificationsList
|
||||||
|
canToggleNotifications={canToggleNotifications}
|
||||||
|
onCreateError={this.createOrgNotificationError}
|
||||||
|
onCreateSuccess={this.createOrgNotificationSuccess}
|
||||||
|
onReadError={this.readOrgNotificationError}
|
||||||
onReadNotifications={this.readOrgNotifications}
|
onReadNotifications={this.readOrgNotifications}
|
||||||
onReadSuccess={this.readOrgNotificationSuccess}
|
onReadSuccess={this.readOrgNotificationSuccess}
|
||||||
onReadError={this.readOrgNotificationError}
|
|
||||||
onCreateSuccess={this.createOrgNotificationSuccess}
|
|
||||||
onCreateError={this.createOrgNotificationError}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ class OrganizationsList extends Component {
|
|||||||
loading: true,
|
loading: true,
|
||||||
results: [],
|
results: [],
|
||||||
selected: [],
|
selected: [],
|
||||||
isModalOpen: false,
|
isModalOpen: false
|
||||||
orgsToDelete: [],
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,14 +73,16 @@ class OrganizationsList extends Component {
|
|||||||
this.onSelectAll = this.onSelectAll.bind(this);
|
this.onSelectAll = this.onSelectAll.bind(this);
|
||||||
this.onSelect = this.onSelect.bind(this);
|
this.onSelect = this.onSelect.bind(this);
|
||||||
this.updateUrl = this.updateUrl.bind(this);
|
this.updateUrl = this.updateUrl.bind(this);
|
||||||
|
this.fetchOptionsOrganizations = this.fetchOptionsOrganizations.bind(this);
|
||||||
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
this.fetchOrganizations = this.fetchOrganizations.bind(this);
|
||||||
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
this.handleOrgDelete = this.handleOrgDelete.bind(this);
|
||||||
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
|
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
|
||||||
this.handleClearOrgsToDelete = this.handleClearOrgsToDelete.bind(this);
|
this.handleCloseOrgDeleteModal = this.handleCloseOrgDeleteModal.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const queryParams = this.getQueryParams();
|
const queryParams = this.getQueryParams();
|
||||||
|
this.fetchOptionsOrganizations();
|
||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,20 +118,20 @@ class OrganizationsList extends Component {
|
|||||||
onSelectAll (isSelected) {
|
onSelectAll (isSelected) {
|
||||||
const { results } = this.state;
|
const { results } = this.state;
|
||||||
|
|
||||||
const selected = isSelected ? results.map(o => o.id) : [];
|
const selected = isSelected ? results : [];
|
||||||
|
|
||||||
this.setState({ selected });
|
this.setState({ selected });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect (id) {
|
onSelect (row) {
|
||||||
const { selected } = this.state;
|
const { selected } = this.state;
|
||||||
|
|
||||||
const isSelected = selected.includes(id);
|
const isSelected = selected.some(s => s.id === row.id);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
this.setState({ selected: selected.filter(s => s !== id) });
|
this.setState({ selected: selected.filter(s => s.id !== row.id) });
|
||||||
} else {
|
} else {
|
||||||
this.setState({ selected: selected.concat(id) });
|
this.setState({ selected: selected.concat(row) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,43 +144,35 @@ class OrganizationsList extends Component {
|
|||||||
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
return Object.assign({}, this.defaultParams, searchParams, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearOrgsToDelete () {
|
handleCloseOrgDeleteModal () {
|
||||||
this.setState({
|
this.setState({
|
||||||
isModalOpen: false,
|
isModalOpen: false
|
||||||
orgsToDelete: []
|
|
||||||
});
|
});
|
||||||
this.onSelectAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenOrgDeleteModal () {
|
handleOpenOrgDeleteModal () {
|
||||||
const { results, selected } = this.state;
|
const { selected } = this.state;
|
||||||
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
const warningTitle = selected.length > 1 ? i18nMark('Delete Organization') : i18nMark('Delete Organizations');
|
||||||
const warningMsg = i18nMark('Are you sure you want to delete:');
|
const warningMsg = i18nMark('Are you sure you want to delete:');
|
||||||
|
|
||||||
const orgsToDelete = [];
|
|
||||||
results.forEach((result) => {
|
|
||||||
selected.forEach((selectedOrg) => {
|
|
||||||
if (result.id === selectedOrg) {
|
|
||||||
orgsToDelete.push({ name: result.name, id: selectedOrg });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.setState({
|
this.setState({
|
||||||
orgsToDelete,
|
|
||||||
isModalOpen: true,
|
isModalOpen: true,
|
||||||
warningTitle,
|
warningTitle,
|
||||||
warningMsg,
|
warningMsg,
|
||||||
loading: false });
|
loading: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleOrgDelete (event) {
|
async handleOrgDelete () {
|
||||||
const { orgsToDelete } = this.state;
|
const { selected } = this.state;
|
||||||
const { api, handleHttpError } = this.props;
|
const { api, handleHttpError } = this.props;
|
||||||
let errorHandled;
|
let errorHandled;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(orgsToDelete.map((org) => api.destroyOrganization(org.id)));
|
await Promise.all(selected.map((org) => api.destroyOrganization(org.id)));
|
||||||
this.handleClearOrgsToDelete();
|
this.setState({
|
||||||
|
isModalOpen: false,
|
||||||
|
selected: []
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorHandled = handleHttpError(err);
|
errorHandled = handleHttpError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -188,7 +181,6 @@ class OrganizationsList extends Component {
|
|||||||
this.fetchOrganizations(queryParams);
|
this.fetchOrganizations(queryParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl (queryParams) {
|
updateUrl (queryParams) {
|
||||||
@@ -248,16 +240,35 @@ class OrganizationsList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchOptionsOrganizations () {
|
||||||
|
const { api } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await api.optionsOrganizations();
|
||||||
|
const { actions } = data;
|
||||||
|
|
||||||
|
const stateToUpdate = {
|
||||||
|
canAdd: Object.prototype.hasOwnProperty.call(actions, 'POST')
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState(stateToUpdate);
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
} finally {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
medium,
|
medium,
|
||||||
} = PageSectionVariants;
|
} = PageSectionVariants;
|
||||||
const {
|
const {
|
||||||
|
canAdd,
|
||||||
count,
|
count,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
noInitialResults,
|
noInitialResults,
|
||||||
orgsToDelete,
|
|
||||||
page,
|
page,
|
||||||
pageCount,
|
pageCount,
|
||||||
page_size,
|
page_size,
|
||||||
@@ -270,6 +281,12 @@ class OrganizationsList extends Component {
|
|||||||
warningMsg,
|
warningMsg,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { match } = this.props;
|
const { match } = this.props;
|
||||||
|
|
||||||
|
const disableDelete = (
|
||||||
|
selected.length === 0
|
||||||
|
|| selected.some(row => !row.summary_fields.user_capabilities.delete)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
@@ -280,15 +297,15 @@ class OrganizationsList extends Component {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
title={warningTitle}
|
title={warningTitle}
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={this.handleClearOrgsToDelete}
|
onClose={this.handleCloseOrgDeleteModal}
|
||||||
actions={[
|
actions={[
|
||||||
<Button variant="danger" key="delete" aria-label="confirm-delete" onClick={this.handleOrgDelete}>{i18n._(t`Delete`)}</Button>,
|
<Button variant="danger" key="delete" aria-label="confirm-delete" onClick={this.handleOrgDelete}>{i18n._(t`Delete`)}</Button>,
|
||||||
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleClearOrgsToDelete}>{i18n._(t`Cancel`)}</Button>
|
<Button variant="secondary" key="cancel" aria-label="cancel-delete" onClick={this.handleCloseOrgDeleteModal}>{i18n._(t`Cancel`)}</Button>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{warningMsg}
|
{warningMsg}
|
||||||
<br />
|
<br />
|
||||||
{orgsToDelete.map((org) => (
|
{selected.map((org) => (
|
||||||
<span key={org.id}>
|
<span key={org.id}>
|
||||||
<strong>
|
<strong>
|
||||||
{org.name}
|
{org.name}
|
||||||
@@ -321,9 +338,27 @@ class OrganizationsList extends Component {
|
|||||||
onSort={this.onSort}
|
onSort={this.onSort}
|
||||||
onSelectAll={this.onSelectAll}
|
onSelectAll={this.onSelectAll}
|
||||||
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
|
||||||
disableTrashCanIcon={selected.length === 0}
|
disableTrashCanIcon={disableDelete}
|
||||||
|
deleteTooltip={
|
||||||
|
selected.some(row => !row.summary_fields.user_capabilities.delete) ? (
|
||||||
|
<div>
|
||||||
|
<Trans>
|
||||||
|
You dont have permission to delete the following Organizations:
|
||||||
|
</Trans>
|
||||||
|
{selected
|
||||||
|
.filter(row => !row.summary_fields.user_capabilities.delete)
|
||||||
|
.map(row => (
|
||||||
|
<div key={row.id}>
|
||||||
|
{row.name}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
showDelete
|
showDelete
|
||||||
showSelectAll
|
showSelectAll
|
||||||
|
showAdd={canAdd}
|
||||||
/>
|
/>
|
||||||
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
<ul className="pf-c-data-list" aria-label={i18n._(t`Organizations List`)}>
|
||||||
{ results.map(o => (
|
{ results.map(o => (
|
||||||
@@ -334,8 +369,8 @@ class OrganizationsList extends Component {
|
|||||||
detailUrl={`${match.url}/${o.id}`}
|
detailUrl={`${match.url}/${o.id}`}
|
||||||
memberCount={o.summary_fields.related_field_counts.users}
|
memberCount={o.summary_fields.related_field_counts.users}
|
||||||
teamCount={o.summary_fields.related_field_counts.teams}
|
teamCount={o.summary_fields.related_field_counts.teams}
|
||||||
isSelected={selected.includes(o.id)}
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
onSelect={() => this.onSelect(o.id, o.name)}
|
onSelect={() => this.onSelect(o)}
|
||||||
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
|
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user