mirror of
https://github.com/ansible/awx.git
synced 2026-02-17 19:20:05 -03:30
Add project detail and unit tests
This commit is contained in:
@@ -1,10 +1,155 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
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 {
|
const ActionButtonWrapper = styled.div`
|
||||||
render() {
|
display: flex;
|
||||||
return <CardBody>Coming soon :)</CardBody>;
|
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 = `${formatDateString(created)} ${i18n._(t`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 = `${formatDateString(modified)} ${i18n._(t`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));
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
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} />);
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
number,
|
number,
|
||||||
string,
|
string,
|
||||||
bool,
|
bool,
|
||||||
|
objectOf,
|
||||||
oneOf,
|
oneOf,
|
||||||
oneOfType,
|
oneOfType,
|
||||||
} from 'prop-types';
|
} from 'prop-types';
|
||||||
@@ -71,11 +72,6 @@ export const JobTemplate = shape({
|
|||||||
project: number,
|
project: number,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Project = shape({
|
|
||||||
id: number.isRequired,
|
|
||||||
name: string.isRequired,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Inventory = shape({
|
export const Inventory = shape({
|
||||||
id: number.isRequired,
|
id: number.isRequired,
|
||||||
name: string,
|
name: string,
|
||||||
@@ -109,6 +105,51 @@ export const Credential = shape({
|
|||||||
kind: string,
|
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({
|
export const Job = shape({
|
||||||
status: string,
|
status: string,
|
||||||
started: string,
|
started: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user