mirror of
https://github.com/ansible/awx.git
synced 2026-05-11 19:37:38 -02:30
Add related job templates to a couple of screens (#11890)
Add related job templates to a couple of screens. Credential and Inventory. Also refactor the component already in place for Projects to be in sync with the Job Templates screen. See: https://github.com/ansible/awx/issues/5867
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t, Plural } from '@lingui/macro';
|
||||||
import { Card } from '@patternfly/react-core';
|
import { Card } from '@patternfly/react-core';
|
||||||
import { JobTemplatesAPI } from 'api';
|
import { JobTemplatesAPI } from 'api';
|
||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
@@ -14,10 +14,14 @@ import PaginatedTable, {
|
|||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
getSearchableKeys,
|
getSearchableKeys,
|
||||||
} from 'components/PaginatedTable';
|
} from 'components/PaginatedTable';
|
||||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
|
||||||
|
import useWsTemplates from 'hooks/useWsTemplates';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
|
import useExpanded from 'hooks/useExpanded';
|
||||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||||
import ProjectTemplatesListItem from './ProjectJobTemplatesListItem';
|
import { TemplateListItem } from 'components/TemplateList';
|
||||||
|
import useToast, { AlertVariant } from 'hooks/useToast';
|
||||||
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('template', {
|
const QS_CONFIG = getQSConfig('template', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -25,13 +29,13 @@ const QS_CONFIG = getQSConfig('template', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function ProjectJobTemplatesList() {
|
function RelatedTemplateList({ searchParams }) {
|
||||||
const { id: projectId } = useParams();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
jobTemplates,
|
results,
|
||||||
itemCount,
|
itemCount,
|
||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
@@ -43,13 +47,12 @@ function ProjectJobTemplatesList() {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
params.project = projectId;
|
|
||||||
const [response, actionsResponse] = await Promise.all([
|
const [response, actionsResponse] = await Promise.all([
|
||||||
JobTemplatesAPI.read(params),
|
JobTemplatesAPI.read(mergeParams(params, searchParams)),
|
||||||
JobTemplatesAPI.readOptions(),
|
JobTemplatesAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
jobTemplates: response.data.results,
|
results: response.data.results,
|
||||||
itemCount: response.data.count,
|
itemCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: actionsResponse.data.actions,
|
||||||
relatedSearchableKeys: (
|
relatedSearchableKeys: (
|
||||||
@@ -57,9 +60,9 @@ function ProjectJobTemplatesList() {
|
|||||||
).map((val) => val.slice(0, -8)),
|
).map((val) => val.slice(0, -8)),
|
||||||
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
|
searchableKeys: getSearchableKeys(actionsResponse.data.actions?.GET),
|
||||||
};
|
};
|
||||||
}, [location, projectId]),
|
}, [location]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
{
|
{
|
||||||
jobTemplates: [],
|
results: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: {},
|
actions: {},
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
@@ -71,9 +74,14 @@ function ProjectJobTemplatesList() {
|
|||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
}, [fetchTemplates]);
|
}, [fetchTemplates]);
|
||||||
|
|
||||||
|
const jobTemplates = useWsTemplates(results);
|
||||||
|
|
||||||
const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
|
const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
|
||||||
useSelected(jobTemplates);
|
useSelected(jobTemplates);
|
||||||
|
|
||||||
|
const { expanded, isAllExpanded, handleExpand, expandAll } =
|
||||||
|
useExpanded(jobTemplates);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading: isDeleteLoading,
|
isLoading: isDeleteLoading,
|
||||||
deleteItems: deleteTemplates,
|
deleteItems: deleteTemplates,
|
||||||
@@ -94,6 +102,18 @@ function ProjectJobTemplatesList() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
(newTemplateId) => {
|
||||||
|
addToast({
|
||||||
|
id: newTemplateId,
|
||||||
|
title: t`Template copied successfully`,
|
||||||
|
variant: AlertVariant.success,
|
||||||
|
hasTimeout: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
const handleTemplateDelete = async () => {
|
const handleTemplateDelete = async () => {
|
||||||
await deleteTemplates();
|
await deleteTemplates();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -106,6 +126,10 @@ function ProjectJobTemplatesList() {
|
|||||||
<ToolbarAddButton key="add" linkTo="/templates/job_template/add/" />
|
<ToolbarAddButton key="add" linkTo="/templates/job_template/add/" />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||||
|
selected[0]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -131,14 +155,32 @@ function ProjectJobTemplatesList() {
|
|||||||
name: t`Modified By (Username)`,
|
name: t`Modified By (Username)`,
|
||||||
key: 'modified_by__username__icontains',
|
key: 'modified_by__username__icontains',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t`Playbook name`,
|
||||||
|
key: 'job_template__playbook__icontains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`Label`,
|
||||||
|
key: 'labels__name__icontains',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
toolbarSearchableKeys={searchableKeys}
|
toolbarSearchableKeys={searchableKeys}
|
||||||
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
|
headerRow={
|
||||||
|
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
||||||
|
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
||||||
|
<HeaderCell sortKey="type">{t`Type`}</HeaderCell>
|
||||||
|
<HeaderCell>{t`Recent jobs`}</HeaderCell>
|
||||||
|
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
}
|
||||||
renderToolbar={(props) => (
|
renderToolbar={(props) => (
|
||||||
<DatalistToolbar
|
<DatalistToolbar
|
||||||
{...props}
|
{...props}
|
||||||
isAllSelected={isAllSelected}
|
isAllSelected={isAllSelected}
|
||||||
onSelectAll={selectAll}
|
onSelectAll={selectAll}
|
||||||
|
isAllExpanded={isAllExpanded}
|
||||||
|
onExpandAll={expandAll}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
...(canAddJT ? [addButton] : []),
|
...(canAddJT ? [addButton] : []),
|
||||||
@@ -147,32 +189,37 @@ function ProjectJobTemplatesList() {
|
|||||||
onDelete={handleTemplateDelete}
|
onDelete={handleTemplateDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={t`Job templates`}
|
pluralizedItemName={t`Job templates`}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={
|
||||||
|
<Plural
|
||||||
|
value={selected.length}
|
||||||
|
one="This template is currently being used by some workflow nodes. Are you sure you want to delete it?"
|
||||||
|
other="Deleting these templates could impact some workflow nodes that rely on them. Are you sure you want to delete anyway?"
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
headerRow={
|
|
||||||
<HeaderRow qsConfig={QS_CONFIG}>
|
|
||||||
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
|
|
||||||
<HeaderCell sortKey="type">{t`Type`}</HeaderCell>
|
|
||||||
<HeaderCell>{t`Recent jobs`}</HeaderCell>
|
|
||||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
|
||||||
</HeaderRow>
|
|
||||||
}
|
|
||||||
renderRow={(template, index) => (
|
renderRow={(template, index) => (
|
||||||
<ProjectTemplatesListItem
|
<TemplateListItem
|
||||||
key={template.id}
|
key={template.id}
|
||||||
value={template.name}
|
value={template.name}
|
||||||
template={template}
|
template={template}
|
||||||
detailUrl={`/templates/${template.type}/${template.id}/details`}
|
detailUrl={`/templates/${template.type}/${template.id}`}
|
||||||
onSelect={() => handleSelect(template)}
|
onSelect={() => handleSelect(template)}
|
||||||
|
isExpanded={expanded.some((row) => row.id === template.id)}
|
||||||
|
onExpand={() => handleExpand(template)}
|
||||||
|
onCopy={handleCopy}
|
||||||
isSelected={selected.some((row) => row.id === template.id)}
|
isSelected={selected.some((row) => row.id === template.id)}
|
||||||
|
fetchTemplates={fetchTemplates}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
emptyStateControls={canAddJT && addButton}
|
emptyStateControls={canAddJT && addButton}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Toast {...toastProps} />
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -186,4 +233,4 @@ function ProjectJobTemplatesList() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProjectJobTemplatesList;
|
export default RelatedTemplateList;
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { JobTemplatesAPI, UnifiedJobTemplatesAPI } from 'api';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
|
import RelatedTemplateList from './RelatedTemplateList';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
|
||||||
|
const mockTemplates = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Job Template 1',
|
||||||
|
url: '/templates/job_template/1',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
edit: true,
|
||||||
|
copy: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Job Template 2',
|
||||||
|
url: '/templates/job_template/2',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Job Template 3',
|
||||||
|
url: '/templates/job_template/3',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('<TemplateList />', () => {
|
||||||
|
let debug;
|
||||||
|
beforeEach(() => {
|
||||||
|
JobTemplatesAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: mockTemplates.length,
|
||||||
|
results: mockTemplates,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
JobTemplatesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
||||||
|
global.console.debug = () => {};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
global.console.debug = debug;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Templates are retrieved from the api and the components finishes loading', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(JobTemplatesAPI.read).toBeCalledWith({
|
||||||
|
credentials__id: 1,
|
||||||
|
order_by: 'name',
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('TemplateListItem').length).toEqual(
|
||||||
|
mockTemplates.length
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleSelect is called when a template list item is selected', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
});
|
||||||
|
const checkBox = wrapper.find('TemplateListItem').at(1).find('input');
|
||||||
|
|
||||||
|
checkBox.simulate('change', {
|
||||||
|
target: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Job Template 2',
|
||||||
|
url: '/templates/job_template/2',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: { user_capabilities: { delete: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('TemplateListItem').at(1).prop('isSelected')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleSelectAll is called when a template list item is selected', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(false);
|
||||||
|
|
||||||
|
const toolBarCheckBox = wrapper.find('Checkbox#select-all');
|
||||||
|
act(() => {
|
||||||
|
toolBarCheckBox.prop('onChange')(true);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete button is disabled if user does not have delete capabilities on a selected template', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
});
|
||||||
|
const deleteAbleItem = wrapper.find('TemplateListItem').at(0).find('input');
|
||||||
|
const nonDeleteAbleItem = wrapper
|
||||||
|
.find('TemplateListItem')
|
||||||
|
.at(2)
|
||||||
|
.find('input');
|
||||||
|
|
||||||
|
deleteAbleItem.simulate('change', {
|
||||||
|
id: 1,
|
||||||
|
name: 'Job Template 1',
|
||||||
|
url: '/templates/job_template/1',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
deleteAbleItem.simulate('change', {
|
||||||
|
id: 1,
|
||||||
|
name: 'Job Template 1',
|
||||||
|
url: '/templates/job_template/1',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
nonDeleteAbleItem.simulate('change', {
|
||||||
|
id: 5,
|
||||||
|
name: 'Workflow Job Template 2',
|
||||||
|
url: '/templates/workflow_job_template/5',
|
||||||
|
type: 'workflow_job_template',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('api is called to delete templates for each selected template.', async () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
});
|
||||||
|
const jobTemplate = wrapper.find('TemplateListItem').at(1).find('input');
|
||||||
|
|
||||||
|
jobTemplate.simulate('change', {
|
||||||
|
target: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Job Template 2',
|
||||||
|
url: '/templates/job_template/2',
|
||||||
|
type: 'job_template',
|
||||||
|
summary_fields: { user_capabilities: { delete: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Delete"]').prop('onClick')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
await wrapper
|
||||||
|
.find('button[aria-label="confirm delete"]')
|
||||||
|
.prop('onClick')();
|
||||||
|
});
|
||||||
|
expect(JobTemplatesAPI.destroy).toBeCalledWith(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('error is shown when template not successfully deleted from api', async () => {
|
||||||
|
JobTemplatesAPI.destroy.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/job_templates/1',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(JobTemplatesAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('TemplateListItem').at(0).invoke('onSelect')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
const modal = wrapper.find('Modal');
|
||||||
|
expect(modal).toHaveLength(1);
|
||||||
|
expect(modal.prop('title')).toEqual('Error!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should properly copy template', async () => {
|
||||||
|
JobTemplatesAPI.copy.mockResolvedValue({});
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<RelatedTemplateList searchParams={{ credentials__id: 1 }} />
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||||
|
});
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
expect(JobTemplatesAPI.copy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui/src/components/RelatedTemplateList/index.js
Normal file
1
awx/ui/src/components/RelatedTemplateList/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './RelatedTemplateList';
|
||||||
@@ -17,6 +17,7 @@ import { ResourceAccessList } from 'components/ResourceAccessList';
|
|||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
import RoutedTabs from 'components/RoutedTabs';
|
import RoutedTabs from 'components/RoutedTabs';
|
||||||
|
import RelatedTemplateList from 'components/RelatedTemplateList';
|
||||||
import { CredentialsAPI } from 'api';
|
import { CredentialsAPI } from 'api';
|
||||||
import CredentialDetail from './CredentialDetail';
|
import CredentialDetail from './CredentialDetail';
|
||||||
import CredentialEdit from './CredentialEdit';
|
import CredentialEdit from './CredentialEdit';
|
||||||
@@ -73,6 +74,11 @@ function Credential({ setBreadcrumb }) {
|
|||||||
link: `/credentials/${id}/access`,
|
link: `/credentials/${id}/access`,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t`Job Templates`,
|
||||||
|
link: `/credentials/${id}/job_templates`,
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
@@ -123,6 +129,11 @@ function Credential({ setBreadcrumb }) {
|
|||||||
apiModel={CredentialsAPI}
|
apiModel={CredentialsAPI}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
|
<Route key="job_templates" path="/credentials/:id/job_templates">
|
||||||
|
<RelatedTemplateList
|
||||||
|
searchParams={{ credentials__id: credential.id }}
|
||||||
|
/>
|
||||||
|
</Route>,
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
{!hasContentLoading && (
|
{!hasContentLoading && (
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import mockCredential from './shared/data.scmCredential.json';
|
import mockCredential from './shared/data.scmCredential.json';
|
||||||
import mockOrgCredential from './shared/data.orgCredential.json';
|
|
||||||
import Credential from './Credential';
|
import Credential from './Credential';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
@@ -32,21 +31,24 @@ describe('<Credential />', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
wrapper.update();
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 3);
|
expect(wrapper.find('Credential').length).toBe(1);
|
||||||
|
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders org-based credential successfully', async () => {
|
test('should render expected tabs', async () => {
|
||||||
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
const expectedTabs = [
|
||||||
data: mockOrgCredential,
|
'Back to Credentials',
|
||||||
});
|
'Details',
|
||||||
|
'Access',
|
||||||
|
'Job Templates',
|
||||||
|
];
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
// org-based credential detail needs access tab
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 3);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function Credentials() {
|
|||||||
[`/credentials/${credential.id}/edit`]: t`Edit Details`,
|
[`/credentials/${credential.id}/edit`]: t`Edit Details`,
|
||||||
[`/credentials/${credential.id}/details`]: t`Details`,
|
[`/credentials/${credential.id}/details`]: t`Details`,
|
||||||
[`/credentials/${credential.id}/access`]: t`Access`,
|
[`/credentials/${credential.id}/access`]: t`Access`,
|
||||||
|
[`/credentials/${credential.id}/job_templates`]: t`Job Templates`,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function Inventories() {
|
|||||||
[`${inventoryPath}/access`]: t`Access`,
|
[`${inventoryPath}/access`]: t`Access`,
|
||||||
[`${inventoryPath}/jobs`]: t`Jobs`,
|
[`${inventoryPath}/jobs`]: t`Jobs`,
|
||||||
[`${inventoryPath}/details`]: t`Details`,
|
[`${inventoryPath}/details`]: t`Details`,
|
||||||
|
[`${inventoryPath}/job_templates`]: t`Job Templates`,
|
||||||
[`${inventoryPath}/edit`]: t`Edit details`,
|
[`${inventoryPath}/edit`]: t`Edit details`,
|
||||||
|
|
||||||
[inventoryHostsPath]: t`Hosts`,
|
[inventoryHostsPath]: t`Hosts`,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ContentLoading from 'components/ContentLoading';
|
|||||||
import JobList from 'components/JobList';
|
import JobList from 'components/JobList';
|
||||||
import RoutedTabs from 'components/RoutedTabs';
|
import RoutedTabs from 'components/RoutedTabs';
|
||||||
import { ResourceAccessList } from 'components/ResourceAccessList';
|
import { ResourceAccessList } from 'components/ResourceAccessList';
|
||||||
|
import RelatedTemplateList from 'components/RelatedTemplateList';
|
||||||
import { InventoriesAPI } from 'api';
|
import { InventoriesAPI } from 'api';
|
||||||
import InventoryDetail from './InventoryDetail';
|
import InventoryDetail from './InventoryDetail';
|
||||||
import InventoryEdit from './InventoryEdit';
|
import InventoryEdit from './InventoryEdit';
|
||||||
@@ -69,6 +70,7 @@ function Inventory({ setBreadcrumb }) {
|
|||||||
link: `${match.url}/jobs`,
|
link: `${match.url}/jobs`,
|
||||||
id: 5,
|
id: 5,
|
||||||
},
|
},
|
||||||
|
{ name: t`Job Templates`, link: `${match.url}/job_templates`, id: 6 },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
@@ -172,6 +174,14 @@ function Inventory({ setBreadcrumb }) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
|
<Route
|
||||||
|
path="/inventories/inventory/:id/job_templates"
|
||||||
|
key="job_templates"
|
||||||
|
>
|
||||||
|
<RelatedTemplateList
|
||||||
|
searchParams={{ inventory__id: inventory.id }}
|
||||||
|
/>
|
||||||
|
</Route>,
|
||||||
<Route path="*" key="not-found">
|
<Route path="*" key="not-found">
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
{match.params.id && (
|
{match.params.id && (
|
||||||
|
|||||||
@@ -31,8 +31,27 @@ describe('<Inventory />', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
wrapper.update();
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 7);
|
expect(wrapper.find('Inventory').length).toBe(1);
|
||||||
|
expect(wrapper.find('RoutedTabs li').length).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', async () => {
|
||||||
|
const expectedTabs = [
|
||||||
|
'Back to Inventories',
|
||||||
|
'Details',
|
||||||
|
'Access',
|
||||||
|
'Groups',
|
||||||
|
'Hosts',
|
||||||
|
'Jobs',
|
||||||
|
'Job Templates',
|
||||||
|
];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import ContentLoading from 'components/ContentLoading';
|
|||||||
import JobList from 'components/JobList';
|
import JobList from 'components/JobList';
|
||||||
import { ResourceAccessList } from 'components/ResourceAccessList';
|
import { ResourceAccessList } from 'components/ResourceAccessList';
|
||||||
import RoutedTabs from 'components/RoutedTabs';
|
import RoutedTabs from 'components/RoutedTabs';
|
||||||
|
import RelatedTemplateList from 'components/RelatedTemplateList';
|
||||||
import SmartInventoryDetail from './SmartInventoryDetail';
|
import SmartInventoryDetail from './SmartInventoryDetail';
|
||||||
import SmartInventoryEdit from './SmartInventoryEdit';
|
import SmartInventoryEdit from './SmartInventoryEdit';
|
||||||
import SmartInventoryHosts from './SmartInventoryHosts';
|
import SmartInventoryHosts from './SmartInventoryHosts';
|
||||||
@@ -70,6 +71,7 @@ function SmartInventory({ setBreadcrumb }) {
|
|||||||
link: `${match.url}/jobs`,
|
link: `${match.url}/jobs`,
|
||||||
id: 3,
|
id: 3,
|
||||||
},
|
},
|
||||||
|
{ name: t`Job Templates`, link: `${match.url}/job_templates`, id: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
@@ -155,6 +157,14 @@ function SmartInventory({ setBreadcrumb }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
|
<Route
|
||||||
|
key="job_templates"
|
||||||
|
path="/inventories/smart_inventory/:id/job_templates"
|
||||||
|
>
|
||||||
|
<RelatedTemplateList
|
||||||
|
searchParams={{ inventory__id: inventory.id }}
|
||||||
|
/>
|
||||||
|
</Route>,
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
{!hasContentLoading && (
|
{!hasContentLoading && (
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
|
|||||||
@@ -32,8 +32,26 @@ describe('<SmartInventory />', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'SmartInventory');
|
wrapper.update();
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', (el) => el.length === 5);
|
expect(wrapper.find('SmartInventory').length).toBe(1);
|
||||||
|
expect(wrapper.find('RoutedTabs li').length).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', async () => {
|
||||||
|
const expectedTabs = [
|
||||||
|
'Back to Inventories',
|
||||||
|
'Details',
|
||||||
|
'Access',
|
||||||
|
'Hosts',
|
||||||
|
'Jobs',
|
||||||
|
'Job Templates',
|
||||||
|
];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error when api throws an error', async () => {
|
test('should show content error when api throws an error', async () => {
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import ContentLoading from 'components/ContentLoading';
|
|||||||
import NotificationList from 'components/NotificationList';
|
import NotificationList from 'components/NotificationList';
|
||||||
import { ResourceAccessList } from 'components/ResourceAccessList';
|
import { ResourceAccessList } from 'components/ResourceAccessList';
|
||||||
import { Schedules } from 'components/Schedule';
|
import { Schedules } from 'components/Schedule';
|
||||||
|
import RelatedTemplateList from 'components/RelatedTemplateList';
|
||||||
import { OrganizationsAPI, ProjectsAPI } from 'api';
|
import { OrganizationsAPI, ProjectsAPI } from 'api';
|
||||||
import ProjectDetail from './ProjectDetail';
|
import ProjectDetail from './ProjectDetail';
|
||||||
import ProjectEdit from './ProjectEdit';
|
import ProjectEdit from './ProjectEdit';
|
||||||
import ProjectJobTemplatesList from './ProjectJobTemplatesList';
|
|
||||||
|
|
||||||
function Project({ setBreadcrumb }) {
|
function Project({ setBreadcrumb }) {
|
||||||
const { me = {} } = useConfig();
|
const { me = {} } = useConfig();
|
||||||
@@ -102,6 +102,10 @@ function Project({ setBreadcrumb }) {
|
|||||||
},
|
},
|
||||||
{ name: t`Details`, link: `/projects/${id}/details` },
|
{ name: t`Details`, link: `/projects/${id}/details` },
|
||||||
{ name: t`Access`, link: `/projects/${id}/access` },
|
{ name: t`Access`, link: `/projects/${id}/access` },
|
||||||
|
{
|
||||||
|
name: t`Job Templates`,
|
||||||
|
link: `/projects/${id}/job_templates`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (canSeeNotificationsTab) {
|
if (canSeeNotificationsTab) {
|
||||||
@@ -110,12 +114,6 @@ function Project({ setBreadcrumb }) {
|
|||||||
link: `/projects/${id}/notifications`,
|
link: `/projects/${id}/notifications`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tabsArray.push({
|
|
||||||
name: t`Job Templates`,
|
|
||||||
link: `/projects/${id}/job_templates`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (project?.scm_type) {
|
if (project?.scm_type) {
|
||||||
tabsArray.push({
|
tabsArray.push({
|
||||||
name: t`Schedules`,
|
name: t`Schedules`,
|
||||||
@@ -176,7 +174,7 @@ function Project({ setBreadcrumb }) {
|
|||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
<Route path="/projects/:id/job_templates">
|
<Route path="/projects/:id/job_templates">
|
||||||
<ProjectJobTemplatesList />
|
<RelatedTemplateList searchParams={{ project__id: project.id }} />
|
||||||
</Route>
|
</Route>
|
||||||
{project?.scm_type && project.scm_type !== '' && (
|
{project?.scm_type && project.scm_type !== '' && (
|
||||||
<Route path="/projects/:id/schedules">
|
<Route path="/projects/:id/schedules">
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe('<Project />', () => {
|
|||||||
'.pf-c-tabs__item-text',
|
'.pf-c-tabs__item-text',
|
||||||
(el) => el.length === 6
|
(el) => el.length === 6
|
||||||
);
|
);
|
||||||
expect(tabs.at(3).text()).toEqual('Notifications');
|
expect(tabs.at(4).text()).toEqual('Notifications');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('notifications tab hidden with reduced permissions', async () => {
|
test('notifications tab hidden with reduced permissions', async () => {
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import 'styled-components/macro';
|
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Button, Tooltip } from '@patternfly/react-core';
|
|
||||||
import { Tr, Td } from '@patternfly/react-table';
|
|
||||||
import {
|
|
||||||
ExclamationTriangleIcon,
|
|
||||||
PencilAltIcon,
|
|
||||||
RocketIcon,
|
|
||||||
} from '@patternfly/react-icons';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
|
||||||
import { LaunchButton } from 'components/LaunchButton';
|
|
||||||
import Sparkline from 'components/Sparkline';
|
|
||||||
import { toTitleCase } from 'util/strings';
|
|
||||||
|
|
||||||
const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)`
|
|
||||||
color: var(--pf-global--warning-color--100);
|
|
||||||
margin-left: 18px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function ProjectJobTemplateListItem({
|
|
||||||
template,
|
|
||||||
isSelected,
|
|
||||||
onSelect,
|
|
||||||
detailUrl,
|
|
||||||
rowIndex,
|
|
||||||
}) {
|
|
||||||
const canLaunch = template.summary_fields.user_capabilities.start;
|
|
||||||
|
|
||||||
const missingResourceIcon =
|
|
||||||
template.type === 'job_template' &&
|
|
||||||
(!template.summary_fields.project ||
|
|
||||||
(!template.summary_fields.inventory &&
|
|
||||||
!template.ask_inventory_on_launch));
|
|
||||||
|
|
||||||
const missingExecutionEnvironment =
|
|
||||||
template.type === 'job_template' &&
|
|
||||||
template.custom_virtualenv &&
|
|
||||||
!template.execution_environment;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr
|
|
||||||
id={`template-row-${template.id}`}
|
|
||||||
ouiaId={`template-row-${template.id}`}
|
|
||||||
>
|
|
||||||
<Td
|
|
||||||
select={{
|
|
||||||
rowIndex,
|
|
||||||
isSelected,
|
|
||||||
onSelect,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Td dataLabel={t`Name`}>
|
|
||||||
<Link to={`${detailUrl}`}>
|
|
||||||
{template.name}
|
|
||||||
{missingResourceIcon && (
|
|
||||||
<Tooltip
|
|
||||||
content={t`Resources are missing from this template.`}
|
|
||||||
position="right"
|
|
||||||
>
|
|
||||||
<ExclamationTriangleIcon css="color: #c9190b; margin-left: 20px;" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{missingExecutionEnvironment && (
|
|
||||||
<Tooltip
|
|
||||||
content={t`Custom virtual environment ${template.custom_virtualenv} must be replaced by an execution environment.`}
|
|
||||||
position="right"
|
|
||||||
className="missing-execution-environment"
|
|
||||||
>
|
|
||||||
<ExclamationTriangleIconWarning />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</Td>
|
|
||||||
<Td dataLabel={t`Type`}>{toTitleCase(template.type)}</Td>
|
|
||||||
<Td dataLabel={t`Recent jobs`}>
|
|
||||||
<Sparkline jobs={template.summary_fields.recent_jobs} />
|
|
||||||
</Td>
|
|
||||||
<ActionsTd dataLabel={t`Actions`}>
|
|
||||||
<ActionItem
|
|
||||||
visible={canLaunch && template.type === 'job_template'}
|
|
||||||
tooltip={t`Launch Template`}
|
|
||||||
>
|
|
||||||
<LaunchButton resource={template}>
|
|
||||||
{({ handleLaunch, isLaunching }) => (
|
|
||||||
<Button
|
|
||||||
ouiaId={`${template.id}-launch-button`}
|
|
||||||
css="grid-column: 1"
|
|
||||||
variant="plain"
|
|
||||||
onClick={handleLaunch}
|
|
||||||
isDisabled={isLaunching}
|
|
||||||
>
|
|
||||||
<RocketIcon />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</LaunchButton>
|
|
||||||
</ActionItem>
|
|
||||||
<ActionItem
|
|
||||||
visible={template.summary_fields.user_capabilities.edit}
|
|
||||||
tooltip={t`Edit Template`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
ouiaId={`${template.id}-edit-button`}
|
|
||||||
css="grid-column: 2"
|
|
||||||
variant="plain"
|
|
||||||
component={Link}
|
|
||||||
to={`/templates/${template.type}/${template.id}/edit`}
|
|
||||||
>
|
|
||||||
<PencilAltIcon />
|
|
||||||
</Button>
|
|
||||||
</ActionItem>
|
|
||||||
</ActionsTd>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ProjectJobTemplateListItem as _ProjectJobTemplateListItem };
|
|
||||||
export default ProjectJobTemplateListItem;
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { createMemoryHistory } from 'history';
|
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
|
||||||
import ProjectJobTemplatesListItem from './ProjectJobTemplatesListItem';
|
|
||||||
|
|
||||||
describe('<ProjectJobTemplatesListItem />', () => {
|
|
||||||
test('launch button shown to users with start capabilities', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
start: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('LaunchButton').exists()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('launch button hidden from users without start capabilities', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
start: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('LaunchButton').exists()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('edit button shown to users with edit capabilities', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('edit button hidden from users without edit capabilities', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('missing resource icon is shown.', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('missing resource icon is not shown when there is a project and an inventory.', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
project: { name: 'Foo', id: 2 },
|
|
||||||
inventory: { name: 'Bar', id: 2 },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
ask_inventory_on_launch: true,
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
project: { name: 'Foo', id: 2 },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
test('missing resource icon is not shown type is workflow_job_template', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'workflow_job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
expect(wrapper.find('ExclamationTriangleIcon').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
test('clicking on template from project templates list navigates properly', () => {
|
|
||||||
const history = createMemoryHistory({
|
|
||||||
initialEntries: ['/projects/1/job_templates'],
|
|
||||||
});
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
detailUrl="/templates/job_template/2/details"
|
|
||||||
template={{
|
|
||||||
id: 2,
|
|
||||||
name: 'Template 2',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>,
|
|
||||||
{ context: { router: { history } } }
|
|
||||||
);
|
|
||||||
wrapper.find('Link').simulate('click', { button: 0 });
|
|
||||||
expect(history.location.pathname).toEqual(
|
|
||||||
'/templates/job_template/2/details'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render warning about missing execution environment', () => {
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ProjectJobTemplatesListItem
|
|
||||||
isSelected={false}
|
|
||||||
template={{
|
|
||||||
id: 1,
|
|
||||||
name: 'Template 1',
|
|
||||||
url: '/templates/job_template/1',
|
|
||||||
type: 'job_template',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
custom_virtualenv: '/var/lib/awx/env',
|
|
||||||
execution_environment: null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
wrapper.find('.missing-execution-environment').prop('content')
|
|
||||||
).toEqual(
|
|
||||||
'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './ProjectJobTemplatesList';
|
|
||||||
Reference in New Issue
Block a user