From 514cba64672d0d2c42543054f153a940467a78dd Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 2 Nov 2020 16:42:08 -0500 Subject: [PATCH 1/2] Convert WorkflowJobTemplate to functional component --- .../screens/Template/WorkflowJobTemplate.jsx | 476 ++++++++---------- .../Template/WorkflowJobTemplate.test.jsx | 314 ++++++------ .../shared/data.workflow_job_template.json | 95 ++++ 3 files changed, 466 insertions(+), 419 deletions(-) create mode 100644 awx/ui_next/src/screens/Template/shared/data.workflow_job_template.json diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index e16a35ce1c..f13926a828 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -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 WorkflowJobTemplateEdit from './JobTemplateEdit'; +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: ( - <> - - {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: ( + <> + + {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 ( - - - - - - ); } + ); - if (contentError) { - return ( - - - - {contentError.response.status === 404 && ( - - {i18n._(t`Template not found.`)}{' '} - {i18n._(t`View all Templates.`)} - - )} - - - - ); - } + 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 ( - {showCardHeader && } - - - {template && ( - - - - )} - {template && ( - - - - )} - {canSeeNotificationsTab && ( - - - - )} - {template && ( - - - - )} - {template && ( - - - - - - - - )} - {template?.id && ( - - - - )} - {template?.id && ( - - - - )} - {template && ( - - - + + {contentError.response.status === 404 && ( + + {i18n._(t`Template not found.`)}{' '} + {i18n._(t`View all Templates.`)} + )} + + + + ); + } + + return ( + + + {showCardHeader && } + + + {template && ( + + + + )} + {template && ( + + + + )} + {template && ( + + + + )} + {template && ( + + + + )} + {canSeeNotificationsTab && ( + + + + )} + {template && ( + + + + + + + + )} + {template?.id && ( + + + + )} + {template && ( + + + + )} + {!hasRolesandTemplateLoading && ( {match.params.id && ( {i18n._(t`View Template Details`)} )} - - - - ); - } + )} + + + + ); } export { WorkflowJobTemplate as _WorkflowJobTemplate }; -export default withI18n()(withRouter(WorkflowJobTemplate)); +export default withI18n()(WorkflowJobTemplate); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx index 2a083ffe2a..9343ede200 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.test.jsx @@ -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('', () => { - const mockMe = { - is_super_user: true, - is_system_auditor: false, - }; +const mockMe = { + is_super_user: true, + is_system_auditor: false, +}; +describe('', () => { 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( - ( - {}} 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( + {}} 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( + {}} me={mockMe} /> + ); + }); + expect(WorkflowJobTemplatesAPI.readDetail).toBeCalled(); + expect(OrganizationsAPI.read).toBeCalled(); + }); + test('notifications tab shown for admins', async done => { + await act(async () => { + wrapper = mountWithContexts( + {}} 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( + {}} 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( + {}} 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( - ( - {}} 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( + {}} 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( + {}} 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(); }); }); diff --git a/awx/ui_next/src/screens/Template/shared/data.workflow_job_template.json b/awx/ui_next/src/screens/Template/shared/data.workflow_job_template.json new file mode 100644 index 0000000000..b120d7c892 --- /dev/null +++ b/awx/ui_next/src/screens/Template/shared/data.workflow_job_template.json @@ -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 +} From 3052e2077d7f19ca3e91c3911d6d273b9cfe8d21 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 3 Nov 2020 14:29:11 -0500 Subject: [PATCH 2/2] Import WorkflowJobTemplateEdit from the correct place --- awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index f13926a828..3e659ff002 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -22,7 +22,7 @@ import NotificationList from '../../components/NotificationList'; import { Schedules } from '../../components/Schedule'; import { ResourceAccessList } from '../../components/ResourceAccessList'; import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail'; -import WorkflowJobTemplateEdit from './JobTemplateEdit'; +import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit'; import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api'; import TemplateSurvey from './TemplateSurvey'; import { Visualizer } from './WorkflowJobTemplateVisualizer';