From 47c1dc817153451847b99df4482438ea863b5997 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 15 Dec 2020 16:09:28 -0500 Subject: [PATCH] Adds spinner to loadiing and updating states --- .../AdHocCommands/AdHocCredentialStep.jsx | 2 +- .../ContentLoading/ContentLoading.jsx | 21 +- .../LoadingSpinner/LoadingSpinner.jsx | 23 ++ .../src/components/LoadingSpinner/index.js | 1 + .../PaginatedDataList/PaginatedDataList.jsx | 16 +- .../PaginatedTable/PaginatedTable.jsx | 12 +- .../Schedule/ScheduleList/ScheduleList.jsx | 2 +- .../src/screens/Dashboard/Dashboard.jsx | 13 +- awx/ui_next/src/screens/Host/Host.jsx | 37 ++- awx/ui_next/src/screens/Host/Host.test.jsx | 7 +- .../JobTemplateDetail/JobTemplateDetail.jsx | 47 ++-- .../JobTemplateDetail.test.jsx | 1 - .../screens/Template/Survey/SurveyList.jsx | 134 ++++------ awx/ui_next/src/screens/Template/Template.jsx | 8 +- .../src/screens/Template/TemplateSurvey.jsx | 40 +-- .../screens/Template/TemplateSurvey.test.jsx | 249 +++++++++++------- .../screens/Template/WorkflowJobTemplate.jsx | 5 + .../shared/data.workflow_job_template.json | 2 +- 18 files changed, 341 insertions(+), 279 deletions(-) create mode 100644 awx/ui_next/src/components/LoadingSpinner/LoadingSpinner.jsx create mode 100644 awx/ui_next/src/components/LoadingSpinner/index.js diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx index fa6f931c24..e95f0b05cb 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx @@ -58,7 +58,7 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) { return ; } if (isLoading) { - return ; + return ; } return (
diff --git a/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx b/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx index b1c51a6b8f..9808cc02af 100644 --- a/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx +++ b/awx/ui_next/src/components/ContentLoading/ContentLoading.jsx @@ -1,22 +1,25 @@ import React from 'react'; -import { t } from '@lingui/macro'; -import { withI18n } from '@lingui/react'; + import styled from 'styled-components'; import { EmptyState as PFEmptyState, - EmptyStateBody, + EmptyStateIcon, + Spinner, } from '@patternfly/react-core'; const EmptyState = styled(PFEmptyState)` --pf-c-empty-state--m-lg--MaxWidth: none; + min-height: 250px; `; // TODO: Better loading state - skeleton lines / spinner, etc. -const ContentLoading = ({ className, i18n }) => ( - - {i18n._(t`Loading...`)} - -); +const ContentLoading = ({ className }) => { + return ( + + + + ); +}; export { ContentLoading as _ContentLoading }; -export default withI18n()(ContentLoading); +export default ContentLoading; diff --git a/awx/ui_next/src/components/LoadingSpinner/LoadingSpinner.jsx b/awx/ui_next/src/components/LoadingSpinner/LoadingSpinner.jsx new file mode 100644 index 0000000000..36ed8adf5e --- /dev/null +++ b/awx/ui_next/src/components/LoadingSpinner/LoadingSpinner.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { Spinner } from '@patternfly/react-core'; +import styled from 'styled-components'; + +const UpdatingContent = styled.div` + position: fixed; + top: 50%; + left: 50%; + z-index: 300; + width: 100%; + height: 100%; + & + * { + opacity: 0.5; + } +`; + +const LoadingSpinner = () => ( + + + +); +export default LoadingSpinner; diff --git a/awx/ui_next/src/components/LoadingSpinner/index.js b/awx/ui_next/src/components/LoadingSpinner/index.js new file mode 100644 index 0000000000..6513c5cb53 --- /dev/null +++ b/awx/ui_next/src/components/LoadingSpinner/index.js @@ -0,0 +1 @@ +export { default } from './LoadingSpinner'; diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx index ab60173e75..7f5fe9afdd 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx @@ -22,6 +22,7 @@ import { import { QSConfig, SearchColumns, SortColumns } from '../../types'; import PaginatedDataListItem from './PaginatedDataListItem'; +import LoadingSpinner from '../LoadingSpinner'; function PaginatedDataList({ items, @@ -100,12 +101,15 @@ function PaginatedDataList({ ); } else { Content = ( - handleListItemSelect(id)} - > - {items.map(renderItem)} - + <> + {hasContentLoading && } + handleListItemSelect(id)} + > + {items.map(renderItem)} + + ); } diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx index 2176bd3b57..9892df34fe 100644 --- a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx +++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx @@ -11,6 +11,7 @@ import ContentError from '../ContentError'; import ContentLoading from '../ContentLoading'; import Pagination from '../Pagination'; import DataListToolbar from '../DataListToolbar'; +import LoadingSpinner from '../LoadingSpinner'; import { encodeNonDefaultQueryString, @@ -82,10 +83,13 @@ function PaginatedTable({ ); } else { Content = ( - - {headerRow} - {items.map(renderRow)} - + <> + {hasContentLoading && } + + {headerRow} + {items.map(renderRow)} + + ); } diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx index 957006022e..0bac50fdbc 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx @@ -62,7 +62,7 @@ function ScheduleList({ scheduleActions.data.actions?.GET || {} ).filter(key => scheduleActions.data.actions?.GET[key].filterable), }; - }, [location, loadSchedules, loadScheduleOptions]), + }, [location.search, loadSchedules, loadScheduleOptions]), { schedules: [], itemCount: 0, diff --git a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx index d348ce28f7..f60e049631 100644 --- a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx +++ b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx @@ -20,7 +20,7 @@ import useRequest from '../../util/useRequest'; import { DashboardAPI } from '../../api'; import Breadcrumbs from '../../components/Breadcrumbs'; import JobList from '../../components/JobList'; - +import ContentLoading from '../../components/ContentLoading'; import LineChart from './shared/LineChart'; import Count from './shared/Count'; import DashboardTemplateList from './shared/DashboardTemplateList'; @@ -62,6 +62,7 @@ function Dashboard({ i18n }) { const [activeTabId, setActiveTabId] = useState(0); const { + isLoading, result: { jobGraphData, countData }, request: fetchDashboardGraph, } = useRequest( @@ -105,7 +106,15 @@ function Dashboard({ i18n }) { useEffect(() => { fetchDashboardGraph(); }, [fetchDashboardGraph, periodSelection, jobTypeSelection]); - + if (isLoading) { + return ( + + + + + + ); + } return ( diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx index 33832e71ee..18a7f80edd 100644 --- a/awx/ui_next/src/screens/Host/Host.jsx +++ b/awx/ui_next/src/screens/Host/Host.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { @@ -20,29 +20,22 @@ import HostDetail from './HostDetail'; import HostEdit from './HostEdit'; import HostGroups from './HostGroups'; import { HostsAPI } from '../../api'; +import useRequest from '../../util/useRequest'; function Host({ i18n, setBreadcrumb }) { - const [host, setHost] = useState(null); - const [contentError, setContentError] = useState(null); - const [hasContentLoading, setHasContentLoading] = useState(true); - const location = useLocation(); const match = useRouteMatch('/hosts/:id'); + const { error, isLoading, result: host, request: fetchHost } = useRequest( + useCallback(async () => { + const { data } = await HostsAPI.readDetail(match.params.id); + setBreadcrumb(data); + return data; + }, [match.params.id, setBreadcrumb]) + ); useEffect(() => { - (async () => { - setContentError(null); - try { - const { data } = await HostsAPI.readDetail(match.params.id); - setHost(data); - setBreadcrumb(data); - } catch (error) { - setContentError(error); - } finally { - setHasContentLoading(false); - } - })(); - }, [match.params.id, location, setBreadcrumb]); + fetchHost(); + }, [fetchHost, location]); const tabsArray = [ { @@ -77,7 +70,7 @@ function Host({ i18n, setBreadcrumb }) { }, ]; - if (hasContentLoading) { + if (isLoading) { return ( @@ -87,12 +80,12 @@ function Host({ i18n, setBreadcrumb }) { ); } - if (contentError) { + if (error) { return ( - - {contentError?.response?.status === 404 && ( + + {error?.response?.status === 404 && ( {i18n._(t`Host not found.`)}{' '} {i18n._(t`View all Hosts.`)} diff --git a/awx/ui_next/src/screens/Host/Host.test.jsx b/awx/ui_next/src/screens/Host/Host.test.jsx index 26862760e9..a875d8777e 100644 --- a/awx/ui_next/src/screens/Host/Host.test.jsx +++ b/awx/ui_next/src/screens/Host/Host.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Route } from 'react-router-dom'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { HostsAPI } from '../../api'; @@ -28,7 +29,11 @@ describe('', () => { beforeEach(async () => { await act(async () => { - wrapper = mountWithContexts( {}} />); + wrapper = mountWithContexts( + + {}} /> + + ); }); }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index 8f3fe4ffcd..db3e45bc91 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -1,4 +1,4 @@ -import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import React, { Fragment, useCallback, useEffect } from 'react'; import { Link, useHistory, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { @@ -59,32 +59,31 @@ function JobTemplateDetail({ i18n, template }) { related: { webhook_receiver }, webhook_key, } = template; - const [contentError, setContentError] = useState(null); - const [hasContentLoading, setHasContentLoading] = useState(false); - const [instanceGroups, setInstanceGroups] = useState([]); const { id: templateId } = useParams(); const history = useHistory(); + const { + isLoading: isLoadingInstanceGroups, + request: fetchInstanceGroups, + error: instanceGroupsError, + result: { instanceGroups }, + } = useRequest( + useCallback(async () => { + const { + data: { results }, + } = await JobTemplatesAPI.readInstanceGroups(templateId); + return { instanceGroups: results }; + }, [templateId]), + { instanceGroups: [] } + ); + useEffect(() => { - (async () => { - setContentError(null); - setHasContentLoading(true); - try { - const { - data: { results = [] }, - } = await JobTemplatesAPI.readInstanceGroups(templateId); - setInstanceGroups(results); - } catch (error) { - setContentError(error); - } finally { - setHasContentLoading(false); - } - })(); - }, [templateId]); + fetchInstanceGroups(); + }, [fetchInstanceGroups]); const { request: deleteJobTemplate, - isLoading, + isLoading: isDeleteLoading, error: deleteError, } = useRequest( useCallback(async () => { @@ -154,11 +153,11 @@ function JobTemplateDetail({ i18n, template }) { ); }; - if (contentError) { - return ; + if (instanceGroupsError) { + return ; } - if (hasContentLoading) { + if (isLoadingInstanceGroups || isDeleteLoading) { return ; } @@ -389,7 +388,7 @@ function JobTemplateDetail({ i18n, template }) { name={name} modalTitle={i18n._(t`Delete Job Template`)} onConfirm={deleteJobTemplate} - isDisabled={isLoading} + isDisabled={isDeleteLoading} > {i18n._(t`Delete`)} diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx index 3f147ebaa0..1ed86103ab 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx @@ -61,7 +61,6 @@ describe('', () => { }); test('should hide edit button for users without edit permission', async () => { - JobTemplatesAPI.readInstanceGroups.mockResolvedValue({ data: {} }); await act(async () => { wrapper = mountWithContexts( { + setIsDeleteModalOpen(false); + setSelected([]); + }} + actions={[ + , + , + ]} + > +
{i18n._(t`This action will delete the following:`)}
+ {selected.map(question => ( + + {question.question_name} +
+
+ ))} + + ); let content; if (isLoading) { @@ -105,6 +147,7 @@ function SurveyList({ canEdit={canEdit} /> ))} + {isDeleteModalOpen && deleteModal} {isPreviewModalOpen && ( )} - , - , - ]} - > -
{i18n._(t`This action will delete the following:`)}
- {selected.map(question => ( - - {question.question_name} -
-
- ))} - - ); - } - if (!questions || questions?.length <= 0) { + + if ((!questions || questions?.length <= 0) && !isLoading) { return ( @@ -193,49 +192,6 @@ function SurveyList({ onToggleDeleteModal={() => setIsDeleteModalOpen(true)} /> {content} - {isDeleteModalOpen && ( - { - setIsDeleteModalOpen(false); - }} - actions={[ - , - , - ]} - > -
{i18n._(t`This action will delete the following:`)}
-
    - {selected.map(question => ( -
  • - {question.question_name} -
  • - ))} -
-
- )} ); } diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx index b6ff53db16..50b238b3b7 100644 --- a/awx/ui_next/src/screens/Template/Template.jsx +++ b/awx/ui_next/src/screens/Template/Template.jsx @@ -91,14 +91,14 @@ function Template({ i18n, setBreadcrumb }) { }; const loadScheduleOptions = useCallback(() => { - return JobTemplatesAPI.readScheduleOptions(template.id); - }, [template]); + return JobTemplatesAPI.readScheduleOptions(templateId); + }, [templateId]); const loadSchedules = useCallback( params => { - return JobTemplatesAPI.readSchedules(template.id, params); + return JobTemplatesAPI.readSchedules(templateId, params); }, - [template] + [templateId] ); const canSeeNotificationsTab = me?.is_system_auditor || isNotifAdmin; diff --git a/awx/ui_next/src/screens/Template/TemplateSurvey.jsx b/awx/ui_next/src/screens/Template/TemplateSurvey.jsx index 0badeffbf5..37b24cea67 100644 --- a/awx/ui_next/src/screens/Template/TemplateSurvey.jsx +++ b/awx/ui_next/src/screens/Template/TemplateSurvey.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Switch, Route, useParams, useLocation } from 'react-router-dom'; +import { Switch, Route, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api'; @@ -12,8 +12,7 @@ import { SurveyList, SurveyQuestionAdd, SurveyQuestionEdit } from './Survey'; function TemplateSurvey({ template, canEdit, i18n }) { const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled); - const { templateType } = useParams(); - const location = useLocation(); + const { templateType, id: templateId } = useParams(); const { result: survey, @@ -25,30 +24,31 @@ function TemplateSurvey({ template, canEdit, i18n }) { useCallback(async () => { const { data } = templateType === 'workflow_job_template' - ? await WorkflowJobTemplatesAPI.readSurvey(template.id) - : await JobTemplatesAPI.readSurvey(template.id); + ? await WorkflowJobTemplatesAPI.readSurvey(templateId) + : await JobTemplatesAPI.readSurvey(templateId); return data; - }, [template.id, templateType]) + }, [templateId, templateType]) ); useEffect(() => { fetchSurvey(); - }, [fetchSurvey, location]); + }, [fetchSurvey]); - const { request: updateSurvey, error: updateError } = useRequest( + const { + request: updateSurvey, + error: updateError, + isLoading: updateLoading, + } = useRequest( useCallback( async updatedSurvey => { if (templateType === 'workflow_job_template') { - await WorkflowJobTemplatesAPI.updateSurvey( - template.id, - updatedSurvey - ); + await WorkflowJobTemplatesAPI.updateSurvey(templateId, updatedSurvey); } else { - await JobTemplatesAPI.updateSurvey(template.id, updatedSurvey); + await JobTemplatesAPI.updateSurvey(templateId, updatedSurvey); } setSurvey(updatedSurvey); }, - [template.id, setSurvey, templateType] + [templateId, setSurvey, templateType] ) ); const updateSurveySpec = spec => { @@ -61,24 +61,24 @@ function TemplateSurvey({ template, canEdit, i18n }) { const { request: deleteSurvey, error: deleteError } = useRequest( useCallback(async () => { - await JobTemplatesAPI.destroySurvey(template.id); + await JobTemplatesAPI.destroySurvey(templateId); setSurvey(null); - }, [template.id, setSurvey]) + }, [templateId, setSurvey]) ); const { request: toggleSurvey, error: toggleError } = useRequest( useCallback(async () => { if (templateType === 'workflow_job_template') { - await WorkflowJobTemplatesAPI.update(template.id, { + await WorkflowJobTemplatesAPI.update(templateId, { survey_enabled: !surveyEnabled, }); } else { - await JobTemplatesAPI.update(template.id, { + await JobTemplatesAPI.update(templateId, { survey_enabled: !surveyEnabled, }); } setSurveyEnabled(!surveyEnabled); - }, [template.id, templateType, surveyEnabled]) + }, [templateId, templateType, surveyEnabled]) ); const { error, dismissError } = useDismissableError( @@ -109,7 +109,7 @@ function TemplateSurvey({ template, canEdit, i18n }) { )} ', () => { test('should fetch survey from API', async () => { const history = createMemoryHistory({ - initialEntries: ['/templates/job_template/1/survey'], + initialEntries: ['/templates/job_template/7/survey'], }); - let wrapper; - await act(async () => { - wrapper = mountWithContexts( - , - { - context: { router: { history } }, - } - ); - }); - wrapper.update(); - expect(JobTemplatesAPI.readSurvey).toBeCalledWith(7); - - expect(wrapper.find('SurveyList').prop('survey')).toEqual(surveyData); - }); - - test('should display error in retrieving survey', async () => { - JobTemplatesAPI.readSurvey.mockRejectedValue(new Error()); - let wrapper; - await act(async () => { - wrapper = mountWithContexts( - - ); - }); - - wrapper.update(); - - expect(wrapper.find('ContentError').length).toBe(1); - }); - - test('should update API with survey changes', async () => { - const history = createMemoryHistory({ - initialEntries: ['/templates/job_template/1/survey'], - }); - let wrapper; - await act(async () => { - wrapper = mountWithContexts( - , - { - context: { router: { history } }, - } - ); - }); - wrapper.update(); - - await act(async () => { - await wrapper.find('SurveyList').invoke('updateSurvey')([ - { question_name: 'Foo', type: 'text', default: 'One', variable: 'foo' }, - { question_name: 'Bar', type: 'text', default: 'Two', variable: 'bar' }, - ]); - }); - expect(JobTemplatesAPI.updateSurvey).toHaveBeenCalledWith(7, { - name: 'Survey', - description: 'description for survey', - spec: [ - { question_name: 'Foo', type: 'text', default: 'One', variable: 'foo' }, - { question_name: 'Bar', type: 'text', default: 'Two', variable: 'bar' }, - ], - }); - }); - - test('should toggle jt survery on', async () => { - const history = createMemoryHistory({ - initialEntries: ['/templates/job_template/1/survey'], - }); - let wrapper; - await act(async () => { - wrapper = mountWithContexts( - , - { - context: { router: { history } }, - } - ); - }); - wrapper.update(); - await act(() => - wrapper.find('Switch[aria-label="Survey Toggle"]').prop('onChange')() - ); - wrapper.update(); - - expect(JobTemplatesAPI.update).toBeCalledWith(7, { survey_enabled: false }); - }); - - test('should toggle wfjt survey on', async () => { - const history = createMemoryHistory({ - initialEntries: ['/templates/workflow_job_template/1/survey'], - }); - - WorkflowJobTemplatesAPI.readSurvey.mockResolvedValueOnce({ - data: surveyData, - }); - let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -132,7 +43,115 @@ describe('', () => { history, route: { location: history.location, - match: { params: { templateType: 'workflow_job_template' } }, + match: { + params: { templateType: 'job_template', id: 7 }, + }, + }, + }, + }, + } + ); + }); + wrapper.update(); + expect(JobTemplatesAPI.readSurvey).toBeCalledWith('7'); + + expect(wrapper.find('SurveyList').prop('survey')).toEqual(surveyData); + }); + + test('should display error in retrieving survey', async () => { + JobTemplatesAPI.readSurvey.mockRejectedValue(new Error()); + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/7/survey'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { templateType: 'job_template', id: 7 }, + }, + }, + }, + }, + } + ); + }); + + wrapper.update(); + + expect(wrapper.find('ContentError').length).toBe(1); + }); + + test('should update API with survey changes', async () => { + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/7/survey'], + }); + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { templateType: 'job_template', id: 7 }, + }, + }, + }, + }, + } + ); + }); + wrapper.update(); + + await act(async () => { + await wrapper.find('SurveyList').invoke('updateSurvey')([ + { question_name: 'Foo', type: 'text', default: 'One', variable: 'foo' }, + { question_name: 'Bar', type: 'text', default: 'Two', variable: 'bar' }, + ]); + }); + expect(JobTemplatesAPI.updateSurvey).toHaveBeenCalledWith('7', { + name: 'Survey', + description: 'description for survey', + spec: [ + { question_name: 'Foo', type: 'text', default: 'One', variable: 'foo' }, + { question_name: 'Bar', type: 'text', default: 'Two', variable: 'bar' }, + ], + }); + }); + + test('should toggle jt survery on', async () => { + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/7/survey'], + }); + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { templateType: 'job_template', id: 7 }, + }, }, }, }, @@ -144,7 +163,49 @@ describe('', () => { wrapper.find('Switch[aria-label="Survey Toggle"]').prop('onChange')() ); wrapper.update(); - expect(WorkflowJobTemplatesAPI.update).toBeCalledWith(7, { + + expect(JobTemplatesAPI.update).toBeCalledWith('7', { + survey_enabled: false, + }); + }); + + test('should toggle wfjt survey on', async () => { + const history = createMemoryHistory({ + initialEntries: ['/templates/workflow_job_template/15/survey'], + }); + + WorkflowJobTemplatesAPI.readSurvey.mockResolvedValueOnce({ + data: surveyData, + }); + + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { + params: { templateType: 'workflow_job_template', id: 15 }, + }, + }, + }, + }, + } + ); + }); + wrapper.update(); + await act(() => + wrapper.find('Switch[aria-label="Survey Toggle"]').prop('onChange')() + ); + + wrapper.update(); + expect(WorkflowJobTemplatesAPI.update).toBeCalledWith('15', { survey_enabled: false, }); }); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index 4b997fa9b9..ee22010983 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -27,6 +27,7 @@ import WorkflowJobTemplateEdit from './WorkflowJobTemplateEdit'; import { WorkflowJobTemplatesAPI, OrganizationsAPI } from '../../api'; import TemplateSurvey from './TemplateSurvey'; import { Visualizer } from './WorkflowJobTemplateVisualizer'; +import ContentLoading from '../../components/ContentLoading'; function WorkflowJobTemplate({ i18n, setBreadcrumb }) { const location = useLocation(); @@ -150,6 +151,10 @@ function WorkflowJobTemplate({ i18n, setBreadcrumb }) { } const contentError = rolesAndTemplateError; + + if (hasRolesandTemplateLoading) { + return ; + } if (!hasRolesandTemplateLoading && contentError) { return ( 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 index b120d7c892..ae4e6fb6d4 100644 --- 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 @@ -81,7 +81,7 @@ "status": "never updated", "extra_vars": "", "organization": null, - "survey_enabled": false, + "survey_enabled": true, "allow_simultaneous": false, "ask_variables_on_launch": false, "inventory": null,