Merge pull request #8526 from mabashian/convert-WorkflowJobTemplatejsx-functional

Convert WorkflowJobTemplate.jsx to functional component

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-11-04 01:14:45 +00:00 committed by GitHub
commit 370440f63d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 465 additions and 418 deletions

View File

@ -1,317 +1,265 @@
import React, { Component } from 'react';
import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import { CaretLeftIcon } from '@patternfly/react-icons';
import { Card, PageSection } from '@patternfly/react-core';
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import {
Switch,
Route,
Redirect,
Link,
useLocation,
useParams,
useRouteMatch,
} from 'react-router-dom';
import RoutedTabs from '../../components/RoutedTabs';
import useRequest from '../../util/useRequest';
import AppendBody from '../../components/AppendBody';
import ContentError from '../../components/ContentError';
import FullPage from '../../components/FullPage';
import JobList from '../../components/JobList';
import RoutedTabs from '../../components/RoutedTabs';
import { Schedules } from '../../components/Schedule';
import ContentLoading from '../../components/ContentLoading';
import { ResourceAccessList } from '../../components/ResourceAccessList';
import NotificationList from '../../components/NotificationList';
import {
WorkflowJobTemplatesAPI,
CredentialsAPI,
OrganizationsAPI,
} from '../../api';
import { Schedules } from '../../components/Schedule';
import { ResourceAccessList } from '../../components/ResourceAccessList';
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit';
import { Visualizer } from './WorkflowJobTemplateVisualizer';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api';
import TemplateSurvey from './TemplateSurvey';
import { Visualizer } from './WorkflowJobTemplateVisualizer';
class WorkflowJobTemplate extends Component {
constructor(props) {
super(props);
function WorkflowJobTemplate({ i18n, me, setBreadcrumb }) {
const location = useLocation();
const { id: templateId } = useParams();
const match = useRouteMatch();
this.state = {
contentError: null,
hasContentLoading: true,
template: null,
isNotifAdmin: false,
};
this.createSchedule = this.createSchedule.bind(this);
this.loadTemplate = this.loadTemplate.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
await this.loadTemplate();
}
async componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
await this.loadTemplate();
}
}
async loadTemplate() {
const { setBreadcrumb, match } = this.props;
const { id } = match.params;
this.setState({ contentError: null });
try {
const [
{ data },
{
data: { actions },
},
] = await Promise.all([
WorkflowJobTemplatesAPI.readDetail(id),
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions(id),
const {
result: { isNotifAdmin, template },
isLoading: hasRolesandTemplateLoading,
error: rolesAndTemplateError,
request: loadTemplateAndRoles,
} = useRequest(
useCallback(async () => {
const [{ data }, actions, notifAdminRes] = await Promise.all([
WorkflowJobTemplatesAPI.readDetail(templateId),
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions(templateId),
OrganizationsAPI.read({
page_size: 1,
role_level: 'notification_admin_role',
}),
]);
let webhookKey;
if (actions.PUT) {
if (data?.webhook_service && data?.related?.webhook_key) {
webhookKey = await WorkflowJobTemplatesAPI.readWebhookKey(id);
if (actions.data.actions.PUT) {
if (data.webhook_service && data?.related?.webhook_key) {
const {
data: { webhook_key },
} = await WorkflowJobTemplatesAPI.readWebhookKey(templateId);
data.webhook_key = webhook_key;
}
}
if (data?.summary_fields?.webhook_credential) {
const {
data: {
summary_fields: {
credential_type: { name },
},
},
} = await CredentialsAPI.readDetail(
data.summary_fields.webhook_credential.id
);
data.summary_fields.webhook_credential.kind = name;
}
const notifAdminRes = await OrganizationsAPI.read({
page_size: 1,
role_level: 'notification_admin_role',
});
setBreadcrumb(data);
this.setState({
template: { ...data, webhook_key: webhookKey?.data.webhook_key },
return {
template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0,
});
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
};
}, [setBreadcrumb, templateId]),
{ isNotifAdmin: false, template: null }
);
useEffect(() => {
loadTemplateAndRoles();
}, [loadTemplateAndRoles, location.pathname]);
createSchedule(data) {
const { template } = this.state;
return WorkflowJobTemplatesAPI.createSchedule(template.id, data);
}
const createSchedule = data => {
return WorkflowJobTemplatesAPI.createSchedule(templateId, data);
};
loadScheduleOptions() {
const { template } = this.state;
return WorkflowJobTemplatesAPI.readScheduleOptions(template.id);
}
const loadScheduleOptions = () => {
return WorkflowJobTemplatesAPI.readScheduleOptions(templateId);
};
loadSchedules(params) {
const { template } = this.state;
return WorkflowJobTemplatesAPI.readSchedules(template.id, params);
}
const loadSchedules = params => {
return WorkflowJobTemplatesAPI.readSchedules(templateId, params);
};
render() {
const { i18n, me, location, match, setBreadcrumb } = this.props;
const {
contentError,
hasContentLoading,
template,
isNotifAdmin,
} = this.state;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const canAddAndEditSurvey =
template?.summary_fields?.user_capabilities.edit ||
template?.summary_fields?.user_capabilities.delete;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const canToggleNotifications = isNotifAdmin;
const canAddAndEditSurvey =
template?.summary_fields?.user_capabilities.edit ||
template?.summary_fields?.user_capabilities.delete;
const tabsArray = [
{
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Templates`)}
</>
),
link: `/templates`,
id: 99,
},
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
});
}
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
const tabsArray = [
{
name: (
<>
<CaretLeftIcon />
{i18n._(t`Back to Templates`)}
</>
),
link: `/templates`,
id: 99,
},
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
});
}
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.push(
{
name: i18n._(t`Visualizer`),
link: `${match.url}/visualizer`,
});
tabsArray.push({
},
{
name: i18n._(t`Completed Jobs`),
link: `${match.url}/completed_jobs`,
});
tabsArray.push({
},
{
name: canAddAndEditSurvey ? i18n._(t`Survey`) : i18n._(t`View Survey`),
link: `${match.url}/survey`,
});
tabsArray.forEach((tab, n) => {
tab.id = n;
});
if (hasContentLoading) {
return (
<PageSection>
<Card>
<ContentLoading />
</Card>
</PageSection>
);
}
);
if (contentError) {
return (
<PageSection>
<Card>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(t`Template not found.`)}{' '}
<Link to="/templates">{i18n._(t`View all Templates.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
tabsArray.forEach((tab, n) => {
tab.id = n;
});
let showCardHeader = true;
let showCardHeader = true;
if (
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
showCardHeader = false;
}
if (
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
showCardHeader = false;
}
const contentError = rolesAndTemplateError;
if (!hasRolesandTemplateLoading && contentError) {
return (
<PageSection>
<Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
<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"
>
<WorkflowJobTemplateDetail template={template} />
</Route>
)}
{template && (
<Route path="/templates/workflow_job_template/:id/access">
<ResourceAccessList
resource={template}
apiModel={WorkflowJobTemplatesAPI}
/>
</Route>
)}
{canSeeNotificationsTab && (
<Route path="/templates/workflow_job_template/:id/notifications">
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications}
apiModel={WorkflowJobTemplatesAPI}
showApprovalsToggle
/>
</Route>
)}
{template && (
<Route
key="wfjt-edit"
path="/templates/workflow_job_template/:id/edit"
>
<WorkflowJobTemplateEdit template={template} />
</Route>
)}
{template && (
<Route
key="wfjt-visualizer"
path="/templates/workflow_job_template/:id/visualizer"
>
<AppendBody>
<FullPage>
<Visualizer template={template} />
</FullPage>
</AppendBody>
</Route>
)}
{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/schedules">
<Schedules
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={template}
createSchedule={this.createSchedule}
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
</Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey
template={template}
canEdit={canAddAndEditSurvey}
/>
</Route>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(t`Template not found.`)}{' '}
<Link to="/templates">{i18n._(t`View all Templates.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
return (
<PageSection>
<Card>
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
<Switch>
<Redirect
from="/templates/:templateType/:id"
to="/templates/:templateType/:id/details"
exact
/>
{template && (
<Route key="details" path="/templates/:templateType/:id/details">
<WorkflowJobTemplateDetail template={template} />
</Route>
)}
{template && (
<Route key="edit" path="/templates/:templateType/:id/edit">
<WorkflowJobTemplateEdit template={template} />
</Route>
)}
{template && (
<Route key="access" path="/templates/:templateType/:id/access">
<ResourceAccessList
resource={template}
apiModel={WorkflowJobTemplatesAPI}
/>
</Route>
)}
{template && (
<Route
key="schedules"
path="/templates/:templateType/:id/schedules"
>
<Schedules
createSchedule={createSchedule}
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={template}
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
</Route>
)}
{canSeeNotificationsTab && (
<Route path="/templates/:templateType/:id/notifications">
<NotificationList
id={Number(templateId)}
canToggleNotifications={isNotifAdmin}
apiModel={WorkflowJobTemplatesAPI}
/>
</Route>
)}
{template && (
<Route
key="wfjt-visualizer"
path="/templates/workflow_job_template/:id/visualizer"
>
<AppendBody>
<FullPage>
<Visualizer template={template} />
</FullPage>
</AppendBody>
</Route>
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList
defaultParams={{
workflow_job__workflow_job_template: template.id,
}}
/>
</Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey
template={template}
canEdit={canAddAndEditSurvey}
/>
</Route>
)}
{!hasRolesandTemplateLoading && (
<Route key="not-found" path="*">
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/workflow_job_template/${match.params.id}/details`}
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(t`View Template Details`)}
</Link>
)}
</ContentError>
</Route>
</Switch>
</Card>
</PageSection>
);
}
)}
</Switch>
</Card>
</PageSection>
);
}
export { WorkflowJobTemplate as _WorkflowJobTemplate };
export default withI18n()(withRouter(WorkflowJobTemplate));
export default withI18n()(WorkflowJobTemplate);

