diff --git a/awx/ui_next/src/components/About/About.jsx b/awx/ui_next/src/components/About/About.jsx
index 8444797830..f68f75b613 100644
--- a/awx/ui_next/src/components/About/About.jsx
+++ b/awx/ui_next/src/components/About/About.jsx
@@ -12,8 +12,8 @@ import {
import { BrandName } from '../../variables';
import brandLogoImg from './brand-logo.svg';
-class About extends React.Component {
- static createSpeechBubble(version) {
+function About({ ansible_version, version, isOpen, onClose, i18n }) {
+ const createSpeechBubble = () => {
let text = `${BrandName} ${version}`;
let top = '';
let bottom = '';
@@ -28,31 +28,22 @@ class About extends React.Component {
bottom = ` --${bottom}-- `;
return top + text + bottom;
- }
+ };
- constructor(props) {
- super(props);
+ const speechBubble = createSpeechBubble();
- this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
- }
-
- render() {
- const { ansible_version, version, isOpen, onClose, i18n } = this.props;
-
- const speechBubble = this.createSpeechBubble(version);
-
- return (
-
-
- {speechBubble}
- {`
+ return (
+
+
+ {speechBubble}
+ {`
\\
\\ ^__^
(oo)\\_______
@@ -60,18 +51,17 @@ class About extends React.Component {
||----w |
|| ||
`}
-
-
-
-
- {i18n._(t`Ansible Version`)}
-
- {ansible_version}
-
-
-
- );
- }
+
+
+
+
+ {i18n._(t`Ansible Version`)}
+
+ {ansible_version}
+
+
+
+ );
}
About.propTypes = {
diff --git a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx
index f09bdbdf3f..4d31af328e 100644
--- a/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx
+++ b/awx/ui_next/src/components/AppContainer/PageHeaderToolbar.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -17,129 +17,100 @@ import { QuestionCircleIcon, UserIcon } from '@patternfly/react-icons';
const DOCLINK =
'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html';
-class PageHeaderToolbar extends Component {
- constructor(props) {
- super(props);
- this.state = {
- isHelpOpen: false,
- isUserOpen: false,
- };
+function PageHeaderToolbar({
+ isAboutDisabled,
+ onAboutClick,
+ onLogoutClick,
+ loggedInUser,
+ i18n,
+}) {
+ const [isHelpOpen, setIsHelpOpen] = useState(false);
+ const [isUserOpen, setIsUserOpen] = useState(false);
- this.handleHelpSelect = this.handleHelpSelect.bind(this);
- this.handleHelpToggle = this.handleHelpToggle.bind(this);
- this.handleUserSelect = this.handleUserSelect.bind(this);
- this.handleUserToggle = this.handleUserToggle.bind(this);
- }
+ const handleHelpSelect = () => {
+ setIsHelpOpen(!isHelpOpen);
+ };
- handleHelpSelect() {
- const { isHelpOpen } = this.state;
+ const handleUserSelect = () => {
+ setIsUserOpen(!isUserOpen);
+ };
- this.setState({ isHelpOpen: !isHelpOpen });
- }
-
- handleUserSelect() {
- const { isUserOpen } = this.state;
-
- this.setState({ isUserOpen: !isUserOpen });
- }
-
- handleHelpToggle(isOpen) {
- this.setState({ isHelpOpen: isOpen });
- }
-
- handleUserToggle(isOpen) {
- this.setState({ isUserOpen: isOpen });
- }
-
- render() {
- const { isHelpOpen, isUserOpen } = this.state;
- const {
- isAboutDisabled,
- onAboutClick,
- onLogoutClick,
- loggedInUser,
- i18n,
- } = this.props;
-
- return (
-
-
- {i18n._(t`Info`)}}>
-
-
-
-
- }
- dropdownItems={[
-
- {i18n._(t`Help`)}
- ,
-
- {i18n._(t`About`)}
- ,
- ]}
- />
-
-
- {i18n._(t`User`)}}>
-
-
-
- {loggedInUser && (
-
- {loggedInUser.username}
-
- )}
-
- }
- dropdownItems={[
-
- {i18n._(t`User Details`)}
- ,
-
- {i18n._(t`Logout`)}
- ,
- ]}
- />
-
-
-
-
- );
- }
+ return (
+
+
+ {i18n._(t`Info`)}}>
+
+
+
+
+ }
+ dropdownItems={[
+
+ {i18n._(t`Help`)}
+ ,
+
+ {i18n._(t`About`)}
+ ,
+ ]}
+ />
+
+
+ {i18n._(t`User`)}}>
+
+
+
+ {loggedInUser && (
+
+ {loggedInUser.username}
+
+ )}
+
+ }
+ dropdownItems={[
+
+ {i18n._(t`User Details`)}
+ ,
+
+ {i18n._(t`Logout`)}
+ ,
+ ]}
+ />
+
+
+
+
+ );
}
PageHeaderToolbar.propTypes = {
diff --git a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx
index a6f5b5d8ea..3b05ba47d5 100644
--- a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx
+++ b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.jsx
@@ -1,4 +1,4 @@
-import React, { Component, Fragment } from 'react';
+import React, { useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { withI18n } from '@lingui/react';
@@ -32,27 +32,15 @@ const Expandable = styled(PFExpandable)`
}
`;
-class ErrorDetail extends Component {
- constructor(props) {
- super(props);
+function ErrorDetail({ error, i18n }) {
+ const { response } = error;
+ const [isExpanded, setIsExpanded] = useState(false);
- this.state = {
- isExpanded: false,
- };
+ const handleToggle = () => {
+ setIsExpanded(!isExpanded);
+ };
- this.handleToggle = this.handleToggle.bind(this);
- 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 renderNetworkError = () => {
const message = getErrorMessage(response);
return (
@@ -74,31 +62,25 @@ class ErrorDetail extends Component {
);
- }
+ };
- renderStack() {
- const { error } = this.props;
+ const renderStack = () => {
return {error.stack};
- }
+ };
- render() {
- const { isExpanded } = this.state;
- const { error, i18n } = this.props;
-
- return (
-
-
- {Object.prototype.hasOwnProperty.call(error, 'response')
- ? this.renderNetworkError()
- : this.renderStack()}
-
-
- );
- }
+ return (
+
+
+ {Object.prototype.hasOwnProperty.call(error, 'response')
+ ? renderNetworkError()
+ : renderStack()}
+
+
+ );
}
ErrorDetail.propTypes = {
diff --git a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx
index f2d87cc515..1816f4a2dd 100644
--- a/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx
+++ b/awx/ui_next/src/components/ErrorDetail/ErrorDetail.test.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import ErrorDetail from './ErrorDetail';
@@ -39,7 +40,7 @@ describe('ErrorDetail', () => {
}
/>
);
- wrapper.find('ExpandableSection').prop('onToggle')();
+ act(() => wrapper.find('ExpandableSection').prop('onToggle')());
wrapper.update();
});
});
diff --git a/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx b/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx
index 2c4115ff5d..bf36a1a14c 100644
--- a/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx
+++ b/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx
@@ -1,80 +1,56 @@
-import React, { Component } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { Redirect, Link } from 'react-router-dom';
import { PageSection, Card } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-
+import useRequest from '../../util/useRequest';
import { UnifiedJobsAPI } from '../../api';
import ContentError from '../../components/ContentError';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
const NOT_FOUND = 'not found';
-class JobTypeRedirect extends Component {
- static defaultProps = {
- view: 'output',
- };
-
- constructor(props) {
- super(props);
-
- 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 {
+function JobTypeRedirect({ id, path, view, i18n }) {
+ const {
+ isLoading,
+ error,
+ result: { job },
+ request: loadJob,
+ } = useRequest(
+ useCallback(async () => {
const { data } = await UnifiedJobsAPI.read({ id });
- const job = data.results[0];
- this.setState({
- job,
- isLoading: false,
- error: job ? null : NOT_FOUND,
- });
- } catch (error) {
- this.setState({
- error,
- isLoading: false,
- });
- }
- }
+ return { job: data };
+ }, [id]),
+ { job: {} }
+ );
+ useEffect(() => {
+ loadJob();
+ }, [loadJob]);
- render() {
- const { path, view, i18n } = this.props;
- const { error, job, isLoading } = this.state;
-
- if (error) {
- return (
-
-
- {error === NOT_FOUND ? (
-
- {i18n._(t`View all Jobs`)}
-
- ) : (
-
- )}
-
-
- );
- }
- if (isLoading) {
- // TODO show loading state
- return
Loading...
;
- }
- const type = JOB_TYPE_URL_SEGMENTS[job.type];
- return ;
+ if (error) {
+ return (
+
+
+ {error === NOT_FOUND ? (
+
+ {i18n._(t`View all Jobs`)}
+
+ ) : (
+
+ )}
+
+
+ );
}
+ if (isLoading) {
+ // TODO show loading state
+ return Loading...
;
+ }
+ const type = JOB_TYPE_URL_SEGMENTS[job.type];
+ return ;
}
+JobTypeRedirect.defaultProps = {
+ view: 'output',
+};
export default withI18n()(JobTypeRedirect);
diff --git a/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx b/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx
index 011d9961ce..774eb3e235 100644
--- a/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx
+++ b/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx
@@ -1,21 +1,17 @@
-import React, { Component, Fragment } from 'react';
+import React, { Fragment } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import Breadcrumbs from '../../components/Breadcrumbs';
-class ManagementJobs extends Component {
- render() {
- const { i18n } = this.props;
-
- return (
-
-
-
- );
- }
+function ManagementJobs({ i18n }) {
+ return (
+
+
+
+ );
}
export default withI18n()(ManagementJobs);
diff --git a/awx/ui_next/src/screens/Organization/Organizations.jsx b/awx/ui_next/src/screens/Organization/Organizations.jsx
index 8ed5f21f1d..de46e1faae 100644
--- a/awx/ui_next/src/screens/Organization/Organizations.jsx
+++ b/awx/ui_next/src/screens/Organization/Organizations.jsx
@@ -1,5 +1,5 @@
-import React, { Component, Fragment } from 'react';
-import { Route, withRouter, Switch } from 'react-router-dom';
+import React, { useState, Fragment } from 'react';
+import { Route, withRouter, Switch, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -10,28 +10,19 @@ import OrganizationsList from './OrganizationList/OrganizationList';
import OrganizationAdd from './OrganizationAdd/OrganizationAdd';
import Organization from './Organization';
-class Organizations extends Component {
- constructor(props) {
- super(props);
-
- const { i18n } = props;
-
- this.state = {
- breadcrumbConfig: {
- '/organizations': i18n._(t`Organizations`),
- '/organizations/add': i18n._(t`Create New Organization`),
- },
- };
- }
-
- setBreadcrumbConfig = organization => {
- const { i18n } = this.props;
+function Organizations({ i18n }) {
+ const match = useRouteMatch();
+ const [breadcrumbConfig, setBreadcrumbConfig] = useState({
+ '/organizations': i18n._(t`Organizations`),
+ '/organizations/add': i18n._(t`Create New Organization`),
+ });
+ const setBreadcrumb = organization => {
if (!organization) {
return;
}
- const breadcrumbConfig = {
+ const breadcrumb = {
'/organizations': i18n._(t`Organizations`),
'/organizations/add': i18n._(t`Create New Organization`),
[`/organizations/${organization.id}`]: `${organization.name}`,
@@ -43,38 +34,29 @@ class Organizations extends Component {
t`Notifications`
),
};
-
- this.setState({ breadcrumbConfig });
+ setBreadcrumbConfig(breadcrumb);
};
- render() {
- const { match } = this.props;
- const { breadcrumbConfig } = this.state;
-
- return (
-
-
-
-
-
-
-
-
- {({ me }) => (
-
- )}
-
-
-
-
-
-
-
- );
- }
+ return (
+
+
+
+
+
+
+
+
+ {({ me }) => (
+
+ )}
+
+
+
+
+
+
+
+ );
}
export { Organizations as _Organizations };
diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx
index cb518b7bff..f87aee15dc 100644
--- a/awx/ui_next/src/screens/Project/Project.jsx
+++ b/awx/ui_next/src/screens/Project/Project.jsx
@@ -137,10 +137,10 @@ function Project({ i18n, setBreadcrumb }) {
);
}
- let showCardHeader = true;
- if (['edit', 'schedules/'].some(name => location.pathname.includes(name))) {
- showCardHeader = false;
- }
+ const showCardHeader = !(
+ location.pathname.endsWith('edit') ||
+ location.pathname.includes('schedules/')
+ );
return (
diff --git a/awx/ui_next/src/screens/Project/Project.test.jsx b/awx/ui_next/src/screens/Project/Project.test.jsx
index 03777176e0..100cdabe97 100644
--- a/awx/ui_next/src/screens/Project/Project.test.jsx
+++ b/awx/ui_next/src/screens/Project/Project.test.jsx
@@ -11,6 +11,14 @@ import mockDetails from './data.project.json';
import Project from './Project';
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 = {
is_super_user: true,
@@ -50,7 +58,7 @@ describe('', () => {
});
const tabs = await waitForElement(
wrapper,
- '.pf-c-tabs__item',
+ '.pf-c-tabs__item-text',
el => el.length === 6
);
expect(tabs.at(3).text()).toEqual('Notifications');
@@ -71,7 +79,7 @@ describe('', () => {
});
const tabs = await waitForElement(
wrapper,
- '.pf-c-tabs__item',
+ '.pf-c-tabs__item-text',
el => el.length === 5
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
@@ -91,7 +99,6 @@ describe('', () => {
{}} me={mockMe} />
);
});
-
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
@@ -115,7 +122,6 @@ describe('', () => {
{}} me={mockMe} />
);
});
-
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
index a51bbb355a..182c4918f1 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx
@@ -1,89 +1,23 @@
/* eslint react/no-unused-state: 0 */
-import React, { Component } from 'react';
-import { withRouter, Redirect } from 'react-router-dom';
+import React, { useState } from 'react';
+import { withRouter, Redirect, useHistory } from 'react-router-dom';
import { CardBody } from '../../../components/Card';
-import ContentError from '../../../components/ContentError';
-import ContentLoading from '../../../components/ContentLoading';
+
import { JobTemplatesAPI } from '../../../api';
import { JobTemplate } from '../../../types';
import { getAddedAndRemoved } from '../../../util/lists';
import JobTemplateForm from '../shared/JobTemplateForm';
-class JobTemplateEdit extends Component {
- static propTypes = {
- template: JobTemplate.isRequired,
- };
+import ContentLoading from '../../../components/ContentLoading';
- constructor(props) {
- super(props);
+function JobTemplateEdit({ template }) {
+ const history = useHistory();
+ const [formSubmitError, setFormSubmitError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
- this.state = {
- hasContentLoading: true,
- contentError: null,
- formSubmitError: null,
- relatedCredentials: [],
- relatedProjectPlaybooks: [],
- };
+ const detailsUrl = `/templates/${template.type}/${template.id}/details`;
- const {
- 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 handleSubmit = async values => {
const {
labels,
instanceGroups,
@@ -95,25 +29,26 @@ class JobTemplateEdit extends Component {
...remainingValues
} = values;
- this.setState({ formSubmitError: null });
+ setFormSubmitError(null);
+ setIsLoading(true);
remainingValues.project = values.project.id;
remainingValues.webhook_credential = webhook_credential?.id || null;
try {
await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([
- this.submitLabels(labels, template?.organization),
- this.submitInstanceGroups(instanceGroups, initialInstanceGroups),
- this.submitCredentials(credentials),
+ submitLabels(labels, template?.organization),
+ submitInstanceGroups(instanceGroups, initialInstanceGroups),
+ submitCredentials(credentials),
]);
- history.push(this.detailsUrl);
+ history.push(detailsUrl);
} 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(
template.summary_fields.labels.results,
labels
@@ -131,10 +66,9 @@ class JobTemplateEdit extends Component {
...associationPromises,
]);
return results;
- }
+ };
- async submitInstanceGroups(groups, initialGroups) {
- const { template } = this.props;
+ const submitInstanceGroups = async (groups, initialGroups) => {
const { added, removed } = getAddedAndRemoved(initialGroups, groups);
const disassociatePromises = await removed.map(group =>
JobTemplatesAPI.disassociateInstanceGroup(template.id, group.id)
@@ -143,10 +77,9 @@ class JobTemplateEdit extends Component {
JobTemplatesAPI.associateInstanceGroup(template.id, group.id)
);
return Promise.all([...disassociatePromises, ...associatePromises]);
- }
+ };
- async submitCredentials(newCredentials) {
- const { template } = this.props;
+ const submitCredentials = async newCredentials => {
const { added, removed } = getAddedAndRemoved(
template.summary_fields.credentials,
newCredentials
@@ -160,55 +93,34 @@ class JobTemplateEdit extends Component {
);
const associatePromise = Promise.all(associateCredentials);
return Promise.all([disassociatePromise, associatePromise]);
+ };
+
+ const handleCancel = () => {
+ history.push(detailsUrl);
+ };
+
+ const canEdit = template?.summary_fields?.user_capabilities?.edit;
+
+ if (!canEdit) {
+ return ;
}
-
- handleCancel() {
- 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 (
-
-
-
- );
- }
-
- if (contentError) {
- return (
-
-
-
- );
- }
-
- if (!canEdit) {
- return ;
- }
-
- return (
-
-
-
- );
+ if (isLoading) {
+ return ;
}
+ return (
+
+
+
+ );
}
+JobTemplateEdit.propTypes = {
+ template: JobTemplate.isRequired,
+};
+
export default withRouter(JobTemplateEdit);
diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx
index ac75d05435..0d9f84288d 100644
--- a/awx/ui_next/src/screens/Template/Template.jsx
+++ b/awx/ui_next/src/screens/Template/Template.jsx
@@ -13,6 +13,7 @@ import {
useRouteMatch,
} from 'react-router-dom';
import RoutedTabs from '../../components/RoutedTabs';
+import { useConfig } from '../../contexts/Config';
import useRequest from '../../util/useRequest';
import ContentError from '../../components/ContentError';
import JobList from '../../components/JobList';
@@ -24,15 +25,16 @@ import JobTemplateEdit from './JobTemplateEdit';
import { JobTemplatesAPI, OrganizationsAPI } from '../../api';
import TemplateSurvey from './TemplateSurvey';
-function Template({ i18n, me, setBreadcrumb }) {
+function Template({ i18n, setBreadcrumb }) {
+ const match = useRouteMatch();
const location = useLocation();
const { id: templateId } = useParams();
- const match = useRouteMatch();
+ const { me = {} } = useConfig();
const {
result: { isNotifAdmin, template },
- isLoading: hasRolesandTemplateLoading,
- error: rolesAndTemplateError,
+ isLoading,
+ error: contentError,
request: loadTemplateAndRoles,
} = useRequest(
useCallback(async () => {
@@ -44,9 +46,8 @@ function Template({ i18n, me, setBreadcrumb }) {
role_level: 'notification_admin_role',
}),
]);
-
- if (actions.data.actions.PUT) {
- if (data.webhook_service && data?.related?.webhook_key) {
+ if (actions?.data?.actions?.PUT) {
+ if (data?.webhook_service && data?.related?.webhook_key) {
const {
data: { webhook_key },
} = await JobTemplatesAPI.readWebhookKey(templateId);
@@ -54,35 +55,40 @@ function Template({ i18n, me, setBreadcrumb }) {
data.webhook_key = webhook_key;
}
}
- setBreadcrumb(data);
-
return {
template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0,
};
- }, [setBreadcrumb, templateId]),
+ }, [templateId]),
{ isNotifAdmin: false, template: null }
);
+
useEffect(() => {
loadTemplateAndRoles();
}, [loadTemplateAndRoles, location.pathname]);
+ useEffect(() => {
+ if (template) {
+ setBreadcrumb(template);
+ }
+ }, [template, setBreadcrumb]);
+
const createSchedule = data => {
- return JobTemplatesAPI.createSchedule(templateId, data);
+ return JobTemplatesAPI.createSchedule(template.id, data);
};
const loadScheduleOptions = useCallback(() => {
- return JobTemplatesAPI.readScheduleOptions(templateId);
- }, [templateId]);
+ return JobTemplatesAPI.readScheduleOptions(template.id);
+ }, [template]);
const loadSchedules = useCallback(
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 =
template?.summary_fields?.user_capabilities.edit ||
template?.summary_fields?.user_capabilities.delete;
@@ -131,17 +137,7 @@ function Template({ i18n, me, setBreadcrumb }) {
tab.id = n;
});
- let showCardHeader = true;
-
- if (
- location.pathname.endsWith('edit') ||
- location.pathname.includes('schedules/')
- ) {
- showCardHeader = false;
- }
-
- const contentError = rolesAndTemplateError;
- if (!hasRolesandTemplateLoading && contentError) {
+ if (contentError) {
return (
@@ -158,38 +154,37 @@ function Template({ i18n, me, setBreadcrumb }) {
);
}
+ const showCardHeader = !(
+ location.pathname.endsWith('edit') ||
+ location.pathname.includes('schedules/')
+ );
+
return (
{showCardHeader && }
-
-
- {template && (
+ {template && (
+
+
- )}
- {template && (
- )}
- {template && (
- )}
- {template && (
- )}
- {canSeeNotificationsTab && (
-
-
-
- )}
- {template?.id && (
+ {canSeeNotificationsTab && (
+
+
+
+ )}
- )}
- {template && (
- )}
- {!hasRolesandTemplateLoading && (
-
-
- {match.params.id && (
-
- {i18n._(t`View Template Details`)}
-
- )}
-
-
- )}
-
+ {!isLoading && (
+
+
+ {match.params.id && (
+
+ {i18n._(t`View Template Details`)}
+
+ )}
+
+
+ )}
+
+ )}
);
diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx
index d63d8f7d46..fbc9e237af 100644
--- a/awx/ui_next/src/screens/Template/Templates.jsx
+++ b/awx/ui_next/src/screens/Template/Templates.jsx
@@ -1,10 +1,9 @@
-import React, { Component } from 'react';
+import React, { useState, useCallback, useRef } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Route, withRouter, Switch } from 'react-router-dom';
import { PageSection } from '@patternfly/react-core';
-import { Config } from '../../contexts/Config';
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
import { TemplateList } from './TemplateList';
import Template from './Template';
@@ -12,123 +11,66 @@ import WorkflowJobTemplate from './WorkflowJobTemplate';
import JobTemplateAdd from './JobTemplateAdd';
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
-class Templates extends Component {
- constructor(props) {
- super(props);
- const { i18n } = this.props;
+function Templates({ i18n }) {
+ const initBreadcrumbs = useRef({
+ '/templates': i18n._(t`Templates`),
+ '/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 = {
- breadcrumbConfig: {
- '/templates': i18n._(t`Templates`),
- '/templates/job_template/add': i18n._(t`Create New Job Template`),
- '/templates/workflow_job_template/add': i18n._(
- t`Create New Workflow Template`
- ),
- },
- };
- }
-
- setBreadCrumbConfig = (template, schedule) => {
- const { i18n } = this.props;
- if (!template) {
- return;
- }
- const breadcrumbConfig = {
- '/templates': i18n._(t`Templates`),
- '/templates/job_template/add': i18n._(t`Create New Job Template`),
- '/templates/workflow_job_template/add': i18n._(
- t`Create New Workflow Template`
- ),
- [`/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 (
- <>
-
-
-
-
-
-
-
-
- (
-
- {({ me }) => (
-
- )}
-
- )}
- />
- (
-
- {({ me }) => (
-
- )}
-
- )}
- />
-
-
-
-
-
-
- >
- );
- }
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
}
export { Templates as _Templates };
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
index 6e791be60e..4b997fa9b9 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx
@@ -13,6 +13,7 @@ import {
useRouteMatch,
} from 'react-router-dom';
import RoutedTabs from '../../components/RoutedTabs';
+import { useConfig } from '../../contexts/Config';
import useRequest from '../../util/useRequest';
import AppendBody from '../../components/AppendBody';
import ContentError from '../../components/ContentError';
@@ -27,10 +28,11 @@ import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api';
import TemplateSurvey from './TemplateSurvey';
import { Visualizer } from './WorkflowJobTemplateVisualizer';
-function WorkflowJobTemplate({ i18n, me, setBreadcrumb }) {
+function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
const location = useLocation();
- const { id: templateId } = useParams();
const match = useRouteMatch();
+ const { id: templateId } = useParams();
+ const { me = {} } = useConfig();
const {
result: { isNotifAdmin, template },
diff --git a/awx/ui_next/testUtils/enzymeHelpers.test.jsx b/awx/ui_next/testUtils/enzymeHelpers.test.jsx
index 44de31bed8..3e7b66a0d6 100644
--- a/awx/ui_next/testUtils/enzymeHelpers.test.jsx
+++ b/awx/ui_next/testUtils/enzymeHelpers.test.jsx
@@ -1,27 +1,24 @@
import React, { Component } from 'react';
import { createMemoryHistory } from 'history';
import { Link } from 'react-router-dom';
-import { withI18n } from '@lingui/react';
-import { t } from '@lingui/macro';
+
import { mountWithContexts, waitForElement } from './enzymeHelpers';
import { Config } from '../src/contexts/Config';
describe('mountWithContexts', () => {
describe('injected I18nProvider', () => {
test('should mount and render', () => {
- const Child = withI18n()(({ i18n }) => (
+ const Child = () => (
- {i18n._(t`Text content`)}
+ Text content
- ));
+ );
const wrapper = mountWithContexts();
expect(wrapper.find('div')).toMatchSnapshot();
});
test('should mount and render deeply nested consumer', () => {
- const Child = withI18n()(({ i18n }) => (
- {i18n._(t`Text content`)}
- ));
+ const Child = () => Text content
;
const Parent = () => ;
const wrapper = mountWithContexts();
expect(wrapper.find('Parent')).toMatchSnapshot();
@@ -146,7 +143,9 @@ describe('waitForElement', () => {
} catch (err) {
error = err;
} 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');
done();
}