1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Merge pull request #4965 from marshmalien/project-detail

Add project detail and unit tests

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-10-14 18:30:30 +00:00 committed by GitHub
commit cbed525547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 419 additions and 11 deletions

View File

@ -1,10 +1,157 @@
import React, { Component } from 'react';
import { CardBody } from '@patternfly/react-core';
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { Project } from '@types';
import { formatDateString } from '@util/dates';
import { Button, CardBody, List, ListItem } from '@patternfly/react-core';
import { DetailList, Detail } from '@components/DetailList';
import { CredentialChip } from '@components/Chip';
class ProjectDetail extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
function ProjectDetail({ project, i18n }) {
const {
allow_override,
created,
custom_virtualenv,
description,
id,
modified,
name,
scm_branch,
scm_clean,
scm_delete_on_update,
scm_type,
scm_update_on_launch,
scm_update_cache_timeout,
scm_url,
summary_fields,
} = project;
let optionsList = '';
if (
scm_clean ||
scm_delete_on_update ||
scm_update_on_launch ||
allow_override
) {
optionsList = (
<List>
{scm_clean && <ListItem>{i18n._(t`Clean`)}</ListItem>}
{scm_delete_on_update && (
<ListItem>{i18n._(t`Delete on Update`)}</ListItem>
)}
{scm_update_on_launch && (
<ListItem>{i18n._(t`Update Revision on Launch`)}</ListItem>
)}
{allow_override && (
<ListItem>{i18n._(t`Allow Branch Override`)}</ListItem>
)}
</List>
);
}
let createdBy = '';
if (created) {
if (summary_fields.created_by && summary_fields.created_by.username) {
createdBy = i18n._(
t`${formatDateString(created)} by ${summary_fields.created_by.username}`
);
} else {
createdBy = formatDateString(created);
}
}
let modifiedBy = '';
if (modified) {
if (summary_fields.modified_by && summary_fields.modified_by.username) {
modifiedBy = i18n._(
t`${formatDateString(modified)} by ${
summary_fields.modified_by.username
}`
);
} else {
modifiedBy = formatDateString(modified);
}
}
return (
<CardBody css="padding-top: 20px">
<DetailList gutter="sm">
<Detail label={i18n._(t`Name`)} value={name} />
<Detail label={i18n._(t`Description`)} value={description} />
{summary_fields.organization && (
<Detail
label={i18n._(t`Organization`)}
value={summary_fields.organization.name}
/>
)}
<Detail label={i18n._(t`SCM Type`)} value={scm_type} />
<Detail label={i18n._(t`SCM URL`)} value={scm_url} />
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} />
{summary_fields.credential && (
<Detail
label={i18n._(t`SCM Credential`)}
value={
<CredentialChip
key={summary_fields.credential.id}
credential={summary_fields.credential}
isReadOnly
/>
}
/>
)}
{optionsList && (
<Detail label={i18n._(t`Options`)} value={optionsList} />
)}
<Detail
label={i18n._(t`Cache Timeout`)}
value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`}
/>
<Detail
label={i18n._(t`Ansible Environment`)}
value={custom_virtualenv}
/>
{/* TODO: Link to user in users */}
<Detail label={i18n._(t`Created`)} value={createdBy} />
{/* TODO: Link to user in users */}
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} />
</DetailList>
<ActionButtonWrapper>
{summary_fields.user_capabilities &&
summary_fields.user_capabilities.edit && (
<Button
aria-label={i18n._(t`edit`)}
component={Link}
to={`/projects/${id}/edit`}
>
{i18n._(t`Edit`)}
</Button>
)}
<Button
variant="secondary"
aria-label={i18n._(t`close`)}
component={Link}
to="/projects"
>
{i18n._(t`Close`)}
</Button>
</ActionButtonWrapper>
</CardBody>
);
}
export default ProjectDetail;
ProjectDetail.propTypes = {
project: Project.isRequired,
};
export default withI18n()(withRouter(ProjectDetail));

View File

@ -0,0 +1,220 @@
import React from 'react';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import ProjectDetail from './ProjectDetail';
describe('<ProjectDetail />', () => {
const mockProject = {
id: 1,
type: 'project',
url: '/api/v2/projects/1',
summary_fields: {
organization: {
id: 10,
name: 'Foo',
},
credential: {
id: 1000,
name: 'qux',
kind: 'scm',
},
last_job: {
id: 9000,
status: 'successful',
},
created_by: {
id: 1,
username: 'admin',
},
modified_by: {
id: 1,
username: 'admin',
},
user_capabilities: {
edit: true,
delete: true,
start: true,
schedule: true,
copy: true,
},
},
created: '2019-10-10T01:15:06.780472Z',
modified: '2019-10-10T01:15:06.780490Z',
name: 'Project 1',
description: 'lorem ipsum',
scm_type: 'git',
scm_url: 'https://mock.com/bar',
scm_branch: 'baz',
scm_refspec: 'refs/remotes/*',
scm_clean: true,
scm_delete_on_update: true,
credential: 100,
status: 'successful',
organization: 10,
scm_update_on_launch: true,
scm_update_cache_timeout: 5,
allow_override: true,
custom_virtualenv: '/custom-venv',
};
test('initially renders succesfully', () => {
mountWithContexts(<ProjectDetail project={mockProject} />);
});
test('should render Details', () => {
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, {
context: {
linguiPublisher: {
i18n: {
_: key => {
if (key.values) {
Object.entries(key.values).forEach(([k, v]) => {
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
});
}
return key.id;
},
},
},
},
});
function assertDetail(label, value) {
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
}
assertDetail('Name', mockProject.name);
assertDetail('Description', mockProject.description);
assertDetail('Organization', mockProject.summary_fields.organization.name);
assertDetail('SCM Type', mockProject.scm_type);
assertDetail('SCM URL', mockProject.scm_url);
assertDetail('SCM Branch', mockProject.scm_branch);
assertDetail(
'SCM Credential',
`Scm: ${mockProject.summary_fields.credential.name}`
);
assertDetail(
'Cache Timeout',
`${mockProject.scm_update_cache_timeout} Seconds`
);
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
assertDetail(
'Created',
`10/10/2019, 1:15:06 AM by ${mockProject.summary_fields.created_by.username}`
);
assertDetail(
'Last Modified',
`10/10/2019, 1:15:06 AM by ${mockProject.summary_fields.modified_by.username}`
);
expect(
wrapper
.find('Detail[label="Options"]')
.containsAllMatchingElements([
<li>Clean</li>,
<li>Delete on Update</li>,
<li>Update Revision on Launch</li>,
<li>Allow Branch Override</li>,
])
).toEqual(true);
});
test('should hide options label when all project options return false', () => {
const mockOptions = {
scm_clean: false,
scm_delete_on_update: false,
scm_update_on_launch: false,
allow_override: false,
created: '',
modified: '',
};
const wrapper = mountWithContexts(
<ProjectDetail project={{ ...mockProject, ...mockOptions }} />
);
expect(wrapper.find('Detail[label="Options"]').length).toBe(0);
});
test('should render with missing summary fields', async done => {
const wrapper = mountWithContexts(
<ProjectDetail project={{ ...mockProject, summary_fields: {} }} />
);
await waitForElement(
wrapper,
'Detail[label="Name"]',
el => el.length === 1
);
done();
});
test('should show edit button for users with edit permission', async done => {
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />);
const editButton = await waitForElement(
wrapper,
'ProjectDetail Button[aria-label="edit"]'
);
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe(`/projects/${mockProject.id}/edit`);
done();
});
test('should hide edit button for users without edit permission', async done => {
const wrapper = mountWithContexts(
<ProjectDetail
project={{
...mockProject,
summary_fields: {
user_capabilities: {
edit: false,
},
},
}}
/>
);
await waitForElement(wrapper, 'ProjectDetail');
expect(wrapper.find('ProjectDetail Button[aria-label="edit"]').length).toBe(
0
);
done();
});
test('edit button should navigate to project edit', () => {
const context = {
router: {
history: {
push: jest.fn(),
replace: jest.fn(),
createHref: jest.fn(),
},
},
};
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, {
context,
});
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1);
expect(context.router.history.push).not.toHaveBeenCalled();
wrapper
.find('Button[aria-label="edit"] Link')
.simulate('click', { button: 0 });
expect(context.router.history.push).toHaveBeenCalledWith(
'/projects/1/edit'
);
});
test('close button should navigate to projects list', () => {
const context = {
router: {
history: {
push: jest.fn(),
replace: jest.fn(),
createHref: jest.fn(),
},
},
};
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, {
context,
});
expect(wrapper.find('Button[aria-label="close"]').length).toBe(1);
expect(context.router.history.push).not.toHaveBeenCalled();
wrapper
.find('Button[aria-label="close"] Link')
.simulate('click', { button: 0 });
expect(context.router.history.push).toHaveBeenCalledWith('/projects');
});
});

View File

@ -4,6 +4,7 @@ import {
number,
string,
bool,
objectOf,
oneOf,
oneOfType,
} from 'prop-types';
@ -71,11 +72,6 @@ export const JobTemplate = shape({
project: number,
});
export const Project = shape({
id: number.isRequired,
name: string.isRequired,
});
export const Inventory = shape({
id: number.isRequired,
name: string,
@ -109,6 +105,51 @@ export const Credential = shape({
kind: string,
});
export const Project = shape({
id: number.isRequired,
type: oneOf(['project']),
url: string,
related: shape(),
summary_fields: shape({
organization: Organization,
credential: Credential,
last_job: shape({}),
last_update: shape({}),
created_by: shape({}),
modified_by: shape({}),
object_roles: shape({}),
user_capabilities: objectOf(bool),
}),
created: string,
name: string.isRequired,
description: string,
scm_type: oneOf(['', 'git', 'hg', 'svn', 'insights']),
scm_url: string,
scm_branch: string,
scm_refspec: string,
scm_clean: bool,
scm_delete_on_update: bool,
credential: number,
status: oneOf([
'new',
'pending',
'waiting',
'running',
'successful',
'failed',
'error',
'canceled',
'never updated',
'ok',
'missing',
]),
organization: number,
scm_update_on_launch: bool,
scm_update_cache_timeout: number,
allow_override: bool,
custom_virtualenv: string,
});
export const Job = shape({
status: string,
started: string,