-
-
-
- {error && (
-
+ {onExpand && (
+
+
- )}
-
- >
+
+
+
+ )}
+
);
}
-VariablesDetail.propTypes = {
- value: oneOfType([shape({}), arrayOf(string), string]).isRequired,
- label: node.isRequired,
- rows: number,
- dataCy: string,
- helpText: string,
-};
-VariablesDetail.defaultProps = {
- rows: null,
- dataCy: '',
- helpText: '',
-};
export default withI18n()(VariablesDetail);
diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx
index 8fbf1e4bfa..6f039c6895 100644
--- a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx
+++ b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx
@@ -4,7 +4,8 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import styled from 'styled-components';
-import { Split, SplitItem } from '@patternfly/react-core';
+import { Split, SplitItem, Button, Modal } from '@patternfly/react-core';
+import { ExpandArrowsAltIcon } from '@patternfly/react-icons';
import { CheckboxField } from '../FormField';
import MultiButtonToggle from '../MultiButtonToggle';
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
@@ -20,6 +21,7 @@ const FieldHeader = styled.div`
const StyledCheckboxField = styled(CheckboxField)`
--pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize);
+ margin-left: auto;
`;
function VariablesField({
@@ -31,10 +33,92 @@ function VariablesField({
promptId,
tooltip,
}) {
- const [field, meta, helpers] = useField(name);
+ const [field, meta] = useField(name);
const [mode, setMode] = useState(
isJsonString(field.value) ? JSON_MODE : YAML_MODE
);
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+ <>
+
setIsExpanded(true)}
+ mode={mode}
+ setMode={setMode}
+ />
+ setIsExpanded(false)}
+ actions={[
+ ,
+ ]}
+ >
+
+
+
+
+ {meta.error ? (
+
+ {meta.error}
+
+ ) : null}
+ >
+ );
+}
+VariablesField.propTypes = {
+ id: string.isRequired,
+ name: string.isRequired,
+ label: string.isRequired,
+ readOnly: bool,
+ promptId: string,
+};
+VariablesField.defaultProps = {
+ readOnly: false,
+ promptId: null,
+};
+
+function VariablesFieldInternals({
+ i18n,
+ id,
+ name,
+ label,
+ readOnly,
+ promptId,
+ tooltip,
+ fullHeight,
+ mode,
+ setMode,
+ onExpand,
+}) {
+ const [field, meta, helpers] = useField(name);
return (
@@ -75,6 +159,16 @@ function VariablesField({
name="ask_variables_on_launch"
/>
)}
+ {onExpand && (
+
+ )}
{
helpers.setValue(newVal);
}}
+ fullHeight={fullHeight}
hasErrors={!!meta.error}
/>
- {meta.error ? (
-
- {meta.error}
-
- ) : null}
);
}
-VariablesField.propTypes = {
- id: string.isRequired,
- name: string.isRequired,
- label: string.isRequired,
- readOnly: bool,
- promptId: string,
-};
-VariablesField.defaultProps = {
- readOnly: false,
- promptId: null,
-};
export default withI18n()(VariablesField);
diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx
index e07ff9d40b..24c896069e 100644
--- a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx
+++ b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx
@@ -32,7 +32,7 @@ describe('VariablesField', () => {
);
const buttons = wrapper.find('Button');
- expect(buttons).toHaveLength(2);
+ expect(buttons).toHaveLength(3);
expect(buttons.at(0).prop('variant')).toEqual('primary');
expect(buttons.at(1).prop('variant')).toEqual('secondary');
await act(async () => {
@@ -136,4 +136,27 @@ describe('VariablesField', () => {
expect(wrapper.find('CodeEditor').prop('mode')).toEqual('javascript');
});
+
+ it('should open modal when expanded', async () => {
+ const value = '---';
+ const wrapper = mountWithContexts(
+
+ {formik => (
+
+ )}
+
+ );
+ expect(wrapper.find('Modal').prop('isOpen')).toEqual(false);
+
+ wrapper.find('Button[variant="plain"]').invoke('onClick')();
+ wrapper.update();
+
+ expect(wrapper.find('Modal').prop('isOpen')).toEqual(true);
+ expect(wrapper.find('Modal CodeEditor')).toHaveLength(1);
+ });
});
diff --git a/awx/ui_next/src/components/DetailList/CodeDetail.jsx b/awx/ui_next/src/components/DetailList/CodeDetail.jsx
index 08c935b18e..90edb259e8 100644
--- a/awx/ui_next/src/components/DetailList/CodeDetail.jsx
+++ b/awx/ui_next/src/components/DetailList/CodeDetail.jsx
@@ -14,15 +14,7 @@ import { DetailName, DetailValue } from './Detail';
import CodeEditor from '../CodeEditor';
import Popover from '../Popover';
-function CodeDetail({
- value,
- label,
- mode,
- rows,
- fullHeight,
- helpText,
- dataCy,
-}) {
+function CodeDetail({ value, label, mode, rows, helpText, dataCy }) {
const labelCy = dataCy ? `${dataCy}-label` : null;
const valueCy = dataCy ? `${dataCy}-value` : null;
@@ -57,7 +49,6 @@ function CodeDetail({
value={value}
readOnly
rows={rows}
- fullHeight={fullHeight}
css="margin-top: 10px"
/>
@@ -69,7 +60,7 @@ CodeDetail.propTypes = {
label: node.isRequired,
dataCy: string,
helpText: string,
- rows: number,
+ rows: oneOfType(number, string),
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
};
CodeDetail.defaultProps = {
diff --git a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx
index 4647d5809e..b3134cb527 100644
--- a/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx
+++ b/awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx
@@ -25,6 +25,7 @@ function ExecutionEnvironmentLookup({
globallyAvailable,
i18n,
isDefaultEnvironment,
+ isGlobalDefaultEnvironment,
isDisabled,
onBlur,
onChange,
@@ -154,17 +155,26 @@ function ExecutionEnvironmentLookup({
>
);
+ const renderLabel = (
+ globalDefaultEnvironment,
+ defaultExecutionEnvironment
+ ) => {
+ if (globalDefaultEnvironment) {
+ return i18n._(t`Global Default Execution Environment`);
+ }
+ if (defaultExecutionEnvironment) {
+ return i18n._(t`Default Execution Environment`);
+ }
+ return i18n._(t`Execution Environment`);
+ };
+
return (
}
>
- {isDisabled ? (
+ {tooltip ? (
{renderLookup()}
) : (
renderLookup()
@@ -180,6 +190,7 @@ ExecutionEnvironmentLookup.propTypes = {
popoverContent: string,
onChange: func.isRequired,
isDefaultEnvironment: bool,
+ isGlobalDefaultEnvironment: bool,
projectId: oneOfType([number, string]),
organizationId: oneOfType([number, string]),
};
@@ -187,6 +198,7 @@ ExecutionEnvironmentLookup.propTypes = {
ExecutionEnvironmentLookup.defaultProps = {
popoverContent: '',
isDefaultEnvironment: false,
+ isGlobalDefaultEnvironment: false,
value: null,
projectId: null,
organizationId: null,
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx
index 55a3228e13..bb86dc2f57 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx
@@ -20,6 +20,7 @@ import ContentLoading from '../../components/ContentLoading';
import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails';
import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit';
+import ExecutionEnvironmentTemplateList from './ExecutionEnvironmentTemplate';
function ExecutionEnvironment({ i18n, setBreadcrumb }) {
const { id } = useParams();
@@ -64,6 +65,11 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) {
link: `/execution_environments/${id}/details`,
id: 0,
},
+ {
+ name: i18n._(t`Templates`),
+ link: `/execution_environments/${id}/templates`,
+ id: 1,
+ },
];
if (!isLoading && contentError) {
@@ -114,6 +120,11 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) {
executionEnvironment={executionEnvironment}
/>
+
+
+
>
)}
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx
index a5c89f8d89..ab70f3bbc1 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx
@@ -64,6 +64,11 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
value={description}
dataCy="execution-environment-detail-description"
/>
+
+
{!managedByTower && (
-
-
- {i18n._(t`Delete`)}
-
+ {summary_fields.user_capabilities?.edit && (
+
+ )}
+ {summary_fields.user_capabilities?.delete && (
+
+ {i18n._(t`Delete`)}
+
+ )}
)}
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx
index ce0bf830ed..62d0289b91 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx
@@ -2,7 +2,10 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
-import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
import { ExecutionEnvironmentsAPI } from '../../../api';
import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails';
@@ -22,6 +25,11 @@ const executionEnvironment = {
credential: '/api/v2/credentials/4/',
},
summary_fields: {
+ user_capabilities: {
+ edit: true,
+ delete: true,
+ copy: true,
+ },
credential: {
id: 4,
name: 'Container Registry',
@@ -73,6 +81,9 @@ describe('', () => {
expect(
wrapper.find('Detail[label="Credential"]').prop('value').props.children
).toEqual(executionEnvironment.summary_fields.credential.name);
+ expect(
+ wrapper.find('Detail[label="Managed by Tower"]').prop('value')
+ ).toEqual('False');
const dates = wrapper.find('UserDateDetail');
expect(dates).toHaveLength(2);
expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created);
@@ -167,6 +178,9 @@ describe('', () => {
expect(
wrapper.find('Detail[label="Credential"]').prop('value').props.children
).toEqual(executionEnvironment.summary_fields.credential.name);
+ expect(
+ wrapper.find('Detail[label="Managed by Tower"]').prop('value')
+ ).toEqual('True');
const dates = wrapper.find('UserDateDetail');
expect(dates).toHaveLength(2);
expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created);
@@ -175,6 +189,7 @@ describe('', () => {
expect(wrapper.find('Button[aria-label="Delete"]')).toHaveLength(0);
});
+
test('should have proper number of delete detail requests', async () => {
const history = createMemoryHistory({
initialEntries: ['/execution_environments/42/details'],
@@ -193,4 +208,71 @@ describe('', () => {
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
).toHaveLength(4);
});
+
+ test('should show edit button for users with edit permission', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ const editButton = await waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentDetails Button[aria-label="edit"]'
+ );
+ expect(editButton.text()).toEqual('Edit');
+ expect(editButton.prop('to')).toBe('/execution_environments/17/edit');
+ });
+
+ test('should hide edit button for users without edit permission', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(wrapper, 'ExecutionEnvironmentDetails');
+ expect(
+ wrapper.find('ExecutionEnvironmentDetails Button[aria-label="edit"]')
+ .length
+ ).toBe(0);
+ });
+
+ test('should show delete button for users with delete permission', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ const deleteButton = await waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentDetails Button[aria-label="Delete"]'
+ );
+ expect(deleteButton.text()).toEqual('Delete');
+ });
+
+ test('should hide delete button for users without delete permission', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(wrapper, 'ExecutionEnvironmentDetails');
+ expect(
+ wrapper.find('ExecutionEnvironmentDetails Button[aria-label="Delete"]')
+ .length
+ ).toBe(0);
+ });
});
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx
new file mode 100644
index 0000000000..c2b36eee30
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx
@@ -0,0 +1,139 @@
+import React, { useEffect, useCallback } from 'react';
+import { useLocation } from 'react-router-dom';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Card } from '@patternfly/react-core';
+
+import { ExecutionEnvironmentsAPI } from '../../../api';
+import { getQSConfig, parseQueryString } from '../../../util/qs';
+import useRequest from '../../../util/useRequest';
+import DatalistToolbar from '../../../components/DataListToolbar';
+import PaginatedDataList from '../../../components/PaginatedDataList';
+
+import ExecutionEnvironmentTemplateListItem from './ExecutionEnvironmentTemplateListItem';
+
+const QS_CONFIG = getQSConfig(
+ 'execution_environments',
+ {
+ page: 1,
+ page_size: 20,
+ order_by: 'name',
+ type: 'job_template,workflow_job_template',
+ },
+ ['id', 'page', 'page_size']
+);
+
+function ExecutionEnvironmentTemplateList({ i18n, executionEnvironment }) {
+ const { id } = executionEnvironment;
+ const location = useLocation();
+
+ const {
+ error: contentError,
+ isLoading,
+ request: fetchTemplates,
+ result: {
+ templates,
+ templatesCount,
+ relatedSearchableKeys,
+ searchableKeys,
+ },
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+
+ const [response, responseActions] = await Promise.all([
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplates(id, params),
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions(id),
+ ]);
+
+ return {
+ templates: response.data.results,
+ templatesCount: response.data.count,
+ actions: responseActions.data.actions,
+ relatedSearchableKeys: (
+ responseActions?.data?.related_search_fields || []
+ ).map(val => val.slice(0, -8)),
+ searchableKeys: Object.keys(
+ responseActions.data.actions?.GET || {}
+ ).filter(key => responseActions.data.actions?.GET[key].filterable),
+ };
+ }, [location, id]),
+ {
+ templates: [],
+ templatesCount: 0,
+ actions: {},
+ relatedSearchableKeys: [],
+ searchableKeys: [],
+ }
+ );
+
+ useEffect(() => {
+ fetchTemplates();
+ }, [fetchTemplates]);
+
+ return (
+ <>
+
+ (
+
+ )}
+ renderItem={template => (
+
+ )}
+ />
+
+ >
+ );
+}
+
+export default withI18n()(ExecutionEnvironmentTemplateList);
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx
new file mode 100644
index 0000000000..078d6d249d
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
+
+import { ExecutionEnvironmentsAPI } from '../../../api';
+import ExecutionEnvironmentTemplateList from './ExecutionEnvironmentTemplateList';
+
+jest.mock('../../../api/');
+
+const templates = {
+ data: {
+ count: 3,
+ results: [
+ {
+ id: 1,
+ type: 'job_template',
+ name: 'Foo',
+ url: '/api/v2/job_templates/1/',
+ related: {
+ execution_environment: '/api/v2/execution_environments/1/',
+ },
+ },
+ {
+ id: 2,
+ type: 'workflow_job_template',
+ name: 'Bar',
+ url: '/api/v2/workflow_job_templates/2/',
+ related: {
+ execution_environment: '/api/v2/execution_environments/1/',
+ },
+ },
+ {
+ id: 3,
+ type: 'job_template',
+ name: 'Fuzz',
+ url: '/api/v2/job_templates/3/',
+ related: {
+ execution_environment: '/api/v2/execution_environments/1/',
+ },
+ },
+ ],
+ },
+};
+
+const mockExecutionEnvironment = {
+ id: 1,
+ name: 'Default EE',
+};
+
+const options = { data: { actions: { GET: {} } } };
+
+describe('', () => {
+ let wrapper;
+
+ test('should mount successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentTemplateList',
+ el => el.length > 0
+ );
+ });
+
+ test('should have data fetched and render 3 rows', async () => {
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplates.mockResolvedValue(
+ templates
+ );
+
+ ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions.mockResolvedValue(
+ options
+ );
+
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ await waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentTemplateList',
+ el => el.length > 0
+ );
+
+ expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(3);
+ expect(ExecutionEnvironmentsAPI.readUnifiedJobTemplates).toBeCalled();
+ expect(ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions).toBeCalled();
+ });
+
+ test('should not render add button', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ waitForElement(
+ wrapper,
+ 'ExecutionEnvironmentTemplateList',
+ el => el.length > 0
+ );
+ expect(wrapper.find('ToolbarAddButton').length).toBe(0);
+ });
+});
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx
new file mode 100644
index 0000000000..4a33126386
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import { Link } from 'react-router-dom';
+import {
+ DataListItem,
+ DataListItemRow,
+ DataListItemCells,
+} from '@patternfly/react-core';
+
+import DataListCell from '../../../components/DataListCell';
+
+function ExecutionEnvironmentTemplateListItem({ template, detailUrl, i18n }) {
+ return (
+
+
+
+
+ {template.name}
+
+ ,
+
+ {template.type === 'job_template'
+ ? i18n._(t`Job Template`)
+ : i18n._(t`Workflow Job Template`)}
+ ,
+ ]}
+ />
+
+
+ );
+}
+
+export default withI18n()(ExecutionEnvironmentTemplateListItem);
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx
new file mode 100644
index 0000000000..9c107ab19b
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+
+import ExecutionEnvironmentTemplateListItem from './ExecutionEnvironmentTemplateListItem';
+
+describe('', () => {
+ let wrapper;
+ const template = {
+ id: 1,
+ name: 'Foo',
+ type: 'job_template',
+ };
+
+ test('should mount successfully', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(1);
+ expect(wrapper.find('DataListCell[aria-label="Name"]').text()).toBe(
+ template.name
+ );
+ expect(
+ wrapper.find('DataListCell[aria-label="Template type"]').text()
+ ).toBe('Job Template');
+ });
+
+ test('should distinguish template types', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(1);
+ expect(
+ wrapper.find('DataListCell[aria-label="Template type"]').text()
+ ).toBe('Workflow Job Template');
+ });
+});
diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js
new file mode 100644
index 0000000000..3bdea254ee
--- /dev/null
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js
@@ -0,0 +1 @@
+export { default } from './ExecutionEnvironmentTemplateList';
diff --git a/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx b/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx
index f33c989f5b..08b447a991 100644
--- a/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx
+++ b/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx
@@ -36,7 +36,7 @@ function HostFacts({ i18n, host }) {
return (
-
+
);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx
index a4c584c121..dc7e47b646 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx
@@ -72,11 +72,12 @@ describe('', () => {
});
test('should open delete modal and then call api to delete the group', async () => {
+ expect(wrapper.find('Modal').length).toBe(1); // variables modal already mounted
await act(async () => {
wrapper.find('button[aria-label="Delete"]').simulate('click');
});
- await waitForElement(wrapper, 'Modal', el => el.length === 1);
- expect(wrapper.find('Modal').length).toBe(1);
+ wrapper.update();
+ expect(wrapper.find('Modal').length).toBe(2);
await act(async () => {
wrapper.find('Radio[id="radio-delete"]').invoke('onChange')();
});
diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx
index 6bffd37ba4..4d93ce58c3 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx
@@ -35,7 +35,7 @@ function InventoryHostFacts({ i18n, host }) {
return (
-
+
);
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx
index a16949776a..e874157d99 100644
--- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx
@@ -122,20 +122,14 @@ const SCMSubForm = ({ autoPopulateProject, i18n }) => {
onSelect={(event, value) => {
setIsOpen(false);
value = value.trim();
- if (!value.endsWith('/')) {
- value += '/';
- }
sourcePathHelpers.setValue(value);
}}
aria-label={i18n._(t`Select source path`)}
placeholder={i18n._(t`Select source path`)}
+ createText={i18n._(t`Set source path to`)}
isCreatable
onCreateOption={value => {
value.trim();
-
- if (!value.endsWith('/')) {
- value += '/';
- }
setSourcePath([...sourcePath, value]);
}}
>
diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx
index 0d3c47f451..715d0df7cd 100644
--- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx
@@ -98,7 +98,7 @@ describe('', () => {
});
wrapper.update();
expect(wrapper.find('Select#source_path').prop('selections')).toEqual(
- 'bar/'
+ 'bar'
);
await act(async () => {
@@ -138,7 +138,7 @@ describe('', () => {
customWrapper.find('Select').invoke('onSelect')({}, 'newPath');
});
customWrapper.update();
- expect(customWrapper.find('Select').prop('selections')).toBe('newPath/');
+ expect(customWrapper.find('Select').prop('selections')).toBe('newPath');
});
test('Update on project update should be disabled', async () => {
const customInitialValues = {
diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
index 9f2c4ae817..d567c24097 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx
@@ -38,7 +38,7 @@ function OrganizationExecEnvList({ i18n, organization }) {
const [response, responseActions] = await Promise.all([
OrganizationsAPI.readExecutionEnvironments(id, params),
- OrganizationsAPI.readExecutionEnvironmentsOptions(id, params),
+ OrganizationsAPI.readExecutionEnvironmentsOptions(id),
]);
return {
diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx
index 02f37b2d96..54eac90e9f 100644
--- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx
+++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx
@@ -9,7 +9,7 @@ import ContentError from '../../../../components/ContentError';
import ContentLoading from '../../../../components/ContentLoading';
import { DetailList } from '../../../../components/DetailList';
import RoutedTabs from '../../../../components/RoutedTabs';
-import { SettingsAPI } from '../../../../api';
+import { SettingsAPI, ExecutionEnvironmentsAPI } from '../../../../api';
import useRequest from '../../../../util/useRequest';
import { useConfig } from '../../../../contexts/Config';
import { useSettings } from '../../../../contexts/Settings';
@@ -23,7 +23,15 @@ function MiscSystemDetail({ i18n }) {
const { isLoading, error, request, result: system } = useRequest(
useCallback(async () => {
const { data } = await SettingsAPI.readCategory('all');
-
+ let DEFAULT_EXECUTION_ENVIRONMENT = '';
+ if (data.DEFAULT_EXECUTION_ENVIRONMENT) {
+ const {
+ data: { name },
+ } = await ExecutionEnvironmentsAPI.readDetail(
+ data.DEFAULT_EXECUTION_ENVIRONMENT
+ );
+ DEFAULT_EXECUTION_ENVIRONMENT = name;
+ }
const {
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
@@ -49,19 +57,17 @@ function MiscSystemDetail({ i18n }) {
'SESSION_COOKIE_AGE',
'TOWER_URL_BASE'
);
-
const systemData = {
...pluckedSystemData,
ACCESS_TOKEN_EXPIRE_SECONDS,
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
+ DEFAULT_EXECUTION_ENVIRONMENT,
};
-
const {
OAUTH2_PROVIDER: OAUTH2_PROVIDER_OPTIONS,
...options
} = allOptions;
-
const systemOptions = {
...options,
ACCESS_TOKEN_EXPIRE_SECONDS: {
@@ -80,7 +86,6 @@ function MiscSystemDetail({ i18n }) {
label: i18n._(t`Authorization Code Expiration`),
},
};
-
const mergedData = {};
Object.keys(systemData).forEach(key => {
mergedData[key] = systemOptions[key];
diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx
index aa8b2e334d..6fbebb6ab8 100644
--- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx
+++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx
@@ -5,7 +5,7 @@ import {
waitForElement,
} from '../../../../../testUtils/enzymeHelpers';
import { SettingsProvider } from '../../../../contexts/Settings';
-import { SettingsAPI } from '../../../../api';
+import { SettingsAPI, ExecutionEnvironmentsAPI } from '../../../../api';
import {
assertDetail,
assertVariableDetail,
@@ -14,13 +14,14 @@ import mockAllOptions from '../../shared/data.allSettingOptions.json';
import MiscSystemDetail from './MiscSystemDetail';
jest.mock('../../../../api/models/Settings');
+jest.mock('../../../../api/models/ExecutionEnvironments');
+
SettingsAPI.readCategory.mockResolvedValue({
data: {
ALLOW_OAUTH2_FOR_EXTERNAL_USERS: false,
AUTH_BASIC_ENABLED: true,
AUTOMATION_ANALYTICS_GATHER_INTERVAL: 14400,
AUTOMATION_ANALYTICS_URL: 'https://example.com',
- CUSTOM_VENV_PATHS: [],
INSIGHTS_TRACKING_STATE: false,
LOGIN_REDIRECT_OVERRIDE: 'https://redirect.com',
MANAGE_ORGANIZATION_AUTH: true,
@@ -36,6 +37,16 @@ SettingsAPI.readCategory.mockResolvedValue({
SESSIONS_PER_USER: -1,
SESSION_COOKIE_AGE: 30000000000,
TOWER_URL_BASE: 'https://towerhost',
+ DEFAULT_EXECUTION_ENVIRONMENT: 1,
+ },
+});
+
+ExecutionEnvironmentsAPI.readDetail.mockResolvedValue({
+ data: {
+ id: 1,
+ name: 'Foo',
+ image: 'quay.io/ansible/awx-ee',
+ pull: 'missing',
},
});
@@ -110,6 +121,33 @@ describe('', () => {
assertDetail(wrapper, 'Red Hat customer username', 'mock name');
assertDetail(wrapper, 'Refresh Token Expiration', '3 seconds');
assertVariableDetail(wrapper, 'Remote Host Headers', '[]');
+ assertDetail(wrapper, 'Global default execution environment', 'Foo');
+ });
+
+ test('should render execution environment as not configured', async () => {
+ ExecutionEnvironmentsAPI.readDetail.mockResolvedValue({
+ data: {},
+ });
+ let newWrapper;
+ await act(async () => {
+ newWrapper = mountWithContexts(
+
+
+
+ );
+ });
+ await waitForElement(newWrapper, 'ContentLoading', el => el.length === 0);
+
+ assertDetail(
+ newWrapper,
+ 'Global default execution environment',
+ 'Not configured'
+ );
});
test('should hide edit button from non-superusers', async () => {
diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx
index bb19b52f21..5411326eb0 100644
--- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx
+++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx
@@ -9,6 +9,7 @@ import ContentError from '../../../../components/ContentError';
import ContentLoading from '../../../../components/ContentLoading';
import { FormSubmitError } from '../../../../components/FormField';
import { FormColumnLayout } from '../../../../components/FormLayout';
+import { ExecutionEnvironmentLookup } from '../../../../components/Lookup';
import { useSettings } from '../../../../contexts/Settings';
import {
BooleanField,
@@ -20,7 +21,7 @@ import {
} from '../../shared';
import useModal from '../../../../util/useModal';
import useRequest from '../../../../util/useRequest';
-import { SettingsAPI } from '../../../../api';
+import { SettingsAPI, ExecutionEnvironmentsAPI } from '../../../../api';
import { pluck, formatJson } from '../../shared/settingUtils';
function MiscSystemEdit({ i18n }) {
@@ -44,7 +45,6 @@ function MiscSystemEdit({ i18n }) {
'AUTH_BASIC_ENABLED',
'AUTOMATION_ANALYTICS_GATHER_INTERVAL',
'AUTOMATION_ANALYTICS_URL',
- 'CUSTOM_VENV_PATHS',
'INSIGHTS_TRACKING_STATE',
'LOGIN_REDIRECT_OVERRIDE',
'MANAGE_ORGANIZATION_AUTH',
@@ -55,7 +55,8 @@ function MiscSystemEdit({ i18n }) {
'REMOTE_HOST_HEADERS',
'SESSIONS_PER_USER',
'SESSION_COOKIE_AGE',
- 'TOWER_URL_BASE'
+ 'TOWER_URL_BASE',
+ 'DEFAULT_EXECUTION_ENVIRONMENT'
);
const systemData = {
@@ -128,6 +129,7 @@ function MiscSystemEdit({ i18n }) {
AUTHORIZATION_CODE_EXPIRE_SECONDS,
...formData
} = form;
+
await submitForm({
...formData,
REMOTE_HOST_HEADERS: formatJson(formData.REMOTE_HOST_HEADERS),
@@ -136,6 +138,8 @@ function MiscSystemEdit({ i18n }) {
REFRESH_TOKEN_EXPIRE_SECONDS,
AUTHORIZATION_CODE_EXPIRE_SECONDS,
},
+ DEFAULT_EXECUTION_ENVIRONMENT:
+ formData.DEFAULT_EXECUTION_ENVIRONMENT?.id || null,
});
};
@@ -178,16 +182,73 @@ function MiscSystemEdit({ i18n }) {
return acc;
}, {});
+ const executionEnvironmentId =
+ system?.DEFAULT_EXECUTION_ENVIRONMENT?.value || null;
+
+ const {
+ isLoading: isLoadingExecutionEnvironment,
+ error: errorExecutionEnvironment,
+ request: fetchExecutionEnvironment,
+ result: executionEnvironment,
+ } = useRequest(
+ useCallback(async () => {
+ if (!executionEnvironmentId) {
+ return '';
+ }
+ const { data } = await ExecutionEnvironmentsAPI.readDetail(
+ executionEnvironmentId
+ );
+ return data;
+ }, [executionEnvironmentId])
+ );
+
+ useEffect(() => {
+ fetchExecutionEnvironment();
+ }, [fetchExecutionEnvironment]);
+
return (
- {isLoading && }
- {!isLoading && error && }
- {!isLoading && system && (
-
+ {(isLoading || isLoadingExecutionEnvironment) && }
+ {!(isLoading || isLoadingExecutionEnvironment) && error && (
+
+ )}
+ {!(isLoading || isLoadingExecutionEnvironment) && system && (
+
{formik => {
return (