Merge pull request #8706 from AlexSCorey/PreLinguiUpgrade

Pre-lingui upgrade 

Reviewed-by: Jake McDermott <yo@jakemcdermott.me>
             https://github.com/jakemcdermott
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-12-16 21:46:31 +00:00
committed by GitHub
14 changed files with 422 additions and 672 deletions

View File

@@ -12,8 +12,8 @@ import {
import { BrandName } from '../../variables'; import { BrandName } from '../../variables';
import brandLogoImg from './brand-logo.svg'; import brandLogoImg from './brand-logo.svg';
class About extends React.Component { function About({ ansible_version, version, isOpen, onClose, i18n }) {
static createSpeechBubble(version) { const createSpeechBubble = () => {
let text = `${BrandName} ${version}`; let text = `${BrandName} ${version}`;
let top = ''; let top = '';
let bottom = ''; let bottom = '';
@@ -28,31 +28,22 @@ class About extends React.Component {
bottom = ` --${bottom}-- `; bottom = ` --${bottom}-- `;
return top + text + bottom; return top + text + bottom;
} };
constructor(props) { const speechBubble = createSpeechBubble();
super(props);
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this); return (
} <AboutModal
isOpen={isOpen}
render() { onClose={onClose}
const { ansible_version, version, isOpen, onClose, i18n } = this.props; productName={`Ansible ${BrandName}`}
trademark={i18n._(t`Copyright 2019 Red Hat, Inc.`)}
const speechBubble = this.createSpeechBubble(version); brandImageSrc={brandLogoImg}
brandImageAlt={i18n._(t`Brand Image`)}
return ( >
<AboutModal <pre>
isOpen={isOpen} {speechBubble}
onClose={onClose} {`
productName={`Ansible ${BrandName}`}
trademark={i18n._(t`Copyright 2019 Red Hat, Inc.`)}
brandImageSrc={brandLogoImg}
brandImageAlt={i18n._(t`Brand Image`)}
>
<pre>
{speechBubble}
{`
\\ \\
\\ ^__^ \\ ^__^
(oo)\\_______ (oo)\\_______
@@ -60,18 +51,17 @@ class About extends React.Component {
||----w | ||----w |
|| || || ||
`} `}
</pre> </pre>
<TextContent> <TextContent>
<TextList component="dl"> <TextList component="dl">
<TextListItem component="dt"> <TextListItem component="dt">
{i18n._(t`Ansible Version`)} {i18n._(t`Ansible Version`)}
</TextListItem> </TextListItem>
<TextListItem component="dd">{ansible_version}</TextListItem> <TextListItem component="dd">{ansible_version}</TextListItem>
</TextList> </TextList>
</TextContent> </TextContent>
</AboutModal> </AboutModal>
); );
}
} }
About.propTypes = { About.propTypes = {

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -17,129 +17,100 @@ import { QuestionCircleIcon, UserIcon } from '@patternfly/react-icons';
const DOCLINK = const DOCLINK =
'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html'; 'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html';
class PageHeaderToolbar extends Component { function PageHeaderToolbar({
constructor(props) { isAboutDisabled,
super(props); onAboutClick,
this.state = { onLogoutClick,
isHelpOpen: false, loggedInUser,
isUserOpen: false, i18n,
}; }) {
const [isHelpOpen, setIsHelpOpen] = useState(false);
const [isUserOpen, setIsUserOpen] = useState(false);
this.handleHelpSelect = this.handleHelpSelect.bind(this); const handleHelpSelect = () => {
this.handleHelpToggle = this.handleHelpToggle.bind(this); setIsHelpOpen(!isHelpOpen);
this.handleUserSelect = this.handleUserSelect.bind(this); };
this.handleUserToggle = this.handleUserToggle.bind(this);
}
handleHelpSelect() { const handleUserSelect = () => {
const { isHelpOpen } = this.state; setIsUserOpen(!isUserOpen);
};
this.setState({ isHelpOpen: !isHelpOpen }); return (
} <PageHeaderTools>
<PageHeaderToolsGroup>
handleUserSelect() { <Tooltip position="left" content={<div>{i18n._(t`Info`)}</div>}>
const { isUserOpen } = this.state; <PageHeaderToolsItem>
<Dropdown
this.setState({ isUserOpen: !isUserOpen }); isPlain
} isOpen={isHelpOpen}
position={DropdownPosition.right}
handleHelpToggle(isOpen) { onSelect={handleHelpSelect}
this.setState({ isHelpOpen: isOpen }); toggle={
} <DropdownToggle
onToggle={setIsHelpOpen}
handleUserToggle(isOpen) { aria-label={i18n._(t`Info`)}
this.setState({ isUserOpen: isOpen }); >
} <QuestionCircleIcon />
</DropdownToggle>
render() { }
const { isHelpOpen, isUserOpen } = this.state; dropdownItems={[
const { <DropdownItem key="help" target="_blank" href={DOCLINK}>
isAboutDisabled, {i18n._(t`Help`)}
onAboutClick, </DropdownItem>,
onLogoutClick, <DropdownItem
loggedInUser, key="about"
i18n, component="button"
} = this.props; isDisabled={isAboutDisabled}
onClick={onAboutClick}
return ( >
<PageHeaderTools> {i18n._(t`About`)}
<PageHeaderToolsGroup> </DropdownItem>,
<Tooltip position="left" content={<div>{i18n._(t`Info`)}</div>}> ]}
<PageHeaderToolsItem> />
<Dropdown </PageHeaderToolsItem>
isPlain </Tooltip>
isOpen={isHelpOpen} <Tooltip position="left" content={<div>{i18n._(t`User`)}</div>}>
position={DropdownPosition.right} <PageHeaderToolsItem>
onSelect={this.handleHelpSelect} <Dropdown
toggle={ id="toolbar-user-dropdown"
<DropdownToggle isPlain
onToggle={this.handleHelpToggle} isOpen={isUserOpen}
aria-label={i18n._(t`Info`)} position={DropdownPosition.right}
> onSelect={handleUserSelect}
<QuestionCircleIcon /> toggle={
</DropdownToggle> <DropdownToggle onToggle={setIsUserOpen}>
} <UserIcon />
dropdownItems={[ {loggedInUser && (
<DropdownItem key="help" target="_blank" href={DOCLINK}> <span style={{ marginLeft: '10px' }}>
{i18n._(t`Help`)} {loggedInUser.username}
</DropdownItem>, </span>
<DropdownItem )}
key="about" </DropdownToggle>
component="button" }
isDisabled={isAboutDisabled} dropdownItems={[
onClick={onAboutClick} <DropdownItem
> key="user"
{i18n._(t`About`)} href={
</DropdownItem>, loggedInUser ? `/users/${loggedInUser.id}/details` : '/home'
]} }
/> >
</PageHeaderToolsItem> {i18n._(t`User Details`)}
</Tooltip> </DropdownItem>,
<Tooltip position="left" content={<div>{i18n._(t`User`)}</div>}> <DropdownItem
<PageHeaderToolsItem> key="logout"
<Dropdown component="button"
id="toolbar-user-dropdown" onClick={onLogoutClick}
isPlain id="logout-button"
isOpen={isUserOpen} >
position={DropdownPosition.right} {i18n._(t`Logout`)}
onSelect={this.handleUserSelect} </DropdownItem>,
toggle={ ]}
<DropdownToggle onToggle={this.handleUserToggle}> />
<UserIcon /> </PageHeaderToolsItem>
{loggedInUser && ( </Tooltip>
<span style={{ marginLeft: '10px' }}> </PageHeaderToolsGroup>
{loggedInUser.username} </PageHeaderTools>
</span> );
)}
</DropdownToggle>
}
dropdownItems={[
<DropdownItem
key="user"
href={
loggedInUser
? `/users/${loggedInUser.id}/details`
: '/home'
}
>
{i18n._(t`User Details`)}
</DropdownItem>,
<DropdownItem
key="logout"
component="button"
onClick={onLogoutClick}
id="logout-button"
>
{i18n._(t`Logout`)}
</DropdownItem>,
]}
/>
</PageHeaderToolsItem>
</Tooltip>
</PageHeaderToolsGroup>
</PageHeaderTools>
);
}
} }
PageHeaderToolbar.propTypes = { PageHeaderToolbar.propTypes = {

View File

@@ -1,4 +1,4 @@
import React, { Component, Fragment } from 'react'; import React, { useState, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
@@ -32,27 +32,15 @@ const Expandable = styled(PFExpandable)`
} }
`; `;
class ErrorDetail extends Component { function ErrorDetail({ error, i18n }) {
constructor(props) { const { response } = error;
super(props); const [isExpanded, setIsExpanded] = useState(false);
this.state = { const handleToggle = () => {
isExpanded: false, setIsExpanded(!isExpanded);
}; };
this.handleToggle = this.handleToggle.bind(this); const renderNetworkError = () => {
this.renderNetworkError = this.renderNetworkError.bind(this);
this.renderStack = this.renderStack.bind(this);
}
handleToggle() {
const { isExpanded } = this.state;
this.setState({ isExpanded: !isExpanded });
}
renderNetworkError() {
const { error } = this.props;
const { response } = error;
const message = getErrorMessage(response); const message = getErrorMessage(response);
return ( return (
@@ -74,31 +62,25 @@ class ErrorDetail extends Component {
</CardBody> </CardBody>
</Fragment> </Fragment>
); );
} };
renderStack() { const renderStack = () => {
const { error } = this.props;
return <CardBody>{error.stack}</CardBody>; return <CardBody>{error.stack}</CardBody>;
} };
render() { return (
const { isExpanded } = this.state; <Expandable
const { error, i18n } = this.props; toggleText={i18n._(t`Details`)}
onToggle={handleToggle}
return ( isExpanded={isExpanded}
<Expandable >
toggleText={i18n._(t`Details`)} <Card>
onToggle={this.handleToggle} {Object.prototype.hasOwnProperty.call(error, 'response')
isExpanded={isExpanded} ? renderNetworkError()
> : renderStack()}
<Card> </Card>
{Object.prototype.hasOwnProperty.call(error, 'response') </Expandable>
? this.renderNetworkError() );
: this.renderStack()}
</Card>
</Expandable>
);
}
} }
ErrorDetail.propTypes = { ErrorDetail.propTypes = {

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import ErrorDetail from './ErrorDetail'; import ErrorDetail from './ErrorDetail';
@@ -39,7 +40,7 @@ describe('ErrorDetail', () => {
} }
/> />
); );
wrapper.find('ExpandableSection').prop('onToggle')(); act(() => wrapper.find('ExpandableSection').prop('onToggle')());
wrapper.update(); wrapper.update();
}); });
}); });

View File

@@ -1,80 +1,56 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect } from 'react';
import { Redirect, Link } from 'react-router-dom'; import { Redirect, Link } from 'react-router-dom';
import { PageSection, Card } from '@patternfly/react-core'; import { PageSection, Card } from '@patternfly/react-core';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import useRequest from '../../util/useRequest';
import { UnifiedJobsAPI } from '../../api'; import { UnifiedJobsAPI } from '../../api';
import ContentError from '../../components/ContentError'; import ContentError from '../../components/ContentError';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
const NOT_FOUND = 'not found'; const NOT_FOUND = 'not found';
class JobTypeRedirect extends Component { function JobTypeRedirect({ id, path, view, i18n }) {
static defaultProps = { const {
view: 'output', isLoading,
}; error,
result: { job },
constructor(props) { request: loadJob,
super(props); } = useRequest(
useCallback(async () => {
this.state = {
error: null,
job: null,
isLoading: true,
};
this.loadJob = this.loadJob.bind(this);
}
componentDidMount() {
this.loadJob();
}
async loadJob() {
const { id } = this.props;
this.setState({ isLoading: true });
try {
const { data } = await UnifiedJobsAPI.read({ id }); const { data } = await UnifiedJobsAPI.read({ id });
const job = data.results[0]; return { job: data };
this.setState({ }, [id]),
job, { job: {} }
isLoading: false, );
error: job ? null : NOT_FOUND, useEffect(() => {
}); loadJob();
} catch (error) { }, [loadJob]);
this.setState({
error,
isLoading: false,
});
}
}
render() { if (error) {
const { path, view, i18n } = this.props; return (
const { error, job, isLoading } = this.state; <PageSection>
<Card>
if (error) { {error === NOT_FOUND ? (
return ( <ContentError isNotFound>
<PageSection> <Link to="/jobs">{i18n._(t`View all Jobs`)}</Link>
<Card> </ContentError>
{error === NOT_FOUND ? ( ) : (
<ContentError isNotFound> <ContentError error={error} />
<Link to="/jobs">{i18n._(t`View all Jobs`)}</Link> )}
</ContentError> </Card>
) : ( </PageSection>
<ContentError error={error} /> );
)}
</Card>
</PageSection>
);
}
if (isLoading) {
// TODO show loading state
return <div>Loading...</div>;
}
const type = JOB_TYPE_URL_SEGMENTS[job.type];
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
} }
if (isLoading) {
// TODO show loading state
return <div>Loading...</div>;
}
const type = JOB_TYPE_URL_SEGMENTS[job.type];
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
} }
JobTypeRedirect.defaultProps = {
view: 'output',
};
export default withI18n()(JobTypeRedirect); export default withI18n()(JobTypeRedirect);

View File

@@ -1,21 +1,17 @@
import React, { Component, Fragment } from 'react'; import React, { Fragment } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import Breadcrumbs from '../../components/Breadcrumbs'; import Breadcrumbs from '../../components/Breadcrumbs';
class ManagementJobs extends Component { function ManagementJobs({ i18n }) {
render() { return (
const { i18n } = this.props; <Fragment>
<Breadcrumbs
return ( breadcrumbConfig={{ '/management_jobs': i18n._(t`Management Jobs`) }}
<Fragment> />
<Breadcrumbs </Fragment>
breadcrumbConfig={{ '/management_jobs': i18n._(t`Management Jobs`) }} );
/>
</Fragment>
);
}
} }
export default withI18n()(ManagementJobs); export default withI18n()(ManagementJobs);

View File

@@ -1,5 +1,5 @@
import React, { Component, Fragment } from 'react'; import React, { useState, Fragment } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom'; import { Route, withRouter, Switch, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -10,28 +10,19 @@ import OrganizationsList from './OrganizationList/OrganizationList';
import OrganizationAdd from './OrganizationAdd/OrganizationAdd'; import OrganizationAdd from './OrganizationAdd/OrganizationAdd';
import Organization from './Organization'; import Organization from './Organization';
class Organizations extends Component { function Organizations({ i18n }) {
constructor(props) { const match = useRouteMatch();
super(props); const [breadcrumbConfig, setBreadcrumbConfig] = useState({
'/organizations': i18n._(t`Organizations`),
const { i18n } = props; '/organizations/add': i18n._(t`Create New Organization`),
});
this.state = {
breadcrumbConfig: {
'/organizations': i18n._(t`Organizations`),
'/organizations/add': i18n._(t`Create New Organization`),
},
};
}
setBreadcrumbConfig = organization => {
const { i18n } = this.props;
const setBreadcrumb = organization => {
if (!organization) { if (!organization) {
return; return;
} }
const breadcrumbConfig = { const breadcrumb = {
'/organizations': i18n._(t`Organizations`), '/organizations': i18n._(t`Organizations`),
'/organizations/add': i18n._(t`Create New Organization`), '/organizations/add': i18n._(t`Create New Organization`),
[`/organizations/${organization.id}`]: `${organization.name}`, [`/organizations/${organization.id}`]: `${organization.name}`,
@@ -43,38 +34,29 @@ class Organizations extends Component {
t`Notifications` t`Notifications`
), ),
}; };
setBreadcrumbConfig(breadcrumb);
this.setState({ breadcrumbConfig });
}; };
render() { return (
const { match } = this.props; <Fragment>
const { breadcrumbConfig } = this.state; <Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
return ( <Route path={`${match.path}/add`}>
<Fragment> <OrganizationAdd />
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} /> </Route>
<Switch> <Route path={`${match.path}/:id`}>
<Route path={`${match.path}/add`}> <Config>
<OrganizationAdd /> {({ me }) => (
</Route> <Organization setBreadcrumb={setBreadcrumb} me={me || {}} />
<Route path={`${match.path}/:id`}> )}
<Config> </Config>
{({ me }) => ( </Route>
<Organization <Route path={`${match.path}`}>
setBreadcrumb={this.setBreadcrumbConfig} <OrganizationsList />
me={me || {}} </Route>
/> </Switch>
)} </Fragment>
</Config> );
</Route>
<Route path={`${match.path}`}>
<OrganizationsList />
</Route>
</Switch>
</Fragment>
);
}
} }
export { Organizations as _Organizations }; export { Organizations as _Organizations };

View File

@@ -137,10 +137,10 @@ function Project({ i18n, setBreadcrumb }) {
); );
} }
let showCardHeader = true; const showCardHeader = !(
if (['edit', 'schedules/'].some(name => location.pathname.includes(name))) { location.pathname.endsWith('edit') ||
showCardHeader = false; location.pathname.includes('schedules/')
} );
return ( return (
<PageSection> <PageSection>

View File

@@ -11,6 +11,14 @@ import mockDetails from './data.project.json';
import Project from './Project'; import Project from './Project';
jest.mock('../../api'); jest.mock('../../api');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useRouteMatch: () => ({
pathname: '/projects/1/details',
url: '/projects/1',
}),
useParams: () => ({ id: 1 }),
}));
const mockMe = { const mockMe = {
is_super_user: true, is_super_user: true,
@@ -50,7 +58,7 @@ describe('<Project />', () => {
}); });
const tabs = await waitForElement( const tabs = await waitForElement(
wrapper, wrapper,
'.pf-c-tabs__item', '.pf-c-tabs__item-text',
el => el.length === 6 el => el.length === 6
); );
expect(tabs.at(3).text()).toEqual('Notifications'); expect(tabs.at(3).text()).toEqual('Notifications');
@@ -71,7 +79,7 @@ describe('<Project />', () => {
}); });
const tabs = await waitForElement( const tabs = await waitForElement(
wrapper, wrapper,
'.pf-c-tabs__item', '.pf-c-tabs__item-text',
el => el.length === 5 el => el.length === 5
); );
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications')); tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
@@ -91,7 +99,6 @@ describe('<Project />', () => {
<Project setBreadcrumb={() => {}} me={mockMe} /> <Project setBreadcrumb={() => {}} me={mockMe} />
); );
}); });
const tabs = await waitForElement( const tabs = await waitForElement(
wrapper, wrapper,
'.pf-c-tabs__item', '.pf-c-tabs__item',
@@ -115,7 +122,6 @@ describe('<Project />', () => {
<Project setBreadcrumb={() => {}} me={mockMe} /> <Project setBreadcrumb={() => {}} me={mockMe} />
); );
}); });
const tabs = await waitForElement( const tabs = await waitForElement(
wrapper, wrapper,
'.pf-c-tabs__item', '.pf-c-tabs__item',

View File

@@ -1,89 +1,23 @@
/* eslint react/no-unused-state: 0 */ /* eslint react/no-unused-state: 0 */
import React, { Component } from 'react'; import React, { useState } from 'react';
import { withRouter, Redirect } from 'react-router-dom'; import { withRouter, Redirect, useHistory } from 'react-router-dom';
import { CardBody } from '../../../components/Card'; import { CardBody } from '../../../components/Card';
import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import { JobTemplatesAPI } from '../../../api'; import { JobTemplatesAPI } from '../../../api';
import { JobTemplate } from '../../../types'; import { JobTemplate } from '../../../types';
import { getAddedAndRemoved } from '../../../util/lists'; import { getAddedAndRemoved } from '../../../util/lists';
import JobTemplateForm from '../shared/JobTemplateForm'; import JobTemplateForm from '../shared/JobTemplateForm';
class JobTemplateEdit extends Component { import ContentLoading from '../../../components/ContentLoading';
static propTypes = {
template: JobTemplate.isRequired,
};
constructor(props) { function JobTemplateEdit({ template }) {
super(props); const history = useHistory();
const [formSubmitError, setFormSubmitError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
this.state = { const detailsUrl = `/templates/${template.type}/${template.id}/details`;
hasContentLoading: true,
contentError: null,
formSubmitError: null,
relatedCredentials: [],
relatedProjectPlaybooks: [],
};
const { const handleSubmit = async values => {
template: { id, type },
} = props;
this.detailsUrl = `/templates/${type}/${id}/details`;
this.handleCancel = this.handleCancel.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.loadRelatedCredentials = this.loadRelatedCredentials.bind(this);
this.submitLabels = this.submitLabels.bind(this);
}
componentDidMount() {
this.loadRelated();
}
async loadRelated() {
this.setState({ contentError: null, hasContentLoading: true });
try {
const [relatedCredentials] = await this.loadRelatedCredentials();
this.setState({
relatedCredentials,
});
} catch (contentError) {
this.setState({ contentError });
} finally {
this.setState({ hasContentLoading: false });
}
}
async loadRelatedCredentials() {
const {
template: { id },
} = this.props;
const params = {
page: 1,
page_size: 200,
order_by: 'name',
};
try {
const {
data: { results: credentials = [] },
} = await JobTemplatesAPI.readCredentials(id, params);
return credentials;
} catch (err) {
if (err.status !== 403) throw err;
this.setState({ hasRelatedCredentialAccess: false });
const {
template: {
summary_fields: { credentials = [] },
},
} = this.props;
return credentials;
}
}
async handleSubmit(values) {
const { template, history } = this.props;
const { const {
labels, labels,
instanceGroups, instanceGroups,
@@ -95,25 +29,26 @@ class JobTemplateEdit extends Component {
...remainingValues ...remainingValues
} = values; } = values;
this.setState({ formSubmitError: null }); setFormSubmitError(null);
setIsLoading(true);
remainingValues.project = values.project.id; remainingValues.project = values.project.id;
remainingValues.webhook_credential = webhook_credential?.id || null; remainingValues.webhook_credential = webhook_credential?.id || null;
try { try {
await JobTemplatesAPI.update(template.id, remainingValues); await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([ await Promise.all([
this.submitLabels(labels, template?.organization), submitLabels(labels, template?.organization),
this.submitInstanceGroups(instanceGroups, initialInstanceGroups), submitInstanceGroups(instanceGroups, initialInstanceGroups),
this.submitCredentials(credentials), submitCredentials(credentials),
]); ]);
history.push(this.detailsUrl); history.push(detailsUrl);
} catch (error) { } catch (error) {
this.setState({ formSubmitError: error }); setFormSubmitError(error);
} finally {
setIsLoading(false);
} }
} };
async submitLabels(labels = [], orgId) {
const { template } = this.props;
const submitLabels = async (labels = [], orgId) => {
const { added, removed } = getAddedAndRemoved( const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results, template.summary_fields.labels.results,
labels labels
@@ -131,10 +66,9 @@ class JobTemplateEdit extends Component {
...associationPromises, ...associationPromises,
]); ]);
return results; return results;
} };
async submitInstanceGroups(groups, initialGroups) { const submitInstanceGroups = async (groups, initialGroups) => {
const { template } = this.props;
const { added, removed } = getAddedAndRemoved(initialGroups, groups); const { added, removed } = getAddedAndRemoved(initialGroups, groups);
const disassociatePromises = await removed.map(group => const disassociatePromises = await removed.map(group =>
JobTemplatesAPI.disassociateInstanceGroup(template.id, group.id) JobTemplatesAPI.disassociateInstanceGroup(template.id, group.id)
@@ -143,10 +77,9 @@ class JobTemplateEdit extends Component {
JobTemplatesAPI.associateInstanceGroup(template.id, group.id) JobTemplatesAPI.associateInstanceGroup(template.id, group.id)
); );
return Promise.all([...disassociatePromises, ...associatePromises]); return Promise.all([...disassociatePromises, ...associatePromises]);
} };
async submitCredentials(newCredentials) { const submitCredentials = async newCredentials => {
const { template } = this.props;
const { added, removed } = getAddedAndRemoved( const { added, removed } = getAddedAndRemoved(
template.summary_fields.credentials, template.summary_fields.credentials,
newCredentials newCredentials
@@ -160,55 +93,34 @@ class JobTemplateEdit extends Component {
); );
const associatePromise = Promise.all(associateCredentials); const associatePromise = Promise.all(associateCredentials);
return Promise.all([disassociatePromise, associatePromise]); return Promise.all([disassociatePromise, associatePromise]);
};
const handleCancel = () => {
history.push(detailsUrl);
};
const canEdit = template?.summary_fields?.user_capabilities?.edit;
if (!canEdit) {
return <Redirect to={detailsUrl} />;
} }
if (isLoading) {
handleCancel() { return <ContentLoading />;
const { history } = this.props;
history.push(this.detailsUrl);
}
render() {
const { template } = this.props;
const {
contentError,
formSubmitError,
hasContentLoading,
relatedProjectPlaybooks,
} = this.state;
const canEdit = template.summary_fields.user_capabilities.edit;
if (hasContentLoading) {
return (
<CardBody>
<ContentLoading />
</CardBody>
);
}
if (contentError) {
return (
<CardBody>
<ContentError error={contentError} />
</CardBody>
);
}
if (!canEdit) {
return <Redirect to={this.detailsUrl} />;
}
return (
<CardBody>
<JobTemplateForm
template={template}
handleCancel={this.handleCancel}
handleSubmit={this.handleSubmit}
relatedProjectPlaybooks={relatedProjectPlaybooks}
submitError={formSubmitError}
/>
</CardBody>
);
} }
return (
<CardBody>
<JobTemplateForm
template={template}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
/>
</CardBody>
);
} }
JobTemplateEdit.propTypes = {
template: JobTemplate.isRequired,
};
export default withRouter(JobTemplateEdit); export default withRouter(JobTemplateEdit);

View File

@@ -13,6 +13,7 @@ import {
useRouteMatch, useRouteMatch,
} from 'react-router-dom'; } from 'react-router-dom';
import RoutedTabs from '../../components/RoutedTabs'; import RoutedTabs from '../../components/RoutedTabs';
import { useConfig } from '../../contexts/Config';
import useRequest from '../../util/useRequest'; import useRequest from '../../util/useRequest';
import ContentError from '../../components/ContentError'; import ContentError from '../../components/ContentError';
import JobList from '../../components/JobList'; import JobList from '../../components/JobList';
@@ -24,15 +25,16 @@ import JobTemplateEdit from './JobTemplateEdit';
import { JobTemplatesAPI, OrganizationsAPI } from '../../api'; import { JobTemplatesAPI, OrganizationsAPI } from '../../api';
import TemplateSurvey from './TemplateSurvey'; import TemplateSurvey from './TemplateSurvey';
function Template({ i18n, me, setBreadcrumb }) { function Template({ i18n, setBreadcrumb }) {
const match = useRouteMatch();
const location = useLocation(); const location = useLocation();
const { id: templateId } = useParams(); const { id: templateId } = useParams();
const match = useRouteMatch(); const { me = {} } = useConfig();
const { const {
result: { isNotifAdmin, template }, result: { isNotifAdmin, template },
isLoading: hasRolesandTemplateLoading, isLoading,
error: rolesAndTemplateError, error: contentError,
request: loadTemplateAndRoles, request: loadTemplateAndRoles,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
@@ -44,9 +46,8 @@ function Template({ i18n, me, setBreadcrumb }) {
role_level: 'notification_admin_role', role_level: 'notification_admin_role',
}), }),
]); ]);
if (actions?.data?.actions?.PUT) {
if (actions.data.actions.PUT) { if (data?.webhook_service && data?.related?.webhook_key) {
if (data.webhook_service && data?.related?.webhook_key) {
const { const {
data: { webhook_key }, data: { webhook_key },
} = await JobTemplatesAPI.readWebhookKey(templateId); } = await JobTemplatesAPI.readWebhookKey(templateId);
@@ -54,35 +55,40 @@ function Template({ i18n, me, setBreadcrumb }) {
data.webhook_key = webhook_key; data.webhook_key = webhook_key;
} }
} }
setBreadcrumb(data);
return { return {
template: data, template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0, isNotifAdmin: notifAdminRes.data.results.length > 0,
}; };
}, [setBreadcrumb, templateId]), }, [templateId]),
{ isNotifAdmin: false, template: null } { isNotifAdmin: false, template: null }
); );
useEffect(() => { useEffect(() => {
loadTemplateAndRoles(); loadTemplateAndRoles();
}, [loadTemplateAndRoles, location.pathname]); }, [loadTemplateAndRoles, location.pathname]);
useEffect(() => {
if (template) {
setBreadcrumb(template);
}
}, [template, setBreadcrumb]);
const createSchedule = data => { const createSchedule = data => {
return JobTemplatesAPI.createSchedule(templateId, data); return JobTemplatesAPI.createSchedule(template.id, data);
}; };
const loadScheduleOptions = useCallback(() => { const loadScheduleOptions = useCallback(() => {
return JobTemplatesAPI.readScheduleOptions(templateId); return JobTemplatesAPI.readScheduleOptions(template.id);
}, [templateId]); }, [template]);
const loadSchedules = useCallback( const loadSchedules = useCallback(
params => { params => {
return JobTemplatesAPI.readSchedules(templateId, params); return JobTemplatesAPI.readSchedules(template.id, params);
}, },
[templateId] [template]
); );
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin; const canSeeNotificationsTab = me?.is_system_auditor || isNotifAdmin;
const canAddAndEditSurvey = const canAddAndEditSurvey =
template?.summary_fields?.user_capabilities.edit || template?.summary_fields?.user_capabilities.edit ||
template?.summary_fields?.user_capabilities.delete; template?.summary_fields?.user_capabilities.delete;
@@ -131,17 +137,7 @@ function Template({ i18n, me, setBreadcrumb }) {
tab.id = n; tab.id = n;
}); });
let showCardHeader = true; if (contentError) {
if (
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
showCardHeader = false;
}
const contentError = rolesAndTemplateError;
if (!hasRolesandTemplateLoading && contentError) {
return ( return (
<PageSection> <PageSection>
<Card> <Card>
@@ -158,38 +154,37 @@ function Template({ i18n, me, setBreadcrumb }) {
); );
} }
const showCardHeader = !(
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
);
return ( return (
<PageSection> <PageSection>
<Card> <Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />} {showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
<Switch> {template && (
<Redirect <Switch>
from="/templates/:templateType/:id" <Redirect
to="/templates/:templateType/:id/details" from="/templates/:templateType/:id"
exact to="/templates/:templateType/:id/details"
/> exact
{template && ( />
<Route key="details" path="/templates/:templateType/:id/details"> <Route key="details" path="/templates/:templateType/:id/details">
<JobTemplateDetail <JobTemplateDetail
hasTemplateLoading={hasRolesandTemplateLoading} hasTemplateLoading={isLoading}
template={template} template={template}
/> />
</Route> </Route>
)}
{template && (
<Route key="edit" path="/templates/:templateType/:id/edit"> <Route key="edit" path="/templates/:templateType/:id/edit">
<JobTemplateEdit template={template} /> <JobTemplateEdit template={template} />
</Route> </Route>
)}
{template && (
<Route key="access" path="/templates/:templateType/:id/access"> <Route key="access" path="/templates/:templateType/:id/access">
<ResourceAccessList <ResourceAccessList
resource={template} resource={template}
apiModel={JobTemplatesAPI} apiModel={JobTemplatesAPI}
/> />
</Route> </Route>
)}
{template && (
<Route <Route
key="schedules" key="schedules"
path="/templates/:templateType/:id/schedules" path="/templates/:templateType/:id/schedules"
@@ -202,43 +197,39 @@ function Template({ i18n, me, setBreadcrumb }) {
loadScheduleOptions={loadScheduleOptions} loadScheduleOptions={loadScheduleOptions}
/> />
</Route> </Route>
)} {canSeeNotificationsTab && (
{canSeeNotificationsTab && ( <Route path="/templates/:templateType/:id/notifications">
<Route path="/templates/:templateType/:id/notifications"> <NotificationList
<NotificationList id={Number(templateId)}
id={Number(templateId)} canToggleNotifications={isNotifAdmin}
canToggleNotifications={isNotifAdmin} apiModel={JobTemplatesAPI}
apiModel={JobTemplatesAPI} />
/> </Route>
</Route> )}
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs"> <Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} /> <JobList defaultParams={{ job__job_template: template.id }} />
</Route> </Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey"> <Route path="/templates/:templateType/:id/survey">
<TemplateSurvey <TemplateSurvey
template={template} template={template}
canEdit={canAddAndEditSurvey} canEdit={canAddAndEditSurvey}
/> />
</Route> </Route>
)} {!isLoading && (
{!hasRolesandTemplateLoading && ( <Route key="not-found" path="*">
<Route key="not-found" path="*"> <ContentError isNotFound>
<ContentError isNotFound> {match.params.id && (
{match.params.id && ( <Link
<Link to={`/templates/${match?.params?.templateType}/${templateId}/details`}
to={`/templates/${match.params.templateType}/${match.params.id}/details`} >
> {i18n._(t`View Template Details`)}
{i18n._(t`View Template Details`)} </Link>
</Link> )}
)} </ContentError>
</ContentError> </Route>
</Route> )}
)} </Switch>
</Switch> )}
</Card> </Card>
</PageSection> </PageSection>
); );

View File

@@ -1,10 +1,9 @@
import React, { Component } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Route, withRouter, Switch } from 'react-router-dom'; import { Route, withRouter, Switch } from 'react-router-dom';
import { PageSection } from '@patternfly/react-core'; import { PageSection } from '@patternfly/react-core';
import { Config } from '../../contexts/Config';
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
import { TemplateList } from './TemplateList'; import { TemplateList } from './TemplateList';
import Template from './Template'; import Template from './Template';
@@ -12,123 +11,66 @@ import WorkflowJobTemplate from './WorkflowJobTemplate';
import JobTemplateAdd from './JobTemplateAdd'; import JobTemplateAdd from './JobTemplateAdd';
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd'; import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
class Templates extends Component { function Templates({ i18n }) {
constructor(props) { const initBreadcrumbs = useRef({
super(props); '/templates': i18n._(t`Templates`),
const { i18n } = this.props; '/templates/job_template/add': i18n._(t`Create New Job Template`),
'/templates/workflow_job_template/add': i18n._(
t`Create New Workflow Template`
),
});
const [breadcrumbConfig, setBreadcrumbs] = useState(initBreadcrumbs.current);
const setBreadcrumbConfig = useCallback(
(template, schedule) => {
if (!template) return;
const templatePath = `/templates/${template.type}/${template.id}`;
const schedulesPath = `${templatePath}/schedules`;
const surveyPath = `${templatePath}/survey`;
setBreadcrumbs({
...initBreadcrumbs.current,
[templatePath]: `${template.name}`,
[`${templatePath}/details`]: i18n._(t`Details`),
[`${templatePath}/edit`]: i18n._(t`Edit Details`),
[`${templatePath}/access`]: i18n._(t`Access`),
[`${templatePath}/notifications`]: i18n._(t`Notifications`),
[`${templatePath}/completed_jobs`]: i18n._(t`Completed Jobs`),
[surveyPath]: i18n._(t`Survey`),
[`${surveyPath}add`]: i18n._(t`Add Question`),
[`${surveyPath}/edit`]: i18n._(t`Edit Question`),
[schedulesPath]: i18n._(t`Schedules`),
[`${schedulesPath}/add`]: i18n._(t`Create New Schedule`),
[`${schedulesPath}/${schedule?.id}`]: `${schedule?.name}`,
[`${schedulesPath}/details`]: i18n._(t`Schedule Details`),
[`${schedulesPath}/edit`]: i18n._(t`Edit Details`),
});
},
[i18n]
);
this.state = { return (
breadcrumbConfig: { <>
'/templates': i18n._(t`Templates`), <Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
'/templates/job_template/add': i18n._(t`Create New Job Template`), <Switch>
'/templates/workflow_job_template/add': i18n._( <Route path="/templates/job_template/add">
t`Create New Workflow Template` <JobTemplateAdd />
), </Route>
}, <Route path="/templates/workflow_job_template/add">
}; <WorkflowJobTemplateAdd />
} </Route>
<Route path="/templates/job_template/:id">
setBreadCrumbConfig = (template, schedule) => { <Template setBreadcrumb={setBreadcrumbConfig} />
const { i18n } = this.props; </Route>
if (!template) { <Route path="/templates/workflow_job_template/:id">
return; <WorkflowJobTemplate setBreadcrumb={setBreadcrumbConfig} />
} </Route>
const breadcrumbConfig = { <Route path="/templates">
'/templates': i18n._(t`Templates`), <PageSection>
'/templates/job_template/add': i18n._(t`Create New Job Template`), <TemplateList />
'/templates/workflow_job_template/add': i18n._( </PageSection>
t`Create New Workflow Template` </Route>
), </Switch>
[`/templates/${template.type}/${template.id}`]: `${template.name}`, </>
[`/templates/${template.type}/${template.id}/details`]: i18n._( );
t`Details`
),
[`/templates/${template.type}/${template.id}/edit`]: i18n._(
t`Edit Details`
),
[`/templates/${template.type}/${template.id}/access`]: i18n._(t`Access`),
[`/templates/${template.type}/${template.id}/notifications`]: i18n._(
t`Notifications`
),
[`/templates/${template.type}/${template.id}/completed_jobs`]: i18n._(
t`Completed Jobs`
),
[`/templates/${template.type}/${template.id}/survey`]: i18n._(t`Survey`),
[`/templates/${template.type}/${template.id}/survey/add`]: i18n._(
t`Add Question`
),
[`/templates/${template.type}/${template.id}/survey/edit`]: i18n._(
t`Edit Question`
),
[`/templates/${template.type}/${template.id}/schedules`]: i18n._(
t`Schedules`
),
[`/templates/${template.type}/${template.id}/schedules/add`]: i18n._(
t`Create New Schedule`
),
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
schedule.id}`]: `${schedule && schedule.name}`,
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
schedule.id}/details`]: i18n._(t`Schedule Details`),
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
schedule.id}/edit`]: i18n._(t`Edit Details`),
};
this.setState({ breadcrumbConfig });
};
render() {
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return (
<>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path={`${match.path}/job_template/add`}>
<JobTemplateAdd />
</Route>
<Route path={`${match.path}/workflow_job_template/add`}>
<WorkflowJobTemplateAdd />
</Route>
<Route
path={`${match.path}/job_template/:id`}
render={({ match: newRouteMatch }) => (
<Config>
{({ me }) => (
<Template
history={history}
location={location}
setBreadcrumb={this.setBreadCrumbConfig}
me={me || {}}
match={newRouteMatch}
/>
)}
</Config>
)}
/>
<Route
path={`${match.path}/workflow_job_template/:id`}
render={({ match: newRouteMatch }) => (
<Config>
{({ me }) => (
<WorkflowJobTemplate
location={location}
setBreadcrumb={this.setBreadCrumbConfig}
me={me || {}}
match={newRouteMatch}
/>
)}
</Config>
)}
/>
<Route path={`${match.path}`}>
<PageSection>
<TemplateList />
</PageSection>
</Route>
</Switch>
</>
);
}
} }
export { Templates as _Templates }; export { Templates as _Templates };

