mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
add CredentialChip component
This commit is contained in:
parent
eee1601528
commit
40f9b0dc7f
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import Chip from './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 Chip } from './Chip';
|
||||
export { default as CredentialChip } from './CredentialChip';
|
||||
|
||||
@ -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) {
|
||||
<div className={`pf-c-form__group ${className || ''}`}>
|
||||
<Split gutter="sm">
|
||||
<SplitItem>
|
||||
<label htmlFor={id} className="pf-c-form__label">{label}</label>
|
||||
<label htmlFor={id} className="pf-c-form__label">
|
||||
{label}
|
||||
</label>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<ButtonGroup>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
if (mode === YAML_MODE) { return; }
|
||||
if (mode === YAML_MODE) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
onChange(jsonToYaml(value));
|
||||
setMode(YAML_MODE);
|
||||
@ -53,7 +64,9 @@ function VariablesInput (props) {
|
||||
</SmallButton>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
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 ? (
|
||||
<div
|
||||
className="pf-c-form__helper-text pf-m-error"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||
{error}
|
||||
</div>
|
||||
) : null }
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 }) {
|
||||
<CardBody>
|
||||
<DetailList>
|
||||
{/* TODO: add status icon? */}
|
||||
<Detail
|
||||
label={i18n._(t`Status`)}
|
||||
value={toTitleCase(job.status)}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Started`)}
|
||||
value={job.started}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Finished`)}
|
||||
value={job.finished}
|
||||
/>
|
||||
<Detail label={i18n._(t`Status`)} value={toTitleCase(job.status)} />
|
||||
<Detail label={i18n._(t`Started`)} value={job.started} />
|
||||
<Detail label={i18n._(t`Finished`)} value={job.finished} />
|
||||
{jobTemplate && (
|
||||
<Detail
|
||||
label={i18n._(t`Template`)}
|
||||
value={(
|
||||
value={
|
||||
<Link to={`/templates/job_template/${jobTemplate.id}`}>
|
||||
{jobTemplate.name}
|
||||
</Link>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`Job Type`)}
|
||||
value={toTitleCase(job.job_type)}
|
||||
/>
|
||||
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(job.job_type)} />
|
||||
{inventory && (
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
value={(
|
||||
<Link to={`/inventory/${inventory.id}`}>
|
||||
{inventory.name}
|
||||
</Link>
|
||||
)}
|
||||
value={
|
||||
<Link to={`/inventory/${inventory.id}`}>{inventory.name}</Link>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: show project status icon */}
|
||||
{project && (
|
||||
<Detail
|
||||
label={i18n._(t`Project`)}
|
||||
value={(
|
||||
<Link to={`/projects/${project.id}`}>
|
||||
{project.name}
|
||||
</Link>
|
||||
)}
|
||||
value={<Link to={`/projects/${project.id}`}>{project.name}</Link>}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`Revision`)}
|
||||
value={job.scm_revision}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Playbook`)}
|
||||
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}
|
||||
/>
|
||||
<Detail label={i18n._(t`Revision`)} value={job.scm_revision} />
|
||||
<Detail label={i18n._(t`Playbook`)} 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 && (
|
||||
<Detail
|
||||
label={i18n._(t`Instance Group`)}
|
||||
value={(
|
||||
value={
|
||||
<Link to={`/instance_groups/${instanceGroup.id}`}>
|
||||
{instanceGroup.name}
|
||||
</Link>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
typeof job.job_slice_number === 'number'
|
||||
&& typeof job.job_slice_count === 'number'
|
||||
&& (
|
||||
{typeof job.job_slice_number === 'number' &&
|
||||
typeof job.job_slice_count === 'number' && (
|
||||
<Detail
|
||||
label={i18n._(t`Job Slice`)}
|
||||
value={`${job.job_slice_number}/${job.job_slice_count}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{(credentials && credentials.length > 0) && (
|
||||
)}
|
||||
{credentials && credentials.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Credentials`)}
|
||||
value={(
|
||||
value={
|
||||
<ChipGroup showOverflowAfter={5}>
|
||||
{credentials.map(c => (
|
||||
<Chip key={c.id} isReadOnly>{c.name}</Chip>
|
||||
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||
))}
|
||||
</ChipGroup>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(labels && labels.count > 0) && (
|
||||
{labels && labels.count > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Credentials`)}
|
||||
value={(
|
||||
value={
|
||||
<ChipGroup showOverflowAfter={5}>
|
||||
{labels.results.map(l => (
|
||||
<Chip key={l.id} isReadOnly>{l.name}</Chip>
|
||||
<Chip key={l.id} isReadOnly>
|
||||
{l.name}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</DetailList>
|
||||
|
||||
@ -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={
|
||||
<ChipGroup showOverflowAfter={5}>
|
||||
{summary_fields.credentials.map(c => (
|
||||
<Chip key={c.id} isReadOnly>
|
||||
<strong className="credential">
|
||||
{c.kind ? credentialType(c.kind) : i18n._(t`Cloud`)}
|
||||
:
|
||||
</strong>
|
||||
{` ${c.name}`}
|
||||
</Chip>
|
||||
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
|
||||
@ -113,19 +113,21 @@ describe('<JobTemplateDetail />', () => {
|
||||
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(
|
||||
<JobTemplateDetail template={template} />
|
||||
);
|
||||
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]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user