diff --git a/awx/ui_next/src/components/Chip/Chip.test.jsx b/awx/ui_next/src/components/Chip/Chip.test.jsx
index 0d4850da6d..8e38d71453 100644
--- a/awx/ui_next/src/components/Chip/Chip.test.jsx
+++ b/awx/ui_next/src/components/Chip/Chip.test.jsx
@@ -1,6 +1,5 @@
import React from 'react';
import { mount } from 'enzyme';
-
import Chip from './Chip';
describe('Chip', () => {
diff --git a/awx/ui_next/src/components/Chip/CredentialChip.jsx b/awx/ui_next/src/components/Chip/CredentialChip.jsx
new file mode 100644
index 0000000000..f10022f37c
--- /dev/null
+++ b/awx/ui_next/src/components/Chip/CredentialChip.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { shape, string, bool } from 'prop-types';
+import { toTitleCase } from '@util/strings';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import Chip from './Chip';
+
+function CredentialChip ({ credential, i18n, ...props }) {
+ let type;
+ if (credential.cloud) {
+ type = i18n._(t`Cloud`);
+ } else if (credential.kind === 'aws' || credential.kind === 'ssh') {
+ type = credential.kind.toUpperCase();
+ } else {
+ type = toTitleCase(credential.kind);
+ }
+
+ return (
+
+ {type}:
+ {credential.name}
+
+ )
+}
+CredentialChip.propTypes = {
+ credential: shape({
+ cloud: bool,
+ kind: string,
+ name: string.isRequired,
+ }).isRequired,
+ i18n: shape({}).isRequired,
+};
+
+export { CredentialChip as _CredentialChip };
+export default withI18n()(CredentialChip);
diff --git a/awx/ui_next/src/components/Chip/CredentialChip.test.jsx b/awx/ui_next/src/components/Chip/CredentialChip.test.jsx
new file mode 100644
index 0000000000..192208a0da
--- /dev/null
+++ b/awx/ui_next/src/components/Chip/CredentialChip.test.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import CredentialChip from './CredentialChip';
+
+describe('CredentialChip', () => {
+ test('should render SSH kind', () => {
+ const credential = {
+ kind: 'ssh',
+ name: 'foo',
+ };
+
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('CredentialChip').text()).toEqual('SSH: foo');
+ });
+
+ test('should render AWS kind', () => {
+ const credential = {
+ kind: 'aws',
+ name: 'foo',
+ };
+
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('CredentialChip').text()).toEqual('AWS: foo');
+ });
+
+ test('should render with "Cloud"', () => {
+ const credential = {
+ cloud: true,
+ kind: 'other',
+ name: 'foo',
+ };
+
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('CredentialChip').text()).toEqual('Cloud: foo');
+ });
+
+ test('should render with other kind', () => {
+ const credential = {
+ kind: 'other',
+ name: 'foo',
+ };
+
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find('CredentialChip').text()).toEqual('Other: foo');
+ });
+});
diff --git a/awx/ui_next/src/components/Chip/index.js b/awx/ui_next/src/components/Chip/index.js
index 96e20db252..56324606bf 100644
--- a/awx/ui_next/src/components/Chip/index.js
+++ b/awx/ui_next/src/components/Chip/index.js
@@ -1,2 +1,3 @@
export { default as ChipGroup } from './ChipGroup';
export { default as Chip } from './Chip';
+export { default as CredentialChip } from './CredentialChip';
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
index efd43f643e..bcfee592a3 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
@@ -9,20 +9,27 @@ import CodeMirrorInput from './CodeMirrorInput';
const YAML_MODE = 'yaml';
const JSON_MODE = 'javascript';
+function formatJson(jsonString) {
+ return JSON.stringify(JSON.parse(jsonString), null, 2);
+}
+
const SmallButton = styled(Button)`
padding: 3px 8px;
font-size: var(--pf-global--FontSize--xs);
`;
-function VariablesInput (props) {
+function VariablesInput(props) {
const { id, label, readOnly, rows, error, onError, className } = props;
- // eslint-disable-next-line react/destructuring-assignment
- const [value, setValue] = useState(props.value);
+ /* eslint-disable react/destructuring-assignment */
+ const defaultValue = isJson(props.value)
+ ? formatJson(props.value)
+ : props.value;
+ const [value, setValue] = useState(defaultValue);
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
- // eslint-disable-next-line react/destructuring-assignment
const isControlled = !!props.onChange;
+ /* eslint-enable react/destructuring-assignment */
- const onChange = (newValue) => {
+ const onChange = newValue => {
if (isControlled) {
props.onChange(newValue);
}
@@ -33,13 +40,17 @@ function VariablesInput (props) {
-
+
{
- if (mode === YAML_MODE) { return; }
+ if (mode === YAML_MODE) {
+ return;
+ }
try {
onChange(jsonToYaml(value));
setMode(YAML_MODE);
@@ -53,7 +64,9 @@ function VariablesInput (props) {
{
- if (mode === JSON_MODE) { return; }
+ if (mode === JSON_MODE) {
+ return;
+ }
try {
onChange(yamlToJson(value));
setMode(JSON_MODE);
@@ -77,13 +90,10 @@ function VariablesInput (props) {
hasErrors={!!error}
/>
{error ? (
-
+
{error}
- ) : null }
+ ) : null}
);
}
diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
index 904150dada..c5571d02b3 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
@@ -6,7 +6,7 @@ import { t } from '@lingui/macro';
import { CardBody, Button } from '@patternfly/react-core';
import styled from 'styled-components';
import { DetailList, Detail } from '@components/DetailList';
-import { ChipGroup, Chip } from '@components/Chip';
+import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { VariablesInput } from '@components/CodeMirrorInput';
import { toTitleCase } from '@util/strings';
@@ -23,7 +23,7 @@ const VERBOSITY = {
4: '4 (Connection Debug)',
};
-function JobDetail ({ job, i18n }) {
+function JobDetail({ job, i18n }) {
const {
job_template: jobTemplate,
project,
@@ -37,121 +37,84 @@ function JobDetail ({ job, i18n }) {
{/* TODO: add status icon? */}
-
-
-
+
+
+
{jobTemplate && (
{jobTemplate.name}
- )}
+ }
/>
)}
-
+
{inventory && (
- {inventory.name}
-
- )}
+ value={
+ {inventory.name}
+ }
/>
)}
{/* TODO: show project status icon */}
{project && (
- {project.name}
-
- )}
+ value={{project.name}}
/>
)}
-
-
-
-
-
-
+
+
+
+
+
+
{instanceGroup && (
{instanceGroup.name}
- )}
+ }
/>
)}
- {
- typeof job.job_slice_number === 'number'
- && typeof job.job_slice_count === 'number'
- && (
+ {typeof job.job_slice_number === 'number' &&
+ typeof job.job_slice_count === 'number' && (
- )
- }
- {(credentials && credentials.length > 0) && (
+ )}
+ {credentials && credentials.length > 0 && (
{credentials.map(c => (
- {c.name}
+
))}
- )}
+ }
/>
)}
- {(labels && labels.count > 0) && (
+ {labels && labels.count > 0 && (
{labels.results.map(l => (
- {l.name}
+
+ {l.name}
+
))}
- )}
+ }
/>
)}
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
index 119df2776e..3e2c951c64 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
@@ -15,10 +15,9 @@ import { t } from '@lingui/macro';
import ContentError from '@components/ContentError';
import LaunchButton from '@components/LaunchButton';
import ContentLoading from '@components/ContentLoading';
-import { ChipGroup, Chip } from '@components/Chip';
+import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { DetailList, Detail } from '@components/DetailList';
import { JobTemplatesAPI } from '@api';
-import { toTitleCase } from '@util/strings';
const ButtonGroup = styled.div`
display: flex;
@@ -104,9 +103,6 @@ class JobTemplateDetail extends Component {
const generateCallBackUrl = `${window.location.origin + url}callback/`;
const isInitialized = !hasTemplateLoading && !hasContentLoading;
- const credentialType = c =>
- c === 'aws' || c === 'ssh' ? c.toUpperCase() : toTitleCase(c);
-
const renderOptionsField =
become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
@@ -206,13 +202,7 @@ class JobTemplateDetail extends Component {
value={
{summary_fields.credentials.map(c => (
-
-
- {c.kind ? credentialType(c.kind) : i18n._(t`Cloud`)}
- :
-
- {` ${c.name}`}
-
+
))}
}
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx
index 46dd3a5aef..dc945229b7 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx
@@ -113,19 +113,21 @@ describe('', () => {
done();
});
- test('Credential type is Cloud if credential.kind is null', async done => {
+ test('should render CredentialChip', () => {
template.summary_fields.credentials = [{ id: 1, name: 'cred', kind: null }];
const wrapper = mountWithContexts(
);
- const jobTemplateDetail = wrapper.find('JobTemplateDetail');
- jobTemplateDetail.setState({
- instanceGroups: mockInstanceGroups.data.results,
+ wrapper.find('JobTemplateDetail').setState({
+ instanceGroups: mockInstanceGroups,
hasContentLoading: false,
contentError: false,
});
- const cred = wrapper.find('strong.credential');
- expect(cred.text()).toContain('Cloud:');
- done();
+
+ const chip = wrapper.find('CredentialChip');
+ expect(chip).toHaveLength(1);
+ expect(chip.prop('credential')).toEqual(
+ template.summary_fields.credentials[0]
+ );
});
});