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;