From 463357c81ee380941ffb38753367c6a12c919e0f Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 25 Jun 2019 10:51:25 -0400 Subject: [PATCH] Add template edit form skeleton --- .../FormActionGroup/FormActionGroup.jsx | 30 ++-- src/components/FormField/FormField.jsx | 21 ++- src/screens/Template/Template.jsx | 92 ++++++++++-- .../Template/TemplateEdit/TemplateEdit.jsx | 64 ++++++++ src/screens/Template/TemplateEdit/index.js | 1 + src/screens/Template/Templates.jsx | 3 +- src/screens/Template/shared/TemplateForm.jsx | 141 ++++++++++++++++++ src/screens/Template/shared/index.js | 1 + src/types.js | 14 +- src/util/validators.jsx | 2 +- 10 files changed, 337 insertions(+), 32 deletions(-) create mode 100644 src/screens/Template/TemplateEdit/TemplateEdit.jsx create mode 100644 src/screens/Template/TemplateEdit/index.js create mode 100644 src/screens/Template/shared/TemplateForm.jsx create mode 100644 src/screens/Template/shared/index.js diff --git a/src/components/FormActionGroup/FormActionGroup.jsx b/src/components/FormActionGroup/FormActionGroup.jsx index 7a91e72c15..634212ff07 100644 --- a/src/components/FormActionGroup/FormActionGroup.jsx +++ b/src/components/FormActionGroup/FormActionGroup.jsx @@ -4,29 +4,31 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { ActionGroup as PFActionGroup, - Toolbar, - ToolbarGroup, Button } from '@patternfly/react-core'; import styled from 'styled-components'; const ActionGroup = styled(PFActionGroup)` - display: flex; - flex-direction: row; - justify-content: flex-end; - --pf-c-form__group--m-action--MarginTop: 0; + display: flex; + justify-content: flex-end; + --pf-c-form__group--m-action--MarginTop: 0; + + .pf-c-form__actions { + display: grid; + gap: 24px; + grid-template-columns: auto auto; + margin: 0; + + & > button { + margin: 0; + } + } `; const FormActionGroup = ({ onSubmit, submitDisabled, onCancel, i18n }) => ( - - - - - - - - + + ); diff --git a/src/components/FormField/FormField.jsx b/src/components/FormField/FormField.jsx index b31297fa48..2833226a26 100644 --- a/src/components/FormField/FormField.jsx +++ b/src/components/FormField/FormField.jsx @@ -1,10 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Field } from 'formik'; -import { FormGroup, TextInput } from '@patternfly/react-core'; +import { FormGroup, TextInput, Tooltip } from '@patternfly/react-core'; +import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; +import styled from 'styled-components'; + +const QuestionCircleIcon = styled(PFQuestionCircleIcon)` + margin-left: 10px; +`; function FormField (props) { - const { id, name, label, validate, isRequired, ...rest } = props; + const { id, name, label, tooltip, validate, isRequired, ...rest } = props; return ( + {tooltip && ( + + + + + )} {}, isRequired: false, + tooltip: null }; export default FormField; diff --git a/src/screens/Template/Template.jsx b/src/screens/Template/Template.jsx index 5e7717bf5d..375c627eb2 100644 --- a/src/screens/Template/Template.jsx +++ b/src/screens/Template/Template.jsx @@ -1,37 +1,73 @@ import React, { Component } from 'react'; import { t } from '@lingui/macro'; import { withI18n } from '@lingui/react'; -import { Card, CardHeader, PageSection } from '@patternfly/react-core'; -import { Switch, Route, Redirect, withRouter } from 'react-router-dom'; - +import { + Card, + CardHeader, + PageSection, +} from '@patternfly/react-core'; +import { + Switch, + Route, + Redirect, + withRouter, +} from 'react-router-dom'; import CardCloseButton from '@components/CardCloseButton'; import ContentError from '@components/ContentError'; import RoutedTabs from '@components/RoutedTabs'; import JobTemplateDetail from './JobTemplateDetail'; import { JobTemplatesAPI } from '@api'; +import TemplateEdit from './TemplateEdit'; class Template extends Component { constructor (props) { super(props); + this.state = { hasContentError: false, hasContentLoading: true, - template: {} + template: null, + actions: null, }; - this.readTemplate = this.readTemplate.bind(this); + this.loadTemplate = this.loadTemplate.bind(this); } - componentDidMount () { - this.readTemplate(); + async componentDidMount () { + await this.loadTemplate(); } - async readTemplate () { + async componentDidUpdate (prevProps) { + const { location } = this.props; + if (location !== prevProps.location) { + await this.loadTemplate(); + } + } + + async loadTemplate () { + const { actions: cachedActions } = this.state; const { setBreadcrumb, match } = this.props; const { id } = match.params; + + let optionsPromise; + if (cachedActions) { + optionsPromise = Promise.resolve({ data: { actions: cachedActions } }); + } else { + optionsPromise = JobTemplatesAPI.readOptions(); + } + + const promises = Promise.all([ + JobTemplatesAPI.readDetail(id), + optionsPromise + ]); + + this.setState({ hasContentError: false, hasContentLoading: true }); try { - const { data } = await JobTemplatesAPI.readDetail(id); + const [{ data }, { data: { actions } }] = await promises; setBreadcrumb(data); - this.setState({ template: data }); + this.setState({ + template: data, + actions + }); } catch { this.setState({ hasContentError: true }); } finally { @@ -40,8 +76,21 @@ class Template extends Component { } render () { - const { match, i18n, history } = this.props; - const { hasContentLoading, template, hasContentError } = this.state; + const { + history, + i18n, + location, + match, + } = this.props; + + const { + actions, + hasContentError, + hasContentLoading, + template + } = this.state; + + const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); const tabsArray = [ { name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, @@ -51,7 +100,8 @@ class Template extends Component { { name: i18n._(t`Completed Jobs`), link: '/home', id: 4 }, { name: i18n._(t`Survey`), link: '/home', id: 5 } ]; - const cardHeader = (hasContentLoading ? null + + let cardHeader = (hasContentLoading ? null : ( @@ -94,11 +148,23 @@ class Template extends Component { )} /> )} + {template && ( + ( + + )} + /> + )} ); } } + export { Template as _Template }; export default withI18n()(withRouter(Template)); diff --git a/src/screens/Template/TemplateEdit/TemplateEdit.jsx b/src/screens/Template/TemplateEdit/TemplateEdit.jsx new file mode 100644 index 0000000000..c1fb0e142a --- /dev/null +++ b/src/screens/Template/TemplateEdit/TemplateEdit.jsx @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withRouter, Redirect } from 'react-router-dom'; +import { CardBody } from '@patternfly/react-core'; +import TemplateForm from '../shared/TemplateForm'; +import { JobTemplatesAPI } from '@api'; +import { JobTemplate } from '@types'; + +class TemplateEdit extends Component { + static propTypes = { + template: JobTemplate.isRequired, + hasPermissions: PropTypes.bool.isRequired, + }; + + constructor (props) { + super(props); + + this.state = { + error: '' + }; + + this.handleCancel = this.handleCancel.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + async handleSubmit (values) { + const { template: { id, type }, history } = this.props; + + try { + await JobTemplatesAPI.update(id, { ...values }); + history.push(`/templates/${type}/${id}/details`); + } catch (error) { + this.setState({ error }); + } + } + + handleCancel () { + const { template: { id, type }, history } = this.props; + history.push(`/templates/${type}/${id}/details`); + } + + render () { + const { template, hasPermissions } = this.props; + const { error } = this.state; + + if (!hasPermissions) { + const { template: { id, type } } = this.props; + return ; + } + + return ( + + + {error ?
error
: null} +
+ ); + } +} + +export default withRouter(TemplateEdit); diff --git a/src/screens/Template/TemplateEdit/index.js b/src/screens/Template/TemplateEdit/index.js new file mode 100644 index 0000000000..aa6384ec8f --- /dev/null +++ b/src/screens/Template/TemplateEdit/index.js @@ -0,0 +1 @@ +export { default } from './TemplateEdit'; diff --git a/src/screens/Template/Templates.jsx b/src/screens/Template/Templates.jsx index 49032fca39..a73ee935cb 100644 --- a/src/screens/Template/Templates.jsx +++ b/src/screens/Template/Templates.jsx @@ -28,7 +28,8 @@ class Templates extends Component { } const breadcrumbConfig = { '/templates': i18n._(t`Templates`), - [`/templates/${template.type}/${template.id}/details`]: i18n._(t`${template.name} Details`) + [`/templates/${template.type}/${template.id}/details`]: i18n._(t`${template.name} Details`), + [`/templates/${template.type}/${template.id}/edit`]: i18n._(t`${template.name} Edit`) }; this.setState({ breadcrumbConfig }); } diff --git a/src/screens/Template/shared/TemplateForm.jsx b/src/screens/Template/shared/TemplateForm.jsx new file mode 100644 index 0000000000..4961039e68 --- /dev/null +++ b/src/screens/Template/shared/TemplateForm.jsx @@ -0,0 +1,141 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Formik, Field } from 'formik'; +import { + Form, + FormGroup, + Tooltip, +} from '@patternfly/react-core'; +import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; +import AnsibleSelect from '@components/AnsibleSelect'; +import FormActionGroup from '@components/FormActionGroup'; +import FormField from '@components/FormField'; +import FormRow from '@components/FormRow'; +import { required } from '@util/validators'; +import styled from 'styled-components'; +import { JobTemplate } from '@types'; + +const QuestionCircleIcon = styled(PFQuestionCircleIcon)` + margin-left: 10px; +`; + +class TemplateForm extends Component { + static propTypes = { + template: JobTemplate.isRequired, + handleCancel: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + }; + + render () { + const { + handleCancel, + handleSubmit, + i18n, + template + } = this.props; + + const jobTypeOptions = [ + { value: '', label: i18n._(t`Choose a job type`), disabled: true }, + { value: 'run', label: i18n._(t`Run`), disabled: false }, + { value: 'check', label: i18n._(t`Check`), disabled: false } + ]; + + return ( + ( +
+ + + + ( + + + + + + + )} + /> + + + + + + + )} + /> + ); + } +} + +export default withI18n()(withRouter(TemplateForm)); + diff --git a/src/screens/Template/shared/index.js b/src/screens/Template/shared/index.js new file mode 100644 index 0000000000..62fcd2ac30 --- /dev/null +++ b/src/screens/Template/shared/index.js @@ -0,0 +1 @@ +export { default } from './TemplateForm'; diff --git a/src/types.js b/src/types.js index d97ceacb8f..951642de7c 100644 --- a/src/types.js +++ b/src/types.js @@ -1,4 +1,4 @@ -import { shape, arrayOf, number, string, bool } from 'prop-types'; +import { shape, arrayOf, number, string, bool, oneOf } from 'prop-types'; export const Role = shape({ descendent_roles: arrayOf(string), @@ -53,3 +53,15 @@ export const QSConfig = shape({ namespace: string, integerFields: arrayOf(string).isRequired, }); + +export const JobTemplate = shape({ + name: string.isRequired, + description: string, + inventory: number.isRequired, + job_type: oneOf([ + 'run', + 'check' + ]), + playbook: string.isRequired, + project: number.isRequired, +}); diff --git a/src/util/validators.jsx b/src/util/validators.jsx index c3da8d4dbe..30c12eff7b 100644 --- a/src/util/validators.jsx +++ b/src/util/validators.jsx @@ -2,7 +2,7 @@ import { t } from '@lingui/macro'; export function required (message, i18n) { return value => { - if (!value.trim()) { + if (typeof value === 'string' && !value.trim()) { return message || i18n._(t`This field must not be blank`); } return undefined;