mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 23:51:09 +03:00
Add template edit form skeleton
This commit is contained in:
parent
7b3e5cd8d5
commit
463357c81e
@ -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;
|
||||
|
||||
.pf-c-form__actions {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: auto auto;
|
||||
margin: 0;
|
||||
|
||||
& > button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const FormActionGroup = ({ onSubmit, submitDisabled, onCancel, i18n }) => (
|
||||
<ActionGroup>
|
||||
<Toolbar>
|
||||
<ToolbarGroup css="margin-right: 20px">
|
||||
<Button aria-label={i18n._(t`Save`)} variant="primary" type="submit" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" type="button" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
</ActionGroup>
|
||||
);
|
||||
|
||||
|
@ -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 (
|
||||
<Field
|
||||
@ -21,6 +27,15 @@ function FormField (props) {
|
||||
isValid={isValid}
|
||||
label={label}
|
||||
>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={tooltip}
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
<TextInput
|
||||
id={id}
|
||||
isRequired={isRequired}
|
||||
@ -45,12 +60,14 @@ FormField.propTypes = {
|
||||
type: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
isRequired: PropTypes.bool,
|
||||
tooltip: PropTypes.string,
|
||||
};
|
||||
|
||||
FormField.defaultProps = {
|
||||
type: 'text',
|
||||
validate: () => {},
|
||||
isRequired: false,
|
||||
tooltip: null
|
||||
};
|
||||
|
||||
export default FormField;
|
||||
|
@ -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
|
||||
: (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<RoutedTabs
|
||||
@ -63,6 +113,10 @@ class Template extends Component {
|
||||
)
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
cardHeader = null;
|
||||
}
|
||||
|
||||
if (!hasContentLoading && hasContentError) {
|
||||
return (
|
||||
<PageSection>
|
||||
@ -94,11 +148,23 @@ class Template extends Component {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
path="/templates/:templateType/:id/edit"
|
||||
render={() => (
|
||||
<TemplateEdit
|
||||
template={template}
|
||||
hasPermissions={canAdd}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Switch>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Template as _Template };
|
||||
export default withI18n()(withRouter(Template));
|
||||
|
64
src/screens/Template/TemplateEdit/TemplateEdit.jsx
Normal file
64
src/screens/Template/TemplateEdit/TemplateEdit.jsx
Normal file
@ -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 <Redirect to={`/templates/${type}/${id}/details`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<TemplateForm
|
||||
template={template}
|
||||
handleCancel={this.handleCancel}
|
||||
handleSubmit={this.handleSubmit}
|
||||
/>
|
||||
{error ? <div> error </div> : null}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(TemplateEdit);
|
1
src/screens/Template/TemplateEdit/index.js
Normal file
1
src/screens/Template/TemplateEdit/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './TemplateEdit';
|
@ -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 });
|
||||
}
|
||||
|
141
src/screens/Template/shared/TemplateForm.jsx
Normal file
141
src/screens/Template/shared/TemplateForm.jsx
Normal file
@ -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 (
|
||||
<Formik
|
||||
initialValues={{
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
job_type: template.job_type,
|
||||
inventory: template.inventory,
|
||||
project: template.project,
|
||||
playbook: template.playbook
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="template-name"
|
||||
name="name"
|
||||
type="text"
|
||||
label={i18n._(t`Name`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="template-description"
|
||||
name="description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
<Field
|
||||
name="job_type"
|
||||
validate={required(null, i18n)}
|
||||
render={({ field }) => (
|
||||
<FormGroup
|
||||
fieldId="template-job-type"
|
||||
isRequired
|
||||
label={i18n._(t`Job Type`)}
|
||||
>
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={i18n._(t`For job templates, select run to execute
|
||||
the playbook. Select check to only check playbook syntax,
|
||||
test environment setup, and report problems without
|
||||
executing the playbook.`)}
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
<AnsibleSelect
|
||||
data={jobTypeOptions}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
id="template-inventory"
|
||||
name="inventory"
|
||||
type="number"
|
||||
label={i18n._(t`Inventory`)}
|
||||
tooltip={i18n._(t`Select the inventory containing the hosts
|
||||
you want this job to manage.`)}
|
||||
isRequired
|
||||
validate={required(null, i18n)}
|
||||
/>
|
||||
<FormField
|
||||
id="template-project"
|
||||
name="project"
|
||||
type="number"
|
||||
label={i18n._(t`Project`)}
|
||||
tooltip={i18n._(t`Select the project containing the playbook
|
||||
you want this job to execute.`)}
|
||||
isRequired
|
||||
validate={required(null, i18n)}
|
||||
/>
|
||||
<FormField
|
||||
id="template-playbook"
|
||||
name="playbook"
|
||||
type="text"
|
||||
label={i18n._(t`Playbook`)}
|
||||
tooltip={i18n._(t`Select the playbook to be executed by this job.`)}
|
||||
isRequired
|
||||
validate={required(null, i18n)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withI18n()(withRouter(TemplateForm));
|
||||
|
1
src/screens/Template/shared/index.js
Normal file
1
src/screens/Template/shared/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './TemplateForm';
|
14
src/types.js
14
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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user