View File

@@ -13,6 +13,7 @@ import {
useRouteMatch, useRouteMatch,
} from 'react-router-dom'; } from 'react-router-dom';
import RoutedTabs from '../../components/RoutedTabs'; import RoutedTabs from '../../components/RoutedTabs';
import { useConfig } from '../../contexts/Config';
import useRequest from '../../util/useRequest'; import useRequest from '../../util/useRequest';
import AppendBody from '../../components/AppendBody'; import AppendBody from '../../components/AppendBody';
import ContentError from '../../components/ContentError'; import ContentError from '../../components/ContentError';
@@ -27,10 +28,11 @@ import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api';
import TemplateSurvey from './TemplateSurvey'; import TemplateSurvey from './TemplateSurvey';
import { Visualizer } from './WorkflowJobTemplateVisualizer'; import { Visualizer } from './WorkflowJobTemplateVisualizer';
function WorkflowJobTemplate({ i18n, me, setBreadcrumb }) { function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
const location = useLocation(); const location = useLocation();
const { id: templateId } = useParams();
const match = useRouteMatch(); const match = useRouteMatch();
const { id: templateId } = useParams();
const { me = {} } = useConfig();
const { const {
result: { isNotifAdmin, template }, result: { isNotifAdmin, template },

View File

@@ -1,27 +1,24 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { mountWithContexts, waitForElement } from './enzymeHelpers'; import { mountWithContexts, waitForElement } from './enzymeHelpers';
import { Config } from '../src/contexts/Config'; import { Config } from '../src/contexts/Config';
describe('mountWithContexts', () => { describe('mountWithContexts', () => {
describe('injected I18nProvider', () => { describe('injected I18nProvider', () => {
test('should mount and render', () => { test('should mount and render', () => {
const Child = withI18n()(({ i18n }) => ( const Child = () => (
<div> <div>
<span>{i18n._(t`Text content`)}</span> <span>Text content</span>
</div> </div>
)); );
const wrapper = mountWithContexts(<Child />); const wrapper = mountWithContexts(<Child />);
expect(wrapper.find('div')).toMatchSnapshot(); expect(wrapper.find('div')).toMatchSnapshot();
}); });
test('should mount and render deeply nested consumer', () => { test('should mount and render deeply nested consumer', () => {
const Child = withI18n()(({ i18n }) => ( const Child = () => <div>Text content</div>;
<div>{i18n._(t`Text content`)}</div>
));
const Parent = () => <Child />; const Parent = () => <Child />;
const wrapper = mountWithContexts(<Parent />); const wrapper = mountWithContexts(<Parent />);
expect(wrapper.find('Parent')).toMatchSnapshot(); expect(wrapper.find('Parent')).toMatchSnapshot();
@@ -146,7 +143,9 @@ describe('waitForElement', () => {
} catch (err) { } catch (err) {
error = err; error = err;
} finally { } finally {
expect(error.message).toContain('Expected condition for <#does-not-exist> not met'); expect(error.message).toContain(
'Expected condition for <#does-not-exist> not met'
);
expect(error.message).toContain('el.length === 1'); expect(error.message).toContain('el.length === 1');
done(); done();
} }