Merge pull request #256 from marshmalien/skeleton-job-results

Skeleton job results
This commit is contained in:
Marliana Lara 2019-06-17 11:48:14 -04:00 committed by GitHub
commit 4e45a3b365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 425 additions and 83 deletions

View File

@ -1,29 +0,0 @@
import React from 'react';
import { mountWithContexts } from '../enzymeHelpers';
import Jobs from '../../src/pages/Jobs';
describe('<Jobs />', () => {
let pageWrapper;
let pageSections;
let title;
beforeEach(() => {
pageWrapper = mountWithContexts(<Jobs />);
pageSections = pageWrapper.find('PageSection');
title = pageWrapper.find('Title');
});
afterEach(() => {
pageWrapper.unmount();
});
test('initially renders without crashing', () => {
expect(pageWrapper.length).toBe(1);
expect(pageSections.length).toBe(2);
expect(title.length).toBe(1);
expect(title.props().size).toBe('2xl');
pageSections.forEach(section => {
expect(section.props().variant).toBeDefined();
});
});
});

View File

@ -0,0 +1,36 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../enzymeHelpers';
import Jobs from '../../../src/pages/Jobs';
describe('<Jobs />', () => {
test('initially renders succesfully', () => {
mountWithContexts(
<Jobs />
);
});
test('should display a breadcrumb heading', () => {
const history = createMemoryHistory({
initialEntries: ['/jobs'],
});
const match = { path: '/jobs', url: '/jobs', isExact: true };
const wrapper = mountWithContexts(
<Jobs />,
{
context: {
router: {
history,
route: {
location: history.location,
match
}
}
}
}
);
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
wrapper.unmount();
});
});

View File

@ -0,0 +1,9 @@
import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import { Job } from '../../../../../src/pages/Jobs';
describe('<Job />', () => {
test('initially renders succesfully', () => {
mountWithContexts(<Job />);
});
});

View File

@ -0,0 +1,24 @@
import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import JobDetail from '../../../../../src/pages/Jobs/JobDetail';
describe('<JobDetail />', () => {
const mockDetails = {
name: 'Foo'
};
test('initially renders succesfully', () => {
mountWithContexts(
<JobDetail job={mockDetails} />
);
});
test('should display a Close button', () => {
const wrapper = mountWithContexts(
<JobDetail job={mockDetails} />
);
expect(wrapper.find('Button[aria-label="close"]').length).toBe(1);
wrapper.unmount();
});
});

View File

@ -0,0 +1,15 @@
import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import JobOutput from '../../../../../src/pages/Jobs/JobOutput';
describe('<JobOutput />', () => {
const mockDetails = {
name: 'Foo'
};
test('initially renders succesfully', () => {
mountWithContexts(
<JobOutput job={mockDetails} />
);
});
});

View File

@ -1,5 +1,6 @@
import Config from './models/Config';
import InstanceGroups from './models/InstanceGroups';
import Jobs from './models/Jobs';
import Me from './models/Me';
import Organizations from './models/Organizations';
import Root from './models/Root';
@ -9,6 +10,7 @@ import Users from './models/Users';
const ConfigAPI = new Config();
const InstanceGroupsAPI = new InstanceGroups();
const JobsAPI = new Jobs();
const MeAPI = new Me();
const OrganizationsAPI = new Organizations();
const RootAPI = new Root();
@ -19,6 +21,7 @@ const UsersAPI = new Users();
export {
ConfigAPI,
InstanceGroupsAPI,
JobsAPI,
MeAPI,
OrganizationsAPI,
RootAPI,

10
src/api/models/Jobs.js Normal file
View File

@ -0,0 +1,10 @@
import Base from '../Base';
class Jobs extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/jobs/';
}
}
export default Jobs;

View File

@ -203,15 +203,6 @@
text-align: right;
}
.awx-orgTabs-container{
display: flex
}
.awx-orgTabs__bottom-border{
flex-grow: 1;
border-bottom: 1px solid #d2d2d2
}
//
// AlertModal styles
//

