mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 01:17:37 -02:30
add CredentialChip component
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import Chip from './Chip';
|
import Chip from './Chip';
|
||||||
|
|
||||||
describe('Chip', () => {
|
describe('Chip', () => {
|
||||||
|
|||||||
35
awx/ui_next/src/components/Chip/CredentialChip.jsx
Normal file
35
awx/ui_next/src/components/Chip/CredentialChip.jsx
Normal file
@@ -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 (
|
||||||
|
<Chip {...props}>
|
||||||
|
<strong>{type}: </strong>
|
||||||
|
{credential.name}
|
||||||
|
</Chip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CredentialChip.propTypes = {
|
||||||
|
credential: shape({
|
||||||
|
cloud: bool,
|
||||||
|
kind: string,
|
||||||
|
name: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
i18n: shape({}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CredentialChip as _CredentialChip };
|
||||||
|
export default withI18n()(CredentialChip);
|
||||||
54
awx/ui_next/src/components/Chip/CredentialChip.test.jsx
Normal file
54
awx/ui_next/src/components/Chip/CredentialChip.test.jsx
Normal file
@@ -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(
|
||||||
|
<CredentialChip credential={credential} />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('CredentialChip').text()).toEqual('SSH: foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render AWS kind', () => {
|
||||||
|
const credential = {
|
||||||
|
kind: 'aws',
|
||||||
|
name: 'foo',
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<CredentialChip credential={credential} />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('CredentialChip').text()).toEqual('AWS: foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render with "Cloud"', () => {
|
||||||
|
const credential = {
|
||||||
|
cloud: true,
|
||||||
|
kind: 'other',
|
||||||
|
name: 'foo',
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<CredentialChip credential={credential} />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('CredentialChip').text()).toEqual('Cloud: foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render with other kind', () => {
|
||||||
|
const credential = {
|
||||||
|
kind: 'other',
|
||||||
|
name: 'foo',
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<CredentialChip credential={credential} />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('CredentialChip').text()).toEqual('Other: foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default as ChipGroup } from './ChipGroup';
|
export { default as ChipGroup } from './ChipGroup';
|
||||||
export { default as Chip } from './Chip';
|
export { default as Chip } from './Chip';
|
||||||
|
export { default as CredentialChip } from './CredentialChip';
|
||||||
|
|||||||
@@ -9,20 +9,27 @@ import CodeMirrorInput from './CodeMirrorInput';
|
|||||||
const YAML_MODE = 'yaml';
|
const YAML_MODE = 'yaml';
|
||||||
const JSON_MODE = 'javascript';
|
const JSON_MODE = 'javascript';
|
||||||
|
|
||||||
|
function formatJson(jsonString) {
|
||||||
|
return JSON.stringify(JSON.parse(jsonString), null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
const SmallButton = styled(Button)`
|
const SmallButton = styled(Button)`
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
font-size: var(--pf-global--FontSize--xs);
|
font-size: var(--pf-global--FontSize--xs);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VariablesInput (props) {
|
function VariablesInput(props) {
|
||||||
const { id, label, readOnly, rows, error, onError, className } = props;
|
const { id, label, readOnly, rows, error, onError, className } = props;
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
/* eslint-disable react/destructuring-assignment */
|
||||||
const [value, setValue] = useState(props.value);
|
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);
|
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
|
||||||
const isControlled = !!props.onChange;
|
const isControlled = !!props.onChange;
|
||||||
|
/* eslint-enable react/destructuring-assignment */
|
||||||
|
|
||||||
const onChange = (newValue) => {
|
const onChange = newValue => {
|
||||||
if (isControlled) {
|
if (isControlled) {
|
||||||
props.onChange(newValue);
|
props.onChange(newValue);
|
||||||
}
|
}
|
||||||
@@ -33,13 +40,17 @@ function VariablesInput (props) {
|
|||||||
<div className={`pf-c-form__group ${className || ''}`}>
|
<div className={`pf-c-form__group ${className || ''}`}>
|
||||||
<Split gutter="sm">
|
<Split gutter="sm">
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<label htmlFor={id} className="pf-c-form__label">{label}</label>
|
<label htmlFor={id} className="pf-c-form__label">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
</SplitItem>
|
</SplitItem>
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<SmallButton
|
<SmallButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (mode === YAML_MODE) { return; }
|
if (mode === YAML_MODE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
onChange(jsonToYaml(value));
|
onChange(jsonToYaml(value));
|
||||||
setMode(YAML_MODE);
|
setMode(YAML_MODE);
|
||||||
@@ -53,7 +64,9 @@ function VariablesInput (props) {
|
|||||||
</SmallButton>
|
</SmallButton>
|
||||||
<SmallButton
|
<SmallButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (mode === JSON_MODE) { return; }
|
if (mode === JSON_MODE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
onChange(yamlToJson(value));
|
onChange(yamlToJson(value));
|
||||||
setMode(JSON_MODE);
|
setMode(JSON_MODE);
|
||||||
@@ -77,13 +90,10 @@ function VariablesInput (props) {
|
|||||||
hasErrors={!!error}
|
hasErrors={!!error}
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<div
|
<div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||||
className="pf-c-form__helper-text pf-m-error"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : null }
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { CardBody, Button } from '@patternfly/react-core';
|
import { CardBody, Button } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
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 { VariablesInput } from '@components/CodeMirrorInput';
|
||||||
import { toTitleCase } from '@util/strings';
|
import { toTitleCase } from '@util/strings';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const VERBOSITY = {
|
|||||||
4: '4 (Connection Debug)',
|
4: '4 (Connection Debug)',
|
||||||
};
|
};
|
||||||
|
|
||||||
function JobDetail ({ job, i18n }) {
|
function JobDetail({ job, i18n }) {
|
||||||
const {
|
const {
|
||||||
job_template: jobTemplate,
|
job_template: jobTemplate,
|
||||||
project,
|
project,
|
||||||
@@ -37,121 +37,84 @@ function JobDetail ({ job, i18n }) {
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList>
|
<DetailList>
|
||||||
{/* TODO: add status icon? */}
|
{/* TODO: add status icon? */}
|
||||||
<Detail
|
<Detail label={i18n._(t`Status`)} value={toTitleCase(job.status)} />
|
||||||
label={i18n._(t`Status`)}
|
<Detail label={i18n._(t`Started`)} value={job.started} />
|
||||||
value={toTitleCase(job.status)}
|
<Detail label={i18n._(t`Finished`)} value={job.finished} />
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Started`)}
|
|
||||||
value={job.started}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Finished`)}
|
|
||||||
value={job.finished}
|
|
||||||
/>
|
|
||||||
{jobTemplate && (
|
{jobTemplate && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Template`)}
|
label={i18n._(t`Template`)}
|
||||||
value={(
|
value={
|
||||||
<Link to={`/templates/job_template/${jobTemplate.id}`}>
|
<Link to={`/templates/job_template/${jobTemplate.id}`}>
|
||||||
{jobTemplate.name}
|
{jobTemplate.name}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Detail
|
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(job.job_type)} />
|
||||||
label={i18n._(t`Job Type`)}
|
|
||||||
value={toTitleCase(job.job_type)}
|
|
||||||
/>
|
|
||||||
{inventory && (
|
{inventory && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Inventory`)}
|
label={i18n._(t`Inventory`)}
|
||||||
value={(
|
value={
|
||||||
<Link to={`/inventory/${inventory.id}`}>
|
<Link to={`/inventory/${inventory.id}`}>{inventory.name}</Link>
|
||||||
{inventory.name}
|
}
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* TODO: show project status icon */}
|
{/* TODO: show project status icon */}
|
||||||
{project && (
|
{project && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Project`)}
|
label={i18n._(t`Project`)}
|
||||||
value={(
|
value={<Link to={`/projects/${project.id}`}>{project.name}</Link>}
|
||||||
<Link to={`/projects/${project.id}`}>
|
|
||||||
{project.name}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Detail
|
<Detail label={i18n._(t`Revision`)} value={job.scm_revision} />
|
||||||
label={i18n._(t`Revision`)}
|
<Detail label={i18n._(t`Playbook`)} value={null} />
|
||||||
value={job.scm_revision}
|
<Detail label={i18n._(t`Limit`)} value={job.limit} />
|
||||||
/>
|
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[job.verbosity]} />
|
||||||
<Detail
|
<Detail label={i18n._(t`Environment`)} value={null} />
|
||||||
label={i18n._(t`Playbook`)}
|
<Detail label={i18n._(t`Execution Node`)} value={job.exucution_node} />
|
||||||
value={null}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Limit`)}
|
|
||||||
value={job.limit}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Verbosity`)}
|
|
||||||
value={VERBOSITY[job.verbosity]}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Environment`)}
|
|
||||||
value={null}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Execution Node`)}
|
|
||||||
value={job.exucution_node}
|
|
||||||
/>
|
|
||||||
{instanceGroup && (
|
{instanceGroup && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Instance Group`)}
|
label={i18n._(t`Instance Group`)}
|
||||||
value={(
|
value={
|
||||||
<Link to={`/instance_groups/${instanceGroup.id}`}>
|
<Link to={`/instance_groups/${instanceGroup.id}`}>
|
||||||
{instanceGroup.name}
|
{instanceGroup.name}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{
|
{typeof job.job_slice_number === 'number' &&
|
||||||
typeof job.job_slice_number === 'number'
|
typeof job.job_slice_count === 'number' && (
|
||||||
&& typeof job.job_slice_count === 'number'
|
|
||||||
&& (
|
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Job Slice`)}
|
label={i18n._(t`Job Slice`)}
|
||||||
value={`${job.job_slice_number}/${job.job_slice_count}`}
|
value={`${job.job_slice_number}/${job.job_slice_count}`}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}
|
{credentials && credentials.length > 0 && (
|
||||||
{(credentials && credentials.length > 0) && (
|
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={i18n._(t`Credentials`)}
|
label={i18n._(t`Credentials`)}
|
||||||
value={(
|
value={
|
||||||
<ChipGroup showOverflowAfter={5}>
|
<ChipGroup showOverflowAfter={5}>
|
||||||
{credentials.map(c => (
|
{credentials.map(c => (
|
||||||
<Chip key={c.id} isReadOnly>{c.name}</Chip>
|
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||||
))}
|
))}
|
||||||
</ChipGroup>
|
</ChipGroup>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(labels && labels.count > 0) && (
|
{labels && labels.count > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={i18n._(t`Credentials`)}
|
label={i18n._(t`Credentials`)}
|
||||||
value={(
|
value={
|
||||||
<ChipGroup showOverflowAfter={5}>
|
<ChipGroup showOverflowAfter={5}>
|
||||||
{labels.results.map(l => (
|
{labels.results.map(l => (
|
||||||
<Chip key={l.id} isReadOnly>{l.name}</Chip>
|
<Chip key={l.id} isReadOnly>
|
||||||
|
{l.name}
|
||||||
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</ChipGroup>
|
</ChipGroup>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DetailList>
|
</DetailList>
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import { t } from '@lingui/macro';
|
|||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import LaunchButton from '@components/LaunchButton';
|
import LaunchButton from '@components/LaunchButton';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
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 { DetailList, Detail } from '@components/DetailList';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI } from '@api';
|
||||||
import { toTitleCase } from '@util/strings';
|
|
||||||
|
|
||||||
const ButtonGroup = styled.div`
|
const ButtonGroup = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -104,9 +103,6 @@ class JobTemplateDetail extends Component {
|
|||||||
const generateCallBackUrl = `${window.location.origin + url}callback/`;
|
const generateCallBackUrl = `${window.location.origin + url}callback/`;
|
||||||
const isInitialized = !hasTemplateLoading && !hasContentLoading;
|
const isInitialized = !hasTemplateLoading && !hasContentLoading;
|
||||||
|
|
||||||
const credentialType = c =>
|
|
||||||
c === 'aws' || c === 'ssh' ? c.toUpperCase() : toTitleCase(c);
|
|
||||||
|
|
||||||
const renderOptionsField =
|
const renderOptionsField =
|
||||||
become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
|
become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
|
||||||
|
|
||||||
@@ -206,13 +202,7 @@ class JobTemplateDetail extends Component {
|
|||||||
value={
|
value={
|
||||||
<ChipGroup showOverflowAfter={5}>
|
<ChipGroup showOverflowAfter={5}>
|
||||||
{summary_fields.credentials.map(c => (
|
{summary_fields.credentials.map(c => (
|
||||||
<Chip key={c.id} isReadOnly>
|
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||||
<strong className="credential">
|
|
||||||
{c.kind ? credentialType(c.kind) : i18n._(t`Cloud`)}
|
|
||||||
:
|
|
||||||
</strong>
|
|
||||||
{` ${c.name}`}
|
|
||||||
</Chip>
|
|
||||||
))}
|
))}
|
||||||
</ChipGroup>
|
</ChipGroup>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,19 +113,21 @@ describe('<JobTemplateDetail />', () => {
|
|||||||
done();
|
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 }];
|
template.summary_fields.credentials = [{ id: 1, name: 'cred', kind: null }];
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateDetail template={template} />
|
<JobTemplateDetail template={template} />
|
||||||
);
|
);
|
||||||
const jobTemplateDetail = wrapper.find('JobTemplateDetail');
|
wrapper.find('JobTemplateDetail').setState({
|
||||||
jobTemplateDetail.setState({
|
instanceGroups: mockInstanceGroups,
|
||||||
instanceGroups: mockInstanceGroups.data.results,
|
|
||||||
hasContentLoading: false,
|
hasContentLoading: false,
|
||||||
contentError: false,
|
contentError: false,
|
||||||
});
|
});
|
||||||
const cred = wrapper.find('strong.credential');
|
|
||||||
expect(cred.text()).toContain('Cloud:');
|
const chip = wrapper.find('CredentialChip');
|
||||||
done();
|
expect(chip).toHaveLength(1);
|
||||||
|
expect(chip.prop('credential')).toEqual(
|
||||||
|
template.summary_fields.credentials[0]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user