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] + ); }); });