View File

@ -1,7 +1,35 @@
import React from 'react';
import { shape, string, number, arrayOf } from 'prop-types';
import { Tab, Tabs } from '@patternfly/react-core';
import { Tab, Tabs as PFTabs } from '@patternfly/react-core';
import { withRouter } from 'react-router-dom';
import styled from 'styled-components';
const Tabs = styled(PFTabs)`
--pf-c-tabs__button--PaddingLeft: 20px;
--pf-c-tabs__button--PaddingRight: 20px;
.pf-c-tabs__list {
li:first-of-type .pf-c-tabs__button {
&::before {
border-left: none;
}
&::after {
margin-left: 0;
}
}
}
&::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
border: solid var(--pf-c-tabs__item--BorderColor);
border-width: var(--pf-c-tabs__item--BorderWidth) 0 var(--pf-c-tabs__item--BorderWidth) 0;
}
`;
function RoutedTabs (props) {
const { history, tabsArray } = props;

View File

@ -1,28 +0,0 @@
import React, { Component, Fragment } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
class Jobs extends Component {
render () {
const { i18n } = this.props;
const { light, medium } = PageSectionVariants;
return (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl">
{i18n._(t`Jobs`)}
</Title>
</PageSection>
<PageSection variant={medium} />
</Fragment>
);
}
}
export default withI18n()(Jobs);

153
src/pages/Jobs/Job.jsx Normal file
View File

@ -0,0 +1,153 @@
import React, { Component } from 'react';
import { Route, withRouter, Switch, Redirect } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { Card, CardHeader as PFCardHeader, PageSection } from '@patternfly/react-core';
import { JobsAPI } from '../../api';
import ContentError from '../../components/ContentError';
import CardCloseButton from '../../components/CardCloseButton';
import RoutedTabs from '../../components/Tabs/RoutedTabs';
import JobDetail from './JobDetail';
import JobOutput from './JobOutput';
export class Job extends Component {
constructor (props) {
super(props);
this.state = {
job: null,
contentError: false,
contentLoading: true,
isInitialized: false
};
this.fetchJob = this.fetchJob.bind(this);
}
async componentDidMount () {
await this.fetchJob();
this.setState({ isInitialized: true });
}
async componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
await this.fetchJob();
}
}
async fetchJob () {
const {
match,
setBreadcrumb,
} = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: false, contentLoading: true });
try {
const { data } = await JobsAPI.readDetail(id);
setBreadcrumb(data);
this.setState({ job: data });
} catch (error) {
this.setState({ contentError: true });
} finally {
this.setState({ contentLoading: false });
}
}
render () {
const {
history,
match,
i18n
} = this.props;
const {
job,
contentError,
contentLoading,
isInitialized
} = this.state;
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
{ name: i18n._(t`Output`), link: `${match.url}/output`, id: 1 }
];
const CardHeader = styled(PFCardHeader)`
--pf-c-card--first-child--PaddingTop: 0;
--pf-c-card--child--PaddingLeft: 0;
--pf-c-card--child--PaddingRight: 0;
position: relative;
`;
let cardHeader = (
<CardHeader>
<RoutedTabs
match={match}
history={history}
tabsArray={tabsArray}
/>
<CardCloseButton linkTo="/jobs" />
</CardHeader>
);
if (!isInitialized) {
cardHeader = null;
}
if (!match) {
cardHeader = null;
}
if (!contentLoading && contentError) {
return (
<PageSection>
<Card className="awx-c-card">
<ContentError />
</Card>
</PageSection>
);
}
return (
<PageSection>
<Card>
{ cardHeader }
<Switch>
<Redirect
from="/jobs/:id"
to="/jobs/:id/details"
exact
/>
{job && (
<Route
path="/jobs/:id/details"
render={() => (
<JobDetail
match={match}
job={job}
/>
)}
/>
)}
{job && (
<Route
path="/jobs/:id/output"
render={() => (
<JobOutput
match={match}
job={job}
/>
)}
/>
)}
</Switch>
</Card>
</PageSection>
);
}
}
export default withI18n()(withRouter(Job));

