Merge pull request #6335 from AlexSCorey/6316-TemplateUsesFunction

Moves template.jsx over to a functional component.

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-03-20 14:54:49 +00:00
committed by GitHub
2 changed files with 236 additions and 291 deletions

View File

@@ -1,8 +1,18 @@
import React, { Component } from 'react'; import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { Card, CardActions, PageSection } from '@patternfly/react-core'; import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom'; import {
Switch,
Route,
Redirect,
withRouter,
Link,
useLocation,
useParams,
useRouteMatch,
} from 'react-router-dom';
import useRequest from '@util/useRequest';
import { TabbedCardHeader } from '@components/Card'; import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
@@ -17,261 +27,195 @@ import JobTemplateEdit from './JobTemplateEdit';
import { JobTemplatesAPI, OrganizationsAPI } from '@api'; import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import TemplateSurvey from './TemplateSurvey'; import TemplateSurvey from './TemplateSurvey';
class Template extends Component { function Template({ i18n, me, setBreadcrumb }) {
constructor(props) { const location = useLocation();
super(props); const { id: templateId } = useParams();
const match = useRouteMatch();
this.state = { const {
contentError: null, result: { isNotifAdmin, template },
hasContentLoading: true, isLoading: hasRolesandTemplateLoading,
template: null, error: rolesAndTemplateError,
isNotifAdmin: false, request: loadTemplateAndRoles,
}; } = useRequest(
this.loadTemplate = this.loadTemplate.bind(this); useCallback(async () => {
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
await this.loadTemplateAndRoles();
}
async componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
await this.loadTemplate();
}
}
async loadTemplateAndRoles() {
const { match, setBreadcrumb } = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: null, hasContentLoading: true });
try {
const [{ data }, notifAdminRes] = await Promise.all([ const [{ data }, notifAdminRes] = await Promise.all([
JobTemplatesAPI.readDetail(id), JobTemplatesAPI.readDetail(templateId),
OrganizationsAPI.read({ OrganizationsAPI.read({
page_size: 1, page_size: 1,
role_level: 'notification_admin_role', role_level: 'notification_admin_role',
}), }),
]); ]);
setBreadcrumb(data); setBreadcrumb(data);
this.setState({
return {
template: data, template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0, isNotifAdmin: notifAdminRes.data.results.length > 0,
}); };
} catch (err) { }, [setBreadcrumb, templateId]),
this.setState({ contentError: err }); { isNotifAdmin: false, template: null }
} finally { );
this.setState({ hasContentLoading: false }); useEffect(() => {
} loadTemplateAndRoles();
} }, [loadTemplateAndRoles]);
async loadTemplate() { const loadScheduleOptions = () => {
const { setBreadcrumb, match } = this.props; return JobTemplatesAPI.readScheduleOptions(templateId);
const { id } = match.params; };
this.setState({ contentError: null, hasContentLoading: true }); const loadSchedules = params => {
try { return JobTemplatesAPI.readSchedules(templateId, params);
const { data } = await JobTemplatesAPI.readDetail(id); };
setBreadcrumb(data);
this.setState({ template: data });
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
loadScheduleOptions() { const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const { template } = this.state;
return JobTemplatesAPI.readScheduleOptions(template.id);
}
loadSchedules(params) { const tabsArray = [
const { template } = this.state; { name: i18n._(t`Details`), link: `${match.url}/details` },
return JobTemplatesAPI.readSchedules(template.id, params); { name: i18n._(t`Access`), link: `${match.url}/access` },
} ];
render() { if (canSeeNotificationsTab) {
const { i18n, location, match, me, setBreadcrumb } = this.props; tabsArray.push({
const { name: i18n._(t`Notifications`),
contentError, link: `${match.url}/notifications`,
hasContentLoading,
isNotifAdmin,
template,
} = this.state;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
});
}
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.push(
{
name: i18n._(t`Completed Jobs`),
link: `${match.url}/completed_jobs`,
},
{
name: i18n._(t`Survey`),
link: `${match.url}/survey`,
}
);
tabsArray.forEach((tab, n) => {
tab.id = n;
}); });
}
let cardHeader = ( if (template) {
<TabbedCardHeader> tabsArray.push({
<RoutedTabs tabsArray={tabsArray} /> name: i18n._(t`Schedules`),
<CardActions> link: `${match.url}/schedules`,
<CardCloseButton linkTo="/templates" /> });
</CardActions> }
</TabbedCardHeader>
);
if ( tabsArray.push(
location.pathname.endsWith('edit') || {
location.pathname.includes('schedules/') name: i18n._(t`Completed Jobs`),
) { link: `${match.url}/completed_jobs`,
cardHeader = null; },
{
name: i18n._(t`Survey`),
link: `${match.url}/survey`,
} }
);
if (!hasContentLoading && contentError) { tabsArray.forEach((tab, n) => {
return ( tab.id = n;
<PageSection> });
<Card>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(`Template not found.`)}{' '}
<Link to="/templates">{i18n._(`View all Templates.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
let cardHeader = (
<TabbedCardHeader>
<RoutedTabs tabsArray={tabsArray} />
<CardActions>
<CardCloseButton linkTo="/templates" />
</CardActions>
</TabbedCardHeader>
);
if (
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
cardHeader = null;
}
const contentError = rolesAndTemplateError;
if (!hasRolesandTemplateLoading && contentError) {
return ( return (
<PageSection> <PageSection>
<Card> <Card>
{cardHeader} <ContentError error={contentError}>
<Switch> {contentError.response.status === 404 && (
<Redirect <span>
from="/templates/:templateType/:id" {i18n._(`Template not found.`)}{' '}
to="/templates/:templateType/:id/details" <Link to="/templates">{i18n._(`View all Templates.`)}</Link>
exact </span>
/>
{template && (
<Route
key="details"
path="/templates/:templateType/:id/details"
render={() => (
<JobTemplateDetail
hasTemplateLoading={hasContentLoading}
template={template}
/>
)}
/>
)} )}
{template && ( </ContentError>
<Route
key="edit"
path="/templates/:templateType/:id/edit"
render={() => <JobTemplateEdit template={template} />}
/>
)}
{template && (
<Route
key="access"
path="/templates/:templateType/:id/access"
render={() => (
<ResourceAccessList
resource={template}
apiModel={JobTemplatesAPI}
/>
)}
/>
)}
{template && (
<Route
key="schedules"
path="/templates/:templateType/:id/schedules"
render={() => (
<Schedules
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={template}
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
)}
/>
)}
{canSeeNotificationsTab && (
<Route
path="/templates/:templateType/:id/notifications"
render={() => (
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={isNotifAdmin}
apiModel={JobTemplatesAPI}
/>
)}
/>
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={template} />
</Route>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>
</Switch>
</Card> </Card>
</PageSection> </PageSection>
); );
} }
return (
<PageSection>
<Card>
{cardHeader}
<Switch>
<Redirect
from="/templates/:templateType/:id"
to="/templates/:templateType/:id/details"
exact
/>
{template && (
<Route key="details" path="/templates/:templateType/:id/details">
<JobTemplateDetail
hasTemplateLoading={hasRolesandTemplateLoading}
template={template}
/>
</Route>
)}
{template && (
<Route key="edit" path="/templates/:templateType/:id/edit">
<JobTemplateEdit template={template} />
</Route>
)}
{template && (
<Route key="access" path="/templates/:templateType/:id/access">
<ResourceAccessList
resource={template}
apiModel={JobTemplatesAPI}
/>
</Route>
)}
{template && (
<Route
key="schedules"
path="/templates/:templateType/:id/schedules"
>
<Schedules
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={template}
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
</Route>
)}
{canSeeNotificationsTab && (
<Route path="/templates/:templateType/:id/notifications">
<NotificationList
id={Number(templateId)}
canToggleNotifications={isNotifAdmin}
apiModel={JobTemplatesAPI}
/>
</Route>
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={template} />
</Route>
)}
{!hasRolesandTemplateLoading && (
<Route key="not-found" path="*">
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
</Route>
)}
</Switch>
</Card>
</PageSection>
);
} }
export { Template as _Template }; export { Template as _Template };

View File

@@ -1,63 +1,57 @@
import React from 'react'; import React from 'react';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { JobTemplatesAPI, OrganizationsAPI } from '@api'; import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import Template, { _Template } from './Template'; import Template from './Template';
import mockJobTemplateData from './shared/data.job_template.json'; import mockJobTemplateData from './shared/data.job_template.json';
jest.mock('@api'); jest.mock('@api/models/JobTemplates');
jest.mock('@api/models/Organizations');
JobTemplatesAPI.readDetail.mockResolvedValue({
data: mockJobTemplateData,
});
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,
next: null,
previous: null,
results: [
{
id: 1,
},
],
},
});
const mockMe = { const mockMe = {
is_super_user: true, is_super_user: true,
is_system_auditor: false, is_system_auditor: false,
}; };
describe('<Template />', () => { describe('<Template />', () => {
test('initially renders succesfully', () => { beforeEach(() => {
mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />); JobTemplatesAPI.readDetail.mockResolvedValue({
data: mockJobTemplateData,
});
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,
next: null,
previous: null,
results: [
{
id: 1,
},
],
},
});
}); });
test('When component mounts API is called and the response is put in state', async done => { test('initially renders succesfully', async () => {
const loadTemplateAndRoles = jest.spyOn( await act(async () => {
_Template.prototype, mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />);
'loadTemplateAndRoles' });
); });
const wrapper = mountWithContexts( test('When component mounts API is called and the response is put in state', async () => {
<Template setBreadcrumb={() => {}} me={mockMe} /> await act(async () => {
); mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />);
await waitForElement( });
wrapper, expect(JobTemplatesAPI.readDetail).toBeCalled();
'Template', expect(OrganizationsAPI.read).toBeCalled();
el => el.state('hasContentLoading') === true
);
expect(loadTemplateAndRoles).toHaveBeenCalled();
await waitForElement(
wrapper,
'Template',
el => el.state('hasContentLoading') === true
);
done();
}); });
test('notifications tab shown for admins', async done => { test('notifications tab shown for admins', async done => {
const wrapper = mountWithContexts( let wrapper;
<Template setBreadcrumb={() => {}} me={mockMe} /> await act(async () => {
); wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement( const tabs = await waitForElement(
wrapper, wrapper,
@@ -77,9 +71,12 @@ describe('<Template />', () => {
}, },
}); });
const wrapper = mountWithContexts( let wrapper;
<Template setBreadcrumb={() => {}} me={mockMe} /> await act(async () => {
); wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement( const tabs = await waitForElement(
wrapper, wrapper,
'.pf-c-tabs__item', '.pf-c-tabs__item',
@@ -93,24 +90,28 @@ describe('<Template />', () => {
const history = createMemoryHistory({ const history = createMemoryHistory({
initialEntries: ['/templates/job_template/1/foobar'], initialEntries: ['/templates/job_template/1/foobar'],
}); });
const wrapper = mountWithContexts( let wrapper;
<Template setBreadcrumb={() => {}} me={mockMe} />, await act(async () => {
{ wrapper = mountWithContexts(
context: { <Template setBreadcrumb={() => {}} me={mockMe} />,
router: { {
history, context: {
route: { router: {
location: history.location, history,
match: { route: {
params: { id: 1 }, location: history.location,
url: '/templates/job_template/1/foobar', match: {
path: '/templates/job_template/1/foobar', params: { id: 1 },
url: '/templates/job_template/1/foobar',
path: '/templates/job_template/1/foobar',
},
}, },
}, },
}, },
}, }
} );
); });
await waitForElement(wrapper, 'ContentError', el => el.length === 1); await waitForElement(wrapper, 'ContentError', el => el.length === 1);
}); });
}); });