add CredentialChip component

This commit is contained in:
Keith Grant 2019-07-10 16:31:05 -07:00
parent eee1601528
commit 40f9b0dc7f
8 changed files with 157 additions and 103 deletions

View File

@ -1,6 +1,5 @@
import React from 'react';
import { mount } from 'enzyme';
import Chip from './Chip';
describe('Chip', () => {

View 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);

View 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');
});
});

View File

@ -1,2 +1,3 @@
export { default as ChipGroup } from './ChipGroup';
export { default as Chip } from './Chip';
export { default as CredentialChip } from './CredentialChip';

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>
}

View File

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