View File

@ -0,0 +1,38 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { CardBody, Button } from '@patternfly/react-core';
import styled from 'styled-components';
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
`;
class JobDetail extends Component {
render () {
const {
job,
i18n
} = this.props;
return (
<CardBody>
<b>{job.name}</b>
<ActionButtonWrapper>
<Button
variant="secondary"
aria-label="close"
component={Link}
to="/jobs"
>
{i18n._(t`Close`)}
</Button>
</ActionButtonWrapper>
</CardBody>
);
}
}
export default withI18n()(withRouter(JobDetail));

View File

@ -0,0 +1,4 @@
import JobDetail from './JobDetail';
export default JobDetail;

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
import { CardBody } from '@patternfly/react-core';
class JobOutput extends Component {
render () {
const {
job
} = this.props;
return (
<CardBody>
<b>{job.name}</b>
</CardBody>
);
}
}
export default JobOutput;

View File

@ -0,0 +1,4 @@
import JobOutput from './JobOutput';
export default JobOutput;

64
src/pages/Jobs/Jobs.jsx Normal file
View File

@ -0,0 +1,64 @@
import React, { Component, Fragment } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
import { Job } from '.';
class Jobs extends Component {
constructor (props) {
super(props);
const { i18n } = props;
this.state = {
breadcrumbConfig: {
'/jobs': i18n._(t`Jobs`)
}
};
}
setBreadcrumbConfig = (job) => {
const { i18n } = this.props;
if (!job) {
return;
}
const breadcrumbConfig = {
'/jobs': i18n._(t`Jobs`),
[`/jobs/${job.id}`]: `${job.name}`,
[`/jobs/${job.id}/details`]: i18n._(t`Details`),
[`/jobs/${job.id}/output`]: i18n._(t`Output`)
};
this.setState({ breadcrumbConfig });
}
render () {
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return (
<Fragment>
<Breadcrumbs
breadcrumbConfig={breadcrumbConfig}
/>
<Switch>
<Route
path={`${match.path}/:id`}
render={() => (
<Job
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
/>
)}
/>
</Switch>
</Fragment>
);
}
}
export default withI18n()(withRouter(Jobs));

2
src/pages/Jobs/index.js Normal file
View File

@ -0,0 +1,2 @@
export { default as Job } from './Job';
export { default } from './Jobs';

View File

@ -2,7 +2,8 @@ import React, { Component } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Switch, Route, withRouter, Redirect } from 'react-router-dom';
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
import { Card, CardHeader as PFCardHeader, PageSection } from '@patternfly/react-core';
import styled from 'styled-components';
import CardCloseButton from '../../../../components/CardCloseButton';
import ContentError from '../../../../components/ContentError';
import OrganizationAccess from './OrganizationAccess';
@ -130,22 +131,21 @@ class Organization extends Component {
});
}
const CardHeader = styled(PFCardHeader)`
--pf-c-card--first-child--PaddingTop: 0;
--pf-c-card--child--PaddingLeft: 0;
--pf-c-card--child--PaddingRight: 0;
position: relative;
`;
let cardHeader = (
<CardHeader style={{ padding: 0 }}>
<React.Fragment>
<div className="awx-orgTabs-container">
<RoutedTabs
match={match}
history={history}
labeltext={i18n._(t`Organization detail tabs`)}
tabsArray={tabsArray}
/>
<CardCloseButton linkTo="/organizations" />
<div
className="awx-orgTabs__bottom-border"
/>
</div>
</React.Fragment>
<CardHeader>
<RoutedTabs
match={match}
history={history}
tabsArray={tabsArray}
/>
<CardCloseButton linkTo="/organizations" />
</CardHeader>
);