View File

@ -1,192 +1,196 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api';
import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import WorkflowJobTemplate from './WorkflowJobTemplate';
import { sleep } from '../../../testUtils/testUtils';
import {
WorkflowJobTemplatesAPI,
CredentialsAPI,
OrganizationsAPI,
} from '../../api';
import mockWorkflowJobTemplateData from './shared/data.workflow_job_template.json';
jest.mock('../../api/models/WorkflowJobTemplates');
jest.mock('../../api/models/Credentials');
jest.mock('../../api/models/Organizations');
describe('<WorkflowJobTemplate/>', () => {
const mockMe = {
is_super_user: true,
is_system_auditor: false,
};
const mockMe = {
is_super_user: true,
is_system_auditor: false,
};
describe('<WorkflowJobTemplate />', () => {
let wrapper;
let history;
beforeAll(() => {
beforeEach(() => {
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: {
id: 1,
name: 'Foo',
description: 'Bar',
created: '2015-07-07T17:21:26.429745Z',
modified: '2019-08-11T19:47:37.980466Z',
extra_vars: '',
webhook_service: 'github',
summary_fields: {
webhook_credential: { id: 1234567, name: 'Foo Webhook Credential' },
created_by: { id: 1, username: 'Athena' },
modified_by: { id: 1, username: 'Apollo' },
recent_jobs: [
{ id: 1, status: 'run' },
{ id: 2, status: 'run' },
{ id: 3, status: 'run' },
],
labels: {
results: [
{ name: 'Label 1', id: 1 },
{ name: 'Label 2', id: 2 },
{ name: 'Label 3', id: 3 },
],
},
user_capabilities: {},
},
related: {
webhook_key: '/api/v2/workflow_job_templates/57/webhook_key/',
},
},
data: mockWorkflowJobTemplateData,
});
WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({
data: { webhook_key: 'WebHook Key' },
});
CredentialsAPI.readDetail.mockResolvedValue({
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
data: {
summary_fields: {
credential_type: { name: 'Github Personal Access Token', id: 1 },
},
actions: { PUT: true },
},
});
OrganizationsAPI.read.mockResolvedValue({
data: { results: [{ id: 1, name: 'Org Foo' }] },
data: {
count: 1,
next: null,
previous: null,
results: [
{
id: 1,
},
],
},
});
WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({
data: {
webhook_key: 'key',
},
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
describe('User can PUT', () => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
data: { actions: { PUT: {} } },
});
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)}
/>,
{
context: {
router: {
history,
},
},
}
);
});
});
test('calls api to get workflow job template data', async () => {
expect(wrapper.find('WorkflowJobTemplate').length).toBe(1);
expect(WorkflowJobTemplatesAPI.readDetail).toBeCalledWith('1');
wrapper.update();
await sleep(0);
expect(WorkflowJobTemplatesAPI.readWebhookKey).toBeCalledWith('1');
expect(
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions
).toBeCalled();
expect(CredentialsAPI.readDetail).toBeCalledWith(1234567);
expect(OrganizationsAPI.read).toBeCalledWith({
page_size: 1,
role_level: 'notification_admin_role',
});
});
test('renders proper tabs', async () => {
const tabs = [
'Details',
'Access',
'Notifications',
'Schedules',
'Visualizer',
'Completed Jobs',
'Survey',
];
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.update();
wrapper.find('TabContainer').forEach(tc => {
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`)));
});
});
test('Does not render Notifications tab', async () => {
OrganizationsAPI.read.mockResolvedValue({
data: { results: [] },
});
const tabs = [
'Details',
'Access',
'Schedules',
'Visualizer',
'Completed Jobs',
'Survey',
];
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.update();
wrapper.find('TabContainer').forEach(tc => {
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`)));
});
test('initially renders succesfully', async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
);
});
});
describe('User cannot PUT', () => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValueOnce(
test('When component mounts API is called and the response is put in state', async () => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
);
});
expect(WorkflowJobTemplatesAPI.readDetail).toBeCalled();
expect(OrganizationsAPI.read).toBeCalled();
});
test('notifications tab shown for admins', async done => {
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 8
);
expect(tabs.at(3).text()).toEqual('Notifications');
done();
});
test('notifications tab hidden with reduced permissions', async done => {
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 0,
next: null,
previous: null,
results: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
el => el.length === 7
);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
done();
});
test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/foobar'],
});
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />,
{
data: { actions: {} },
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/templates/workflow_job_template/1/foobar',
path: '/templates/workflow_job_template/1/foobar',
},
},
},
},
}
);
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)}
/>,
{
context: {
router: {
history,
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
test('should call to get webhook key', async () => {
const history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/foobar'],
});
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/templates/workflow_job_template/1/foobar',
path: '/templates/workflow_job_template/1/foobar',
},
},
},
}
);
});
},
}
);
});
test('should not call for webhook key', async () => {
expect(WorkflowJobTemplatesAPI.readWebhookKey).not.toBeCalled();
expect(WorkflowJobTemplatesAPI.readWebhookKey).toHaveBeenCalled();
});
test('should not call to get webhook key', async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValueOnce(
{
data: {
actions: {},
},
}
);
const history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/foobar'],
});
await act(async () => {
wrapper = mountWithContexts(
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/templates/workflow_job_template/1/foobar',
path: '/templates/workflow_job_template/1/foobar',
},
},
},
},
}
);
});
expect(WorkflowJobTemplatesAPI.readWebhookKey).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,95 @@
{
"id": 15,
"type": "workflow_job_template",
"url": "/api/v2/workflow_job_templates/15/",
"related": {
"named_url": "/api/v2/workflow_job_templates/A workflow++/",
"created_by": "/api/v2/users/1/",
"modified_by": "/api/v2/users/1/",
"workflow_jobs": "/api/v2/workflow_job_templates/15/workflow_jobs/",
"schedules": "/api/v2/workflow_job_templates/15/schedules/",
"launch": "/api/v2/workflow_job_templates/15/launch/",
"webhook_key": "/api/v2/workflow_job_templates/15/webhook_key/",
"webhook_receiver": "/api/v2/workflow_job_templates/15/github/",
"workflow_nodes": "/api/v2/workflow_job_templates/15/workflow_nodes/",
"labels": "/api/v2/workflow_job_templates/15/labels/",
"activity_stream": "/api/v2/workflow_job_templates/15/activity_stream/",
"notification_templates_started": "/api/v2/workflow_job_templates/15/notification_templates_started/",
"notification_templates_success": "/api/v2/workflow_job_templates/15/notification_templates_success/",
"notification_templates_error": "/api/v2/workflow_job_templates/15/notification_templates_error/",
"notification_templates_approvals": "/api/v2/workflow_job_templates/15/notification_templates_approvals/",
"access_list": "/api/v2/workflow_job_templates/15/access_list/",
"object_roles": "/api/v2/workflow_job_templates/15/object_roles/",
"survey_spec": "/api/v2/workflow_job_templates/15/survey_spec/",
"copy": "/api/v2/workflow_job_templates/15/copy/"
},
"summary_fields": {
"created_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"modified_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"object_roles": {
"admin_role": {
"description": "Can manage all aspects of the workflow job template",
"name": "Admin",
"id": 68
},
"execute_role": {
"description": "May run the workflow job template",
"name": "Execute",
"id": 69
},
"read_role": {
"description": "May view settings for the workflow job template",
"name": "Read",
"id": 70
},
"approval_role": {
"description": "Can approve or deny a workflow approval node",
"name": "Approve",
"id": 71
}
},
"user_capabilities": {
"edit": true,
"delete": true,
"start": true,
"schedule": true,
"copy": true
},
"labels": {
"count": 0,
"results": []
},
"recent_jobs": []
},
"created": "2020-10-30T14:29:59.728159Z",
"modified": "2020-11-03T14:48:50.519450Z",
"name": "A workflow",
"description": "",
"last_job_run": null,
"last_job_failed": false,
"next_job_run": null,
"status": "never updated",
"extra_vars": "",
"organization": null,
"survey_enabled": false,
"allow_simultaneous": false,
"ask_variables_on_launch": false,
"inventory": null,
"limit": "",
"scm_branch": "",
"ask_inventory_on_launch": false,
"ask_scm_branch_on_launch": false,
"ask_limit_on_launch": false,
"webhook_service": "github",
"webhook_credential": null
}