diff --git a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx
new file mode 100644
index 0000000000..ad2c254f06
--- /dev/null
+++ b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx
@@ -0,0 +1,155 @@
+import React, { useState, useCallback } from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { useParams } from 'react-router-dom';
+import styled from 'styled-components';
+import useRequest, { useDismissableError } from '../../util/useRequest';
+import SelectableCard from '../SelectableCard';
+import AlertModal from '../AlertModal';
+import ErrorDetail from '../ErrorDetail';
+import Wizard from '../Wizard/Wizard';
+import useSelected from '../../util/useSelected';
+import SelectResourceStep from '../AddRole/SelectResourceStep';
+import SelectRoleStep from '../AddRole/SelectRoleStep';
+import getResourceTypes from './resources.data';
+
+const Grid = styled.div`
+ display: grid;
+ grid-gap: 20px;
+ grid-template-columns: 33% 33% 33%;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+`;
+
+function UserAndTeamAccessAdd({
+ i18n,
+ isOpen,
+ title,
+ onSave,
+ apiModel,
+ onClose,
+}) {
+ const [selectedResourceType, setSelectedResourceType] = useState();
+ const [stepIdReached, setStepIdReached] = useState(1);
+ const { id: userId } = useParams();
+ const {
+ selected: resourcesSelected,
+ handleSelect: handleResourceSelect,
+ } = useSelected([]);
+
+ const {
+ selected: rolesSelected,
+ handleSelect: handleRoleSelect,
+ } = useSelected([]);
+
+ const { request: handleWizardSave, error: saveError } = useRequest(
+ useCallback(async () => {
+ const roleRequests = [];
+ const resourceRolesTypes = resourcesSelected.flatMap(resource =>
+ Object.values(resource.summary_fields.object_roles)
+ );
+
+ rolesSelected.map(role =>
+ resourceRolesTypes.forEach(rolename => {
+ if (rolename.name === role.name) {
+ roleRequests.push(apiModel.associateRole(userId, rolename.id));
+ }
+ })
+ );
+
+ await Promise.all(roleRequests);
+ onSave();
+ }, [onSave, rolesSelected, apiModel, userId, resourcesSelected]),
+ {}
+ );
+
+ const { error, dismissError } = useDismissableError(saveError);
+
+ const steps = [
+ {
+ id: 1,
+ name: i18n._(t`Add resource type`),
+ component: (
+
+ {getResourceTypes(i18n).map(resource => (
+ setSelectedResourceType(resource)}
+ />
+ ))}
+
+ ),
+ },
+ {
+ id: 2,
+ name: i18n._(t`Select items from list`),
+ component: selectedResourceType && (
+
+ ),
+ enableNext: resourcesSelected.length > 0,
+ canJumpTo: stepIdReached >= 2,
+ },
+ {
+ id: 3,
+ name: i18n._(t`Select roles to apply`),
+ component: resourcesSelected?.length > 0 && (
+
+ ),
+ nextButtonText: i18n._(t`Save`),
+ canJumpTo: stepIdReached >= 3,
+ },
+ ];
+
+ if (error) {
+ return (
+
+ {i18n._(t`Failed to associate role`)}
+
+
+ );
+ }
+
+ return (
+
+ setStepIdReached(stepIdReached < id ? id : stepIdReached)
+ }
+ onSave={handleWizardSave}
+ />
+ );
+}
+
+export default withI18n()(UserAndTeamAccessAdd);
diff --git a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx
new file mode 100644
index 0000000000..0a703a7024
--- /dev/null
+++ b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx
@@ -0,0 +1,225 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { UsersAPI, JobTemplatesAPI } from '../../api';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../testUtils/enzymeHelpers';
+import UserAndTeamAccessAdd from './UserAndTeamAccessAdd';
+
+jest.mock('../../api/models/Teams');
+jest.mock('../../api/models/Users');
+jest.mock('../../api/models/JobTemplates');
+
+describe('', () => {
+ const resources = {
+ data: {
+ results: [
+ {
+ id: 1,
+ name: 'Job Template Foo Bar',
+ url: '/api/v2/job_template/1/',
+ summary_fields: {
+ object_roles: {
+ admin_role: {
+ description: 'Can manage all aspects of the job template',
+ name: 'Admin',
+ id: 164,
+ },
+ execute_role: {
+ description: 'May run the job template',
+ name: 'Execute',
+ id: 165,
+ },
+ read_role: {
+ description: 'May view settings for the job template',
+ name: 'Read',
+ id: 166,
+ },
+ },
+ },
+ },
+ ],
+ count: 1,
+ },
+ };
+ let wrapper;
+ beforeEach(async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}}
+ onClose={() => {}}
+ title="Add user permissions"
+ />
+ );
+ });
+ });
+ afterEach(() => {
+ wrapper.unmount();
+ jest.clearAllMocks();
+ });
+ test('should mount properly', async () => {
+ expect(wrapper.find('PFWizard').length).toBe(1);
+ });
+ test('should disable steps', async () => {
+ expect(
+ wrapper
+ .find('WizardNavItem[text="Select ttems from list"]')
+ .prop('isDisabled')
+ ).toBe(true);
+ expect(
+ wrapper
+ .find('WizardNavItem[text="Select roles to apply"]')
+ .prop('isDisabled')
+ ).toBe(true);
+ await act(async () =>
+ wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
+ fetchItems: JobTemplatesAPI.read,
+ label: 'Job template',
+ selectedResource: 'jobTemplate',
+ searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
+ sortColumns: [{ name: 'Name', key: 'name' }],
+ })
+ );
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+ wrapper.update();
+ expect(
+ wrapper.find('WizardNavItem[text="Add resource type"]').prop('isDisabled')
+ ).toBe(false);
+ expect(
+ wrapper
+ .find('WizardNavItem[text="Select ttems from list"]')
+ .prop('isDisabled')
+ ).toBe(false);
+ expect(
+ wrapper
+ .find('WizardNavItem[text="Select roles to apply"]')
+ .prop('isDisabled')
+ ).toBe(true);
+ });
+
+ test('should call api to associate role', async () => {
+ JobTemplatesAPI.read.mockResolvedValue(resources);
+ UsersAPI.associateRole.mockResolvedValue({});
+
+ await act(async () =>
+ wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
+ fetchItems: JobTemplatesAPI.read,
+ label: 'Job template',
+ selectedResource: 'jobTemplate',
+ searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
+ sortColumns: [{ name: 'Name', key: 'name' }],
+ })
+ );
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+ await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0);
+ expect(JobTemplatesAPI.read).toHaveBeenCalled();
+ await act(async () =>
+ wrapper
+ .find('CheckboxListItem')
+ .first()
+ .find('input[type="checkbox"]')
+ .simulate('change', { target: { checked: true } })
+ );
+
+ wrapper.update();
+
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+
+ wrapper.update();
+
+ expect(wrapper.find('RolesStep').length).toBe(1);
+
+ await act(async () =>
+ wrapper
+ .find('CheckboxCard')
+ .first()
+ .prop('onSelect')()
+ );
+
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+
+ await expect(UsersAPI.associateRole).toHaveBeenCalled();
+ });
+
+ test('should throw error', async () => {
+ JobTemplatesAPI.read.mockResolvedValue(resources);
+ UsersAPI.associateRole.mockRejectedValue(
+ new Error({
+ response: {
+ config: {
+ method: 'post',
+ url: '/api/v2/users/a/roles',
+ },
+ data: 'An error occurred',
+ status: 403,
+ },
+ })
+ );
+
+ jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ id: 'a',
+ }),
+ }));
+
+ await act(async () =>
+ wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({
+ fetchItems: JobTemplatesAPI.read,
+ label: 'Job template',
+ selectedResource: 'jobTemplate',
+ searchColumns: [{ name: 'Name', key: 'name', isDefault: true }],
+ sortColumns: [{ name: 'Name', key: 'name' }],
+ })
+ );
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+ await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0);
+ expect(JobTemplatesAPI.read).toHaveBeenCalled();
+ await act(async () =>
+ wrapper
+ .find('CheckboxListItem')
+ .first()
+ .find('input[type="checkbox"]')
+ .simulate('change', { target: { checked: true } })
+ );
+
+ wrapper.update();
+
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+
+ wrapper.update();
+
+ expect(wrapper.find('RolesStep').length).toBe(1);
+
+ await act(async () =>
+ wrapper
+ .find('CheckboxCard')
+ .first()
+ .prop('onSelect')()
+ );
+
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+
+ await expect(UsersAPI.associateRole).toHaveBeenCalled();
+ wrapper.update();
+ expect(wrapper.find('AlertModal').length).toBe(1);
+ });
+});
diff --git a/awx/ui_next/src/components/UserAccessAdd/index.js b/awx/ui_next/src/components/UserAccessAdd/index.js
new file mode 100644
index 0000000000..c445f018ba
--- /dev/null
+++ b/awx/ui_next/src/components/UserAccessAdd/index.js
@@ -0,0 +1 @@
+export { default } from './UserAndTeamAccessAdd';
diff --git a/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx b/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx
new file mode 100644
index 0000000000..0000036c59
--- /dev/null
+++ b/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx
@@ -0,0 +1,208 @@
+import { t } from '@lingui/macro';
+import {
+ JobTemplatesAPI,
+ WorkflowJobTemplatesAPI,
+ CredentialsAPI,
+ InventoriesAPI,
+ ProjectsAPI,
+ OrganizationsAPI,
+} from '../../api';
+
+export default function getResourceTypes(i18n) {
+ return [
+ {
+ selectedResource: 'jobTemplate',
+ label: i18n._(t`Job templates`),
+ searchColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ isDefault: true,
+ },
+ {
+ name: i18n._(t`Playbook name`),
+ key: 'playbook',
+ },
+ {
+ name: i18n._(t`Created By (Username)`),
+ key: 'created_by__username',
+ },
+ {
+ name: i18n._(t`Modified By (Username)`),
+ key: 'modified_by__username',
+ },
+ ],
+ sortColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ },
+ ],
+ fetchItems: () => JobTemplatesAPI.read(),
+ },
+ {
+ selectedResource: 'workflowJobTemplate',
+ label: i18n._(t`Workflow job templates`),
+ searchColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ isDefault: true,
+ },
+ {
+ name: i18n._(t`Playbook name`),
+ key: 'playbook',
+ },
+ {
+ name: i18n._(t`Created By (Username)`),
+ key: 'created_by__username',
+ },
+ {
+ name: i18n._(t`Modified By (Username)`),
+ key: 'modified_by__username',
+ },
+ ],
+ sortColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ },
+ ],
+ fetchItems: () => WorkflowJobTemplatesAPI.read(),
+ },
+ {
+ selectedResource: 'credential',
+ label: i18n._(t`Credentials`),
+ searchColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ isDefault: true,
+ },
+ {
+ name: i18n._(t`Type`),
+ key: 'scm_type',
+ options: [
+ [``, i18n._(t`Manual`)],
+ [`git`, i18n._(t`Git`)],
+ [`hg`, i18n._(t`Mercurial`)],
+ [`svn`, i18n._(t`Subversion`)],
+ [`insights`, i18n._(t`Red Hat Insights`)],
+ ],
+ },
+ {
+ name: i18n._(t`Source Control URL`),
+ key: 'scm_url',
+ },
+ {
+ name: i18n._(t`Modified By (Username)`),
+ key: 'modified_by__username',
+ },
+ {
+ name: i18n._(t`Created By (Username)`),
+ key: 'created_by__username',
+ },
+ ],
+ sortColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ },
+ ],
+ fetchItems: () => CredentialsAPI.read(),
+ },
+ {
+ selectedResource: 'inventory',
+ label: i18n._(t`Inventories`),
+ searchColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ isDefault: true,
+ },
+ {
+ name: i18n._(t`Created By (Username)`),
+ key: 'created_by__username',
+ },
+ {
+ name: i18n._(t`Modified By (Username)`),
+ key: 'modified_by__username',
+ },
+ ],
+ sortColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ },
+ ],
+ fetchItems: () => InventoriesAPI.read(),
+ },
+ {
+ selectedResource: 'project',
+ label: i18n._(t`Projects`),
+ searchColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ isDefault: true,
+ },
+ {
+ name: i18n._(t`Type`),
+ key: 'scm_type',
+ options: [
+ [``, i18n._(t`Manual`)],
+ [`git`, i18n._(t`Git`)],
+ [`hg`, i18n._(t`Mercurial`)],
+ [`svn`, i18n._(t`Subversion`)],
+ [`insights`, i18n._(t`Red Hat Insights`)],
+ ],
+ },
+ {
+ name: i18n._(t`Source Control URL`),
+ key: 'scm_url',
+ },
+ {
+ name: i18n._(t`Modified By (Username)`),
+ key: 'modified_by__username',
+ },
+ {
+ name: i18n._(t`Created By (Username)`),
+ key: 'created_by__username',
+ },
+ ],
+ sortColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ },
+ ],
+ fetchItems: () => ProjectsAPI.read(),
+ },
+ {
+ selectedResource: 'organization',
+ label: i18n._(t`Organizations`),
+ searchColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ isDefault: true,
+ },
+ {
+ name: i18n._(t`Created By (Username)`),
+ key: 'created_by__username',
+ },
+ {
+ name: i18n._(t`Modified By (Username)`),
+ key: 'modified_by__username',
+ },
+ ],
+ sortColumns: [
+ {
+ name: i18n._(t`Name`),
+ key: 'name',
+ },
+ ],
+ fetchItems: () => OrganizationsAPI.read(),
+ },
+ ];
+}
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx
index 79531fb561..6c6f2c2910 100644
--- a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx
+++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx
@@ -1,19 +1,18 @@
-import React, { useCallback, useEffect } from 'react';
-import { useLocation, useRouteMatch, useParams } from 'react-router-dom';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useLocation, useParams } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-import { Card } from '@patternfly/react-core';
+import { Button } from '@patternfly/react-core';
import { TeamsAPI } from '../../../api';
import useRequest from '../../../util/useRequest';
import DataListToolbar from '../../../components/DataListToolbar';
-import PaginatedDataList, {
- ToolbarAddButton,
-} from '../../../components/PaginatedDataList';
+import PaginatedDataList from '../../../components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import TeamAccessListItem from './TeamAccessListItem';
+import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd';
const QS_CONFIG = getQSConfig('team', {
page: 1,
@@ -22,8 +21,8 @@ const QS_CONFIG = getQSConfig('team', {
});
function TeamAccessList({ i18n }) {
+ const [isWizardOpen, setIsWizardOpen] = useState(false);
const { search } = useLocation();
- const match = useRouteMatch();
const { id } = useParams();
const {
@@ -57,6 +56,11 @@ function TeamAccessList({ i18n }) {
fetchRoles();
}, [fetchRoles]);
+ const saveRoles = () => {
+ setIsWizardOpen(false);
+ fetchRoles();
+ };
+
const canAdd =
options && Object.prototype.hasOwnProperty.call(options, 'POST');
@@ -77,7 +81,7 @@ function TeamAccessList({ i18n }) {
};
return (
-
+ <>
]
+ ? [
+ ,
+ ]
: []),
]}
/>
@@ -117,13 +131,17 @@ function TeamAccessList({ i18n }) {
onSelect={() => {}}
/>
)}
- emptyStateControls={
- canAdd ? (
-
- ) : null
- }
/>
-
+ {isWizardOpen && (
+ setIsWizardOpen(false)}
+ title={i18n._(t`Add team permissions`)}
+ />
+ )}
+ >
);
}
export default withI18n()(TeamAccessList);
diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx
index b7b5e26025..88311c8643 100644
--- a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx
+++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx
@@ -141,4 +141,67 @@ describe('', () => {
'/inventories/smart_inventory/77/details'
);
});
+ test('should not render add button', async () => {
+ TeamsAPI.readRoleOptions.mockResolvedValueOnce({
+ data: {},
+ });
+
+ TeamsAPI.readRoles.mockResolvedValue({
+ data: {
+ results: [
+ {
+ id: 2,
+ name: 'Admin',
+ type: 'role',
+ url: '/api/v2/roles/257/',
+ summary_fields: {
+ resource_name: 'template delete project',
+ resource_id: 15,
+ resource_type: 'job_template',
+ resource_type_display_name: 'Job Template',
+ user_capabilities: { unattach: true },
+ },
+ description: 'Can manage all aspects of the job template',
+ },
+ ],
+ count: 1,
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+ ,
+ {
+ context: {
+ router: {
+ history,
+ route: {
+ location: history.location,
+ match: { params: { id: 18 } },
+ },
+ },
+ },
+ }
+ );
+ });
+
+ waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
+ expect(wrapper.find('Button[aria-label="Add resource roles"]').length).toBe(
+ 0
+ );
+ });
+ test('should open and close wizard', async () => {
+ waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
+ await act(async () =>
+ wrapper.find('Button[aria-label="Add resource roles"]').prop('onClick')()
+ );
+ wrapper.update();
+ expect(wrapper.find('PFWizard').length).toBe(1);
+ await act(async () =>
+ wrapper.find("Button[aria-label='Close']").prop('onClick')()
+ );
+ wrapper.update();
+ expect(wrapper.find('PFWizard').length).toBe(0);
+ });
});
diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx
index db62a14bd6..b44a83f303 100644
--- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx
+++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx
@@ -1,15 +1,16 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { useParams, useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
+import { Button } from '@patternfly/react-core';
+
import { getQSConfig, parseQueryString } from '../../../util/qs';
import { UsersAPI } from '../../../api';
import useRequest from '../../../util/useRequest';
-import PaginatedDataList, {
- ToolbarAddButton,
-} from '../../../components/PaginatedDataList';
+import PaginatedDataList from '../../../components/PaginatedDataList';
import DatalistToolbar from '../../../components/DataListToolbar';
import UserAccessListItem from './UserAccessListItem';
+import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd';
const QS_CONFIG = getQSConfig('roles', {
page: 1,
@@ -22,6 +23,7 @@ const QS_CONFIG = getQSConfig('roles', {
function UserAccessList({ i18n }) {
const { id } = useParams();
const { search } = useLocation();
+ const [isWizardOpen, setIsWizardOpen] = useState(false);
const {
isLoading,
@@ -55,6 +57,11 @@ function UserAccessList({ i18n }) {
const canAdd =
options && Object.prototype.hasOwnProperty.call(options, 'POST');
+ const saveRoles = () => {
+ setIsWizardOpen(false);
+ fetchRoles();
+ };
+
const detailUrl = role => {
const { resource_id, resource_type } = role.summary_fields;
@@ -72,48 +79,71 @@ function UserAccessList({ i18n }) {
};
return (
- {
- return (
- {}}
- isSelected={false}
+ <>
+ {
+ return (
+ {}}
+ isSelected={false}
+ />
+ );
+ }}
+ renderToolbar={props => (
+ {
+ setIsWizardOpen(true);
+ }}
+ >
+ Add
+ ,
+ ]
+ : []),
+ ]}
/>
- );
- }}
- renderToolbar={props => (
- ] : []),
- ]}
+ )}
+ />
+ {isWizardOpen && (
+ setIsWizardOpen(false)}
+ title={i18n._(t`Add user permissions`)}
/>
)}
- />
+ >
);
}
export default withI18n()(UserAccessList);
diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx
index acb1400608..8a59012a30 100644
--- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx
+++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx
@@ -29,6 +29,7 @@ describe('', () => {
resource_type_display_name: 'Job Template',
user_capabilities: { unattach: true },
},
+ description: 'Can manage all aspects of the job template',
},
{
id: 3,
@@ -42,6 +43,7 @@ describe('', () => {
resource_type_display_name: 'Job Template',
user_capabilities: { unattach: true },
},
+ description: 'Can manage all aspects of the job template',
},
{
id: 4,
@@ -55,10 +57,11 @@ describe('', () => {
resource_type_display_name: 'Credential',
user_capabilities: { unattach: true },
},
+ description: 'May run the job template',
},
{
id: 5,
- name: 'Update',
+ name: 'Read',
type: 'role',
url: '/api/v2/roles/259/',
summary_fields: {
@@ -68,6 +71,7 @@ describe('', () => {
resource_type_display_name: 'Inventory',
user_capabilities: { unattach: true },
},
+ description: 'May view settings for the job template',
},
{
id: 6,
@@ -75,15 +79,16 @@ describe('', () => {
type: 'role',
url: '/api/v2/roles/260/',
summary_fields: {
- resource_name: 'Smart Inventory Foo',
+ resource_name: 'Project Foo',
resource_id: 77,
- resource_type: 'smart_inventory',
- resource_type_display_name: 'Inventory',
+ resource_type: 'project',
+ resource_type_display_name: 'Project',
user_capabilities: { unattach: true },
},
+ description: 'Can manage all aspects of the job template',
},
],
- count: 4,
+ count: 5,
},
});
@@ -138,7 +143,86 @@ describe('', () => {
'/inventories/inventory/76/details'
);
expect(wrapper.find('Link#userRole-6').prop('to')).toBe(
- '/inventories/smart_inventory/77/details'
+ '/projects/77/details'
);
});
+ test('should not render add button', async () => {
+ UsersAPI.readRoleOptions.mockResolvedValueOnce({
+ data: {},
+ });
+
+ UsersAPI.readRoles.mockResolvedValue({
+ data: {
+ results: [
+ {
+ id: 2,
+ name: 'Admin',
+ type: 'role',
+ url: '/api/v2/roles/257/',
+ summary_fields: {
+ resource_name: 'template delete project',
+ resource_id: 15,
+ resource_type: 'job_template',
+ resource_type_display_name: 'Job Template',
+ user_capabilities: { unattach: true },
+ object_roles: {
+ admin_role: {
+ description: 'Can manage all aspects of the job template',
+ name: 'Admin',
+ id: 164,
+ },
+ execute_role: {
+ description: 'May run the job template',
+ name: 'Execute',
+ id: 165,
+ },
+ read_role: {
+ description: 'May view settings for the job template',
+ name: 'Read',
+ id: 166,
+ },
+ },
+ },
+ },
+ ],
+ count: 1,
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+
+ ,
+ {
+ context: {
+ router: {
+ history,
+ route: {
+ location: history.location,
+ match: { params: { id: 18 } },
+ },
+ },
+ },
+ }
+ );
+ });
+
+ waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
+ expect(wrapper.find('Button[aria-label="Add resource roles"]').length).toBe(
+ 0
+ );
+ });
+ test('should open and close wizard', async () => {
+ waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
+ await act(async () =>
+ wrapper.find('Button[aria-label="Add resource roles"]').prop('onClick')()
+ );
+ wrapper.update();
+ expect(wrapper.find('PFWizard').length).toBe(1);
+ await act(async () =>
+ wrapper.find("Button[aria-label='Close']").prop('onClick')()
+ );
+ wrapper.update();
+ expect(wrapper.find('PFWizard').length).toBe(0);
+ });
});