Convert WorkflowJobTemplate to functional component

This commit is contained in:
mabashian
2020-11-02 16:42:08 -05:00
parent df54a1edb5
commit 514cba6467
3 changed files with 466 additions and 419 deletions

View File

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

View File

@@ -1,192 +1,196 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api';
import { import {
mountWithContexts, mountWithContexts,
waitForElement, waitForElement,
} from '../../../testUtils/enzymeHelpers'; } from '../../../testUtils/enzymeHelpers';
import WorkflowJobTemplate from './WorkflowJobTemplate'; import WorkflowJobTemplate from './WorkflowJobTemplate';
import { sleep } from '../../../testUtils/testUtils'; import mockWorkflowJobTemplateData from './shared/data.workflow_job_template.json';
import {
WorkflowJobTemplatesAPI,
CredentialsAPI,
OrganizationsAPI,
} from '../../api';
jest.mock('../../api/models/WorkflowJobTemplates'); jest.mock('../../api/models/WorkflowJobTemplates');
jest.mock('../../api/models/Credentials');
jest.mock('../../api/models/Organizations'); jest.mock('../../api/models/Organizations');
describe('<WorkflowJobTemplate/>', () => {
const mockMe = { const mockMe = {
is_super_user: true, is_super_user: true,
is_system_auditor: false, is_system_auditor: false,
}; };
describe('<WorkflowJobTemplate />', () => {
let wrapper; let wrapper;
let history; beforeEach(() => {
beforeAll(() => {
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({ WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: { data: mockWorkflowJobTemplateData,
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/',
},
},
}); });
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({
data: { webhook_key: 'WebHook Key' },
});
CredentialsAPI.readDetail.mockResolvedValue({
data: { data: {
summary_fields: { actions: { PUT: true },
credential_type: { name: 'Github Personal Access Token', id: 1 },
},
}, },
}); });
OrganizationsAPI.read.mockResolvedValue({ 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(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
wrapper.unmount(); wrapper.unmount();
}); });
describe('User can PUT', () => { test('initially renders succesfully', async () => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
data: { actions: { PUT: {} } },
});
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<Route
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} /> <WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)} );
/>, });
});
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} />,
{ {
context: { context: {
router: { router: {
history, history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/templates/workflow_job_template/1/foobar',
path: '/templates/workflow_job_template/1/foobar',
},
},
}, },
}, },
} }
); );
}); });
});
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); await waitForElement(wrapper, 'ContentError', el => el.length === 1);
expect(OrganizationsAPI.read).toBeCalledWith({
page_size: 1,
role_level: 'notification_admin_role',
}); });
test('should call to get webhook key', async () => {
const history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/foobar'],
}); });
await act(async () => {
test('renders proper tabs', async () => { wrapper = mountWithContexts(
const tabs = [ <WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />,
'Details', {
'Access', context: {
'Notifications', router: {
'Schedules', history,
'Visualizer', route: {
'Completed Jobs', location: history.location,
'Survey', match: {
]; params: { id: 1 },
waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); url: '/templates/workflow_job_template/1/foobar',
wrapper.update(); path: '/templates/workflow_job_template/1/foobar',
wrapper.find('TabContainer').forEach(tc => { },
tabs.forEach(t => expect(tc.prop(`aria-label=[${t}]`))); },
},
},
}
);
}); });
expect(WorkflowJobTemplatesAPI.readWebhookKey).toHaveBeenCalled();
}); });
test('should not call to get webhook key', async () => {
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}]`)));
});
});
});
describe('User cannot PUT', () => {
beforeEach(async () => {
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValueOnce( WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValueOnce(
{ {
data: { actions: {} }, data: {
actions: {},
},
} }
); );
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'], const history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/foobar'],
}); });
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<Route <WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />,
path="/templates/workflow_job_template/:id/details"
component={() => (
<WorkflowJobTemplate setBreadcrumb={() => {}} me={mockMe} />
)}
/>,
{ {
context: { context: {
router: { router: {
history, 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();
test('should not call for webhook key', async () => {
expect(WorkflowJobTemplatesAPI.readWebhookKey).not.toBeCalled();
});
}); });
}); });

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
}