mirror of
https://github.com/ansible/awx.git
synced 2026-01-27 08:31:28 -03:30
Adds WorkflowJobTemplate Add and Edit form and functionality.
This commit is contained in:
parent
c1bfcd73fb
commit
52a8935b20
@ -6,6 +6,24 @@ class WorkflowJobTemplates extends Base {
|
||||
this.baseUrl = '/api/v2/workflow_job_templates/';
|
||||
}
|
||||
|
||||
associateLabel(id, label, orgId) {
|
||||
return this.http.post(`${this.baseUrl}${id}/labels/`, {
|
||||
name: label.name,
|
||||
organization: orgId,
|
||||
});
|
||||
}
|
||||
|
||||
createNode(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data);
|
||||
}
|
||||
|
||||
disassociateLabel(id, label) {
|
||||
return this.http.post(`${this.baseUrl}${id}/labels/`, {
|
||||
id: label.id,
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
launch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||
}
|
||||
@ -20,17 +38,16 @@ class WorkflowJobTemplates extends Base {
|
||||
});
|
||||
}
|
||||
|
||||
readScheduleList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/schedules/`, {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
readWebhookKey(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/webhook_key/`);
|
||||
}
|
||||
|
||||
createNode(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/workflow_nodes/`, data);
|
||||
}
|
||||
|
||||
readScheduleList(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobTemplates;
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
|
||||
import {
|
||||
JobTemplatesAPI,
|
||||
@ -141,7 +141,7 @@ function TemplateList({ i18n }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection>
|
||||
<Card>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
@ -245,7 +245,7 @@ function TemplateList({ i18n }) {
|
||||
{i18n._(t`Failed to delete one or more templates.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
</>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { Component } 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';
|
||||
@ -10,6 +9,7 @@ import { TemplateList } from './TemplateList';
|
||||
import Template from './Template';
|
||||
import WorkflowJobTemplate from './WorkflowJobTemplate';
|
||||
import JobTemplateAdd from './JobTemplateAdd';
|
||||
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
|
||||
|
||||
class Templates extends Component {
|
||||
constructor(props) {
|
||||
@ -20,6 +20,9 @@ class Templates extends Component {
|
||||
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 Job Template`
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -56,47 +59,49 @@ class Templates extends Component {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||
<PageSection>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${match.path}/job_template/add`}
|
||||
render={() => <JobTemplateAdd />}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/job_template/:id`}
|
||||
render={({ match: newRouteMatch }) => (
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Template
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadCrumbConfig}
|
||||
me={me || {}}
|
||||
match={newRouteMatch}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/workflow_job_template/:id`}
|
||||
render={({ match: newRouteMatch }) => (
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<WorkflowJobTemplate
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadCrumbConfig}
|
||||
me={me || {}}
|
||||
match={newRouteMatch}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route path={`${match.path}`} render={() => <TemplateList />} />
|
||||
</Switch>
|
||||
</PageSection>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${match.path}/job_template/add`}
|
||||
render={() => <JobTemplateAdd />}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/workflow_job_template/add`}
|
||||
render={() => <WorkflowJobTemplateAdd />}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/job_template/:id`}
|
||||
render={({ match: newRouteMatch }) => (
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Template
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadCrumbConfig}
|
||||
me={me || {}}
|
||||
match={newRouteMatch}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/workflow_job_template/:id`}
|
||||
render={({ match: newRouteMatch }) => (
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<WorkflowJobTemplate
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadCrumbConfig}
|
||||
me={me || {}}
|
||||
match={newRouteMatch}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
)}
|
||||
/>
|
||||
<Route path={`${match.path}`} render={() => <TemplateList />} />
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import RoutedTabs from '@components/RoutedTabs';
|
||||
import ScheduleList from '@components/ScheduleList';
|
||||
import { WorkflowJobTemplatesAPI, CredentialsAPI } from '@api';
|
||||
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
||||
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
|
||||
import { Visualizer } from './WorkflowJobTemplateVisualizer';
|
||||
|
||||
class WorkflowJobTemplate extends Component {
|
||||
@ -134,73 +135,99 @@ class WorkflowJobTemplate extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{cardHeader}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/templates/workflow_job_template/:id"
|
||||
to="/templates/workflow_job_template/:id/details"
|
||||
exact
|
||||
/>
|
||||
{template && (
|
||||
<Route
|
||||
key="wfjt-details"
|
||||
path="/templates/workflow_job_template/:id/details"
|
||||
render={() => (
|
||||
<WorkflowJobTemplateDetail
|
||||
template={template}
|
||||
webHookKey={webHookKey}
|
||||
/>
|
||||
)}
|
||||
<PageSection>
|
||||
<Card className="awx-c-card">
|
||||
{cardHeader}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/templates/workflow_job_template/:id"
|
||||
to="/templates/workflow_job_template/:id/details"
|
||||
exact
|
||||
/>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
key="wfjt-visualizer"
|
||||
path="/templates/workflow_job_template/:id/visualizer"
|
||||
render={() => (
|
||||
<AppendBody>
|
||||
<FullPage>
|
||||
<Visualizer template={template} />
|
||||
</FullPage>
|
||||
</AppendBody>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{template?.id && (
|
||||
<Route path="/templates/workflow_job_template/:id/completed_jobs">
|
||||
<JobList
|
||||
defaultParams={{
|
||||
workflow_job__workflow_job_template: template.id,
|
||||
}}
|
||||
{template && (
|
||||
<Route
|
||||
key="wfjt-details"
|
||||
path="/templates/workflow_job_template/:id/details"
|
||||
render={() => (
|
||||
<WorkflowJobTemplateDetail
|
||||
template={template}
|
||||
webHookKey={webHookKey}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
)}
|
||||
{template && (
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
key="wfjt-edit"
|
||||
path="/templates/workflow_job_template/:id/edit"
|
||||
render={() => (
|
||||
<WorkflowJobTemplateEdit
|
||||
template={template}
|
||||
webHookKey={webHookKey}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
key="wfjt-visualizer"
|
||||
path="/templates/workflow_job_template/:id/visualizer"
|
||||
render={() => (
|
||||
<AppendBody>
|
||||
<FullPage>
|
||||
<Visualizer template={template} />
|
||||
</FullPage>
|
||||
</AppendBody>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{template?.id && (
|
||||
<Route path="/templates/workflow_job_template/:id/completed_jobs">
|
||||
<JobList
|
||||
defaultParams={{
|
||||
workflow_job__workflow_job_template: template.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
)}
|
||||
|
||||
{template?.id && (
|
||||
<Route path="/templates/workflow_job_template/:id/completed_jobs">
|
||||
<JobList
|
||||
defaultParams={{
|
||||
workflow_job__workflow_job_template: template.id,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
)}
|
||||
{template && (
|
||||
<Route
|
||||
path="/templates/:templateType/:id/schedules"
|
||||
render={() => (
|
||||
<ScheduleList loadSchedules={this.loadSchedules} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path="/templates/:templateType/:id/schedules"
|
||||
render={() => <ScheduleList loadSchedules={this.loadSchedules} />}
|
||||
key="not-found"
|
||||
path="*"
|
||||
render={() =>
|
||||
!hasContentLoading && (
|
||||
<ContentError isNotFound>
|
||||
{match.params.id && (
|
||||
<Link
|
||||
to={`/templates/workflow_job_template/${match.params.id}/details`}
|
||||
>
|
||||
{i18n._(`View Template Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
key="not-found"
|
||||
path="*"
|
||||
render={() =>
|
||||
!hasContentLoading && (
|
||||
<ContentError isNotFound>
|
||||
{match.params.id && (
|
||||
<Link
|
||||
to={`/templates/workflow_job_template/${match.params.id}/details`}
|
||||
>
|
||||
{i18n._(`View Template Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Switch>
|
||||
</Card>
|
||||
</Switch>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import WorkflowJobTemplateForm from '../shared/WorkflowJobTemplateForm';
|
||||
|
||||
function WorkflowJobTemplateAdd() {
|
||||
const [formSubmitError, setFormSubmitError] = useState();
|
||||
const history = useHistory();
|
||||
const handleSubmit = async values => {
|
||||
const { labels, organizationId, ...remainingValues } = values;
|
||||
try {
|
||||
const {
|
||||
data: { id },
|
||||
} = await WorkflowJobTemplatesAPI.create(remainingValues);
|
||||
await Promise.all([submitLabels(id, labels, organizationId)]);
|
||||
history.push(`/templates/workflow_job_template/${id}/details`);
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
}
|
||||
};
|
||||
const submitLabels = (templateId, labels = [], organizationId) => {
|
||||
const associatePromises = labels.map(label =>
|
||||
WorkflowJobTemplatesAPI.associateLabel(templateId, label, organizationId)
|
||||
);
|
||||
return Promise.all([...associatePromises]);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
history.push(`/templates`);
|
||||
};
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<WorkflowJobTemplateForm
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</CardBody>
|
||||
{formSubmitError ? <div>formSubmitError</div> : ''}
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowJobTemplateAdd;
|
||||
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<WorkflowJobTemplateAdd/>', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
beforeEach(async () => {
|
||||
WorkflowJobTemplatesAPI.create.mockResolvedValue({ data: { id: 1 } });
|
||||
await act(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/templates/workflow_job_template/add'],
|
||||
});
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/templates/workflow_job_template/add"
|
||||
component={() => <WorkflowJobTemplateAdd />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('initially renders successfully', async () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('calls workflowJobTemplatesAPI with correct information on submit', async () => {
|
||||
await act(async () => {
|
||||
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
|
||||
name: 'Alex',
|
||||
labels: [{ name: 'Foo', id: 1 }, { name: 'bar', id: 2 }],
|
||||
organizationId: 1,
|
||||
});
|
||||
});
|
||||
expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalledWith({
|
||||
name: 'Alex',
|
||||
});
|
||||
expect(WorkflowJobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
test('handleCancel navigates the user to the /templates', async () => {
|
||||
await act(async () => {
|
||||
await wrapper.find('WorkflowJobTemplateForm').invoke('handleCancel')();
|
||||
});
|
||||
expect(history.location.pathname).toBe('/templates');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './WorkflowJobTemplateAdd';
|
||||
@ -0,0 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { CardBody } from '@components/Card';
|
||||
import { getAddedAndRemoved } from '@util/lists';
|
||||
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '@api';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { WorkflowJobTemplateForm } from '../shared';
|
||||
|
||||
function WorkflowJobTemplateEdit({ template, hasContentLoading }) {
|
||||
const [formSubmitError, setFormSubmitError] = useState();
|
||||
const history = useHistory();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { labels, ...remainingValues } = values;
|
||||
try {
|
||||
await WorkflowJobTemplatesAPI.update(template.id, remainingValues);
|
||||
await Promise.all([submitLabels(labels, values.organization)]);
|
||||
history.push(`/templates/workflow_job_template/${template.id}/details`);
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
}
|
||||
};
|
||||
const submitLabels = async (labels = [], orgId) => {
|
||||
const { added, removed } = getAddedAndRemoved(
|
||||
template.summary_fields.labels.results,
|
||||
labels
|
||||
);
|
||||
if (!orgId && !template.organization) {
|
||||
try {
|
||||
const {
|
||||
data: { results },
|
||||
} = await OrganizationsAPI.read();
|
||||
orgId = results[0].id;
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
}
|
||||
}
|
||||
const disassociationPromises = removed.map(label =>
|
||||
WorkflowJobTemplatesAPI.disassociateLabel(template.id, label)
|
||||
);
|
||||
const associationPromises = added.map(label => {
|
||||
return WorkflowJobTemplatesAPI.associateLabel(
|
||||
template.id,
|
||||
label,
|
||||
orgId || template.organization
|
||||
);
|
||||
});
|
||||
|
||||
const results = await Promise.all([
|
||||
...disassociationPromises,
|
||||
...associationPromises,
|
||||
]);
|
||||
return results;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push(`/templates`);
|
||||
};
|
||||
if (hasContentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<CardBody>
|
||||
<WorkflowJobTemplateForm
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
template={template}
|
||||
/>
|
||||
</CardBody>
|
||||
{formSubmitError ? <div>formSubmitError</div> : ''}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default WorkflowJobTemplateEdit;
|
||||
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
|
||||
|
||||
jest.mock('@api');
|
||||
const mockTemplate = {
|
||||
id: 6,
|
||||
name: 'Foo',
|
||||
description: 'Foo description',
|
||||
summary_fields: {
|
||||
inventory: { id: 1, name: 'Inventory 1' },
|
||||
organization: { id: 1, name: 'Organization 1' },
|
||||
labels: {
|
||||
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }],
|
||||
},
|
||||
},
|
||||
scm_branch: 'devel',
|
||||
limit: '5000',
|
||||
variables: '---',
|
||||
};
|
||||
describe('<WorkflowJobTemplateEdit/>', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/templates/workflow_job_template/6/edit'],
|
||||
});
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/templates/workflow_job_template/:id/edit"
|
||||
component={() => <WorkflowJobTemplateEdit template={mockTemplate} />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 6 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('renders successfully', () => {
|
||||
expect(wrapper.find('WorkflowJobTemplateEdit').length).toBe(1);
|
||||
});
|
||||
test('api is called to properly to save the updated template.', async () => {
|
||||
await act(async () => {
|
||||
await wrapper.find('WorkflowJobTemplateForm').invoke('handleSubmit')({
|
||||
id: 6,
|
||||
name: 'Alex',
|
||||
description: 'Apollo and Athena',
|
||||
inventory: { id: 1, name: 'Inventory 1' },
|
||||
organization: 2,
|
||||
labels: [{ name: 'Label 2', id: 2 }, { name: 'Generated Label' }],
|
||||
scm_branch: 'master',
|
||||
limit: '5000',
|
||||
variables: '---',
|
||||
});
|
||||
});
|
||||
expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalledWith(6, {
|
||||
id: 6,
|
||||
name: 'Alex',
|
||||
description: 'Apollo and Athena',
|
||||
inventory: { id: 1, name: 'Inventory 1' },
|
||||
organization: 2,
|
||||
scm_branch: 'master',
|
||||
limit: '5000',
|
||||
variables: '---',
|
||||
});
|
||||
wrapper.update();
|
||||
await expect(WorkflowJobTemplatesAPI.disassociateLabel).toBeCalledWith(6, {
|
||||
name: 'Label 1',
|
||||
id: 1,
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
await expect(WorkflowJobTemplatesAPI.associateLabel).toBeCalledTimes(1);
|
||||
});
|
||||
test('handleCancel navigates the user to the /templates', async () => {
|
||||
await act(async () => {
|
||||
await wrapper.find('WorkflowJobTemplateForm').invoke('handleCancel')();
|
||||
});
|
||||
expect(history.location.pathname).toBe('/templates');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './WorkflowJobTemplateEdit';
|
||||
@ -0,0 +1,176 @@
|
||||
import React, { useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Formik, Field } from 'formik';
|
||||
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import { required } from '@util/validators';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FormRow from '@components/FormRow';
|
||||
import FormField, { FieldTooltip } from '@components/FormField';
|
||||
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
|
||||
import { InventoryLookup } from '@components/Lookup';
|
||||
import { VariablesField } from '@components/CodeMirrorInput';
|
||||
import FormActionGroup from '@components/FormActionGroup';
|
||||
import ContentError from '@components/ContentError';
|
||||
import LabelSelect from './LabelSelect';
|
||||
|
||||
function WorkflowJobTemplateForm({
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
i18n,
|
||||
template = {},
|
||||
}) {
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [inventory, setInventory] = useState(
|
||||
template?.summary_fields?.inventory || null
|
||||
);
|
||||
const [organization, setOrganization] = useState(
|
||||
template?.summary_fields?.organization || null
|
||||
);
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{
|
||||
name: template.name || '',
|
||||
description: template.description || '',
|
||||
inventory: template.summary_fields?.inventory?.id || '',
|
||||
organization: template.summary_fields?.organization?.id || '',
|
||||
labels: template.summary_fields?.labels?.results || [],
|
||||
variables: template.variables || '---',
|
||||
limit: template.limit || '',
|
||||
scmBranch: template.scm_branch || '',
|
||||
ask_limit_on_launch: template.ask_limit_on_launch || false,
|
||||
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
|
||||
ask_variables_on_launch: template.ask_variables_on_launch || false,
|
||||
ask_scm_branch_on_launch: template.ask_scm_branch_on_launch || false,
|
||||
}}
|
||||
>
|
||||
{formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="wfjt-name"
|
||||
name="name"
|
||||
type="text"
|
||||
label={i18n._(t`Name`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="wfjt-description"
|
||||
name="description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
<Field
|
||||
id="wfjt-organization"
|
||||
label={i18n._(t`Organization`)}
|
||||
name="organization"
|
||||
>
|
||||
{({ form }) => (
|
||||
<OrganizationLookup
|
||||
helperTextInvalid={form.errors.organization}
|
||||
isValid={
|
||||
!form.touched.organization || !form.errors.organization
|
||||
}
|
||||
onBlur={() => form.setFieldTouched('organization')}
|
||||
onChange={value => {
|
||||
form.setFieldValue('organization', value?.id || null);
|
||||
setOrganization(value);
|
||||
}}
|
||||
value={organization}
|
||||
touched={form.touched.organization}
|
||||
error={form.errors.organization}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Field name="inventory">
|
||||
{({ form }) => (
|
||||
<InventoryLookup
|
||||
value={inventory}
|
||||
onBlur={() => form.setFieldTouched('inventory')}
|
||||
tooltip={i18n._(t`Select the inventory containing the hosts
|
||||
you want this job to manage.`)}
|
||||
isValid={!form.touched.inventory || !form.errors.inventory}
|
||||
helperTextInvalid={form.errors.inventory}
|
||||
onChange={value => {
|
||||
form.setFieldValue('inventory', value?.id || null);
|
||||
form.setFieldValue(
|
||||
'organizationId',
|
||||
value?.organization || null
|
||||
);
|
||||
setInventory(value);
|
||||
}}
|
||||
touched={form.touched.inventory}
|
||||
error={form.errors.inventory}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<FormGroup
|
||||
fieldId="wfjt-limit"
|
||||
name="limit"
|
||||
label={i18n._(t`Limit`)}
|
||||
>
|
||||
<FieldTooltip
|
||||
content={i18n._(
|
||||
t`Provide a host pattern to further constrain the list of hosts that will be managed or affected by the workflow. This limit is applied to all job template nodes that prompt for a limit. Refer to Ansible documentation for more information and examples on patterns.`
|
||||
)}
|
||||
/>
|
||||
<FormField type="text" name="limit" id="wfjt-limit" label="" />
|
||||
</FormGroup>
|
||||
<FormField
|
||||
type="text"
|
||||
id="wfjt-scmBranch"
|
||||
label={i18n._(t`SCM Branch`)}
|
||||
name="scmBranch"
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow>
|
||||
<Field name="labels">
|
||||
{({ field, form }) => (
|
||||
<FormGroup label={i18n._(t`Labels`)} fieldId="wfjt-labels">
|
||||
<FieldTooltip
|
||||
content={i18n._(t`Optional labels that describe this job template,
|
||||
such as 'dev' or 'test'. Labels can be used to group and filter
|
||||
job templates and completed jobs.`)}
|
||||
/>
|
||||
<LabelSelect
|
||||
value={field.value}
|
||||
onChange={labels => form.setFieldValue('labels', labels)}
|
||||
onError={() => setContentError()}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<VariablesField
|
||||
id="host-variables"
|
||||
name="variables"
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowJobTemplateForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
export default withI18n()(WorkflowJobTemplateForm);
|
||||
@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import WorkflowJobTemplateForm from './WorkflowJobTemplateForm';
|
||||
|
||||
describe('<WorkflowJobTemplateForm/>', () => {
|
||||
let wrapper;
|
||||
const handleSubmit = jest.fn();
|
||||
const handleCancel = jest.fn();
|
||||
const mockTemplate = {
|
||||
id: 6,
|
||||
name: 'Foo',
|
||||
description: 'Foo description',
|
||||
summary_fields: {
|
||||
inventory: { id: 1, name: 'Inventory 1' },
|
||||
organization: { id: 1, name: 'Organization 1' },
|
||||
labels: {
|
||||
results: [{ name: 'Label 1', id: 1 }, { name: 'Label 2', id: 2 }],
|
||||
},
|
||||
},
|
||||
scm_branch: 'devel',
|
||||
limit: '5000',
|
||||
variables: '---',
|
||||
};
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowJobTemplateForm
|
||||
template={mockTemplate}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('renders successfully', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
test('all the fields render successfully', () => {
|
||||
const fields = [
|
||||
'name',
|
||||
'description',
|
||||
'organization',
|
||||
'inventory',
|
||||
'limit',
|
||||
'scmBranch',
|
||||
'labels',
|
||||
'variables',
|
||||
];
|
||||
const assertField = (field, index) => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('Field')
|
||||
.at(index)
|
||||
.prop('name')
|
||||
).toBe(`${field}`);
|
||||
};
|
||||
fields.map((field, index) => assertField(field, index));
|
||||
});
|
||||
test('changing inputs should update values', async () => {
|
||||
const inputsToChange = [
|
||||
{ element: 'wfjt-name', value: { value: 'new foo', name: 'name' } },
|
||||
{
|
||||
element: 'wfjt-description',
|
||||
value: { value: 'new bar', name: 'description' },
|
||||
},
|
||||
{ element: 'wfjt-limit', value: { value: 1234567890, name: 'limit' } },
|
||||
{
|
||||
element: 'wfjt-scmBranch',
|
||||
value: { value: 'new branch', name: 'scmBranch' },
|
||||
},
|
||||
];
|
||||
const changeInputs = async ({ element, value }) => {
|
||||
wrapper.find(`input#${element}`).simulate('change', {
|
||||
target: value,
|
||||
});
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
inputsToChange.map(input => changeInputs(input));
|
||||
wrapper.find('LabelSelect').invoke('onChange')([
|
||||
{ name: 'new label', id: 5 },
|
||||
{ name: 'Label 1', id: 1 },
|
||||
{ name: 'Label 2', id: 2 },
|
||||
]);
|
||||
wrapper.find('InventoryLookup').invoke('onChange')({
|
||||
id: 3,
|
||||
name: 'inventory',
|
||||
});
|
||||
wrapper.find('OrganizationLookup').invoke('onChange')({
|
||||
id: 3,
|
||||
name: 'organization',
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
const assertChanges = ({ element, value }) => {
|
||||
expect(wrapper.find(`input#${element}`).prop('value')).toEqual(
|
||||
typeof value.value === 'string' ? `${value.value}` : value.value
|
||||
);
|
||||
};
|
||||
|
||||
inputsToChange.map(input => assertChanges(input));
|
||||
expect(wrapper.find('InventoryLookup').prop('value')).toEqual({
|
||||
id: 3,
|
||||
name: 'inventory',
|
||||
});
|
||||
expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({
|
||||
id: 3,
|
||||
name: 'organization',
|
||||
});
|
||||
expect(wrapper.find('LabelSelect').prop('value')).toEqual([
|
||||
{ name: 'new label', id: 5 },
|
||||
{ name: 'Label 1', id: 1 },
|
||||
{ name: 'Label 2', id: 2 },
|
||||
]);
|
||||
});
|
||||
test('handleSubmit is called on submit button click', async () => {
|
||||
await act(async () => {
|
||||
await wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
expect(handleSubmit).toBeCalled();
|
||||
});
|
||||
});
|
||||
test('handleCancel is called on cancel button click', () => {
|
||||
act(() => {
|
||||
wrapper.find('button[aria-label="Cancel"]').simulate('click');
|
||||
});
|
||||
|
||||
expect(handleCancel).toBeCalled();
|
||||
});
|
||||
});
|
||||
@ -1 +1,2 @@
|
||||
export { default } from './JobTemplateForm';
|
||||
export { default as JobTemplateForm } from './JobTemplateForm';
|
||||
export { default as WorkflowJobTemplateForm } from './WorkflowJobTemplateForm';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user