From d82f68c88e692835da5bc627058832cbd02bece8 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 30 Nov 2020 12:32:00 -0500 Subject: [PATCH] moves some files to hooks in preparation for lingUI upgrade --- awx/ui_next/src/components/About/About.jsx | 64 +++--- .../AppContainer/PageHeaderToolbar.jsx | 213 ++++++++---------- .../components/ErrorDetail/ErrorDetail.jsx | 66 ++---- .../ErrorDetail/ErrorDetail.test.jsx | 3 +- .../src/screens/Job/JobTypeRedirect.jsx | 104 ++++----- .../screens/ManagementJob/ManagementJobs.jsx | 22 +- .../screens/Organization/Organizations.jsx | 80 +++---- .../src/screens/Project/Project.test.jsx | 14 +- .../JobTemplateEdit/JobTemplateEdit.jsx | 187 ++++----------- .../src/screens/Template/Templates.jsx | 134 +++++------ awx/ui_next/testUtils/enzymeHelpers.test.jsx | 17 +- 11 files changed, 349 insertions(+), 555 deletions(-) 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.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..1e206e03f3 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -1,89 +1,21 @@ /* 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, - }; +function JobTemplateEdit({ template }) { + const { id, type } = template; + const history = useHistory(); + const [formSubmitError, setFormSubmitError] = useState(null); - constructor(props) { - super(props); + const detailsUrl = `/templates/${type}/${id}/details`; - this.state = { - hasContentLoading: true, - contentError: null, - formSubmitError: null, - relatedCredentials: [], - relatedProjectPlaybooks: [], - }; - - 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 +27,23 @@ class JobTemplateEdit extends Component { ...remainingValues } = values; - this.setState({ formSubmitError: null }); + setFormSubmitError(null); 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); - } catch (error) { - this.setState({ formSubmitError: error }); + history.push(detailsUrl); + } catch (err) { + setFormSubmitError(err); } - } - - 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 +61,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 +72,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 +88,32 @@ 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 ( - - - - ); - } + return ( + + + + ); } +JobTemplateForm.propTypes = { + template: JobTemplate.isRequired, +}; + export default withRouter(JobTemplateEdit); diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx index d63d8f7d46..a9d5ecc726 100644 --- a/awx/ui_next/src/screens/Template/Templates.jsx +++ b/awx/ui_next/src/screens/Template/Templates.jsx @@ -1,7 +1,7 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Route, withRouter, Switch } from 'react-router-dom'; +import { Route, withRouter, Switch, useRouteMatch } from 'react-router-dom'; import { PageSection } from '@patternfly/react-core'; import { Config } from '../../contexts/Config'; @@ -12,28 +12,21 @@ 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 match = useRouteMatch(); + const [breadcrumbConfig, setBreadcrumbs] = useState({ + '/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` + ), + }); - 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; + const setBreadCrumbConfig = (template, schedule) => { if (!template) { return; } - const breadcrumbConfig = { + const breadcrumb = { '/templates': i18n._(t`Templates`), '/templates/job_template/add': i18n._(t`Create New Job Template`), '/templates/workflow_job_template/add': i18n._( @@ -73,62 +66,55 @@ class Templates extends Component { [`/templates/${template.type}/${template.id}/schedules/${schedule && schedule.id}/edit`]: i18n._(t`Edit Details`), }; - this.setState({ breadcrumbConfig }); + setBreadcrumbs(breadcrumb); }; - render() { - const { match, history, location } = this.props; - const { breadcrumbConfig } = this.state; - return ( - <> - - - - - - - - - ( - - {({ me }) => ( -