Adds spinner to loadiing and updating states

This commit is contained in:
Alex Corey 2020-12-15 16:09:28 -05:00
parent 766b2f774d
commit 47c1dc8171
18 changed files with 341 additions and 279 deletions

View File

@ -58,7 +58,7 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) {
return <ContentError error={error} />;
}
if (isLoading) {
return <ContentLoading error={error} />;
return <ContentLoading />;
}
return (
<Form>

View File

@ -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 }) => (
<EmptyState variant="full" className={className}>
<EmptyStateBody>{i18n._(t`Loading...`)}</EmptyStateBody>
</EmptyState>
);
const ContentLoading = ({ className }) => {
return (
<EmptyState variant="full" className={className}>
<EmptyStateIcon variant="container" component={Spinner} />
</EmptyState>
);
};
export { ContentLoading as _ContentLoading };
export default withI18n()(ContentLoading);
export default ContentLoading;

View File

@ -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 = () => (
<UpdatingContent>
<Spinner />
</UpdatingContent>
);
export default LoadingSpinner;

View File

@ -0,0 +1 @@
export { default } from './LoadingSpinner';

View File

@ -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 = (
<DataList
aria-label={dataListLabel}
onSelectDataListItem={id => handleListItemSelect(id)}
>
{items.map(renderItem)}
</DataList>
<>
{hasContentLoading && <LoadingSpinner />}
<DataList
aria-label={dataListLabel}
onSelectDataListItem={id => handleListItemSelect(id)}
>
{items.map(renderItem)}
</DataList>
</>
);
}

View File

@ -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 = (
<TableComposable aria-label={dataListLabel}>
{headerRow}
<Tbody>{items.map(renderRow)}</Tbody>
</TableComposable>
<>
{hasContentLoading && <LoadingSpinner />}
<TableComposable aria-label={dataListLabel}>
{headerRow}
<Tbody>{items.map(renderRow)}</Tbody>
</TableComposable>
</>
);
}

View File

@ -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,

View File

@ -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 (
<PageSection>
<Card>
<ContentLoading />
</Card>
</PageSection>
);
}
return (
<Fragment>
<Breadcrumbs breadcrumbConfig={{ '/home': i18n._(t`Dashboard`) }} />

View File

@ -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 (
<PageSection>
<Card>
@ -87,12 +80,12 @@ function Host({ i18n, setBreadcrumb }) {
);
}
if (contentError) {
if (error) {
return (
<PageSection>
<Card>
<ContentError error={contentError}>
{contentError?.response?.status === 404 && (
<ContentError error={error}>
{error?.response?.status === 404 && (
<span>
{i18n._(t`Host not found.`)}{' '}
<Link to="/hosts">{i18n._(t`View all Hosts.`)}</Link>

View File

@ -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('<Host />', () => {
beforeEach(async () => {
await act(async () => {
wrapper = mountWithContexts(<Host setBreadcrumb={() => {}} />);
wrapper = mountWithContexts(
<Route path="/hosts/:id/details">
<Host setBreadcrumb={() => {}} />
</Route>
);
});
});

View File

@ -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 <ContentError error={contentError} />;
if (instanceGroupsError) {
return <ContentError error={instanceGroupsError} />;
}
if (hasContentLoading) {
if (isLoadingInstanceGroups || isDeleteLoading) {
return <ContentLoading />;
}
@ -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`)}
</DeleteButton>

View File

@ -61,7 +61,6 @@ describe('<JobTemplateDetail />', () => {
});
test('should hide edit button for users without edit permission', async () => {
JobTemplatesAPI.readInstanceGroups.mockResolvedValue({ data: {} });
await act(async () => {
wrapper = mountWithContexts(
<JobTemplateDetail

View File

@ -85,6 +85,48 @@ function SurveyList({
const end = questions.slice(index + 2);
updateSurvey([...beginning, swapWith, question, ...end]);
};
const deleteModal = (
<AlertModal
variant="danger"
title={
isAllSelected ? i18n._(t`Delete Survey`) : i18n._(t`Delete Questions`)
}
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setSelected([]);
}}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={() => {
setIsDeleteModalOpen(false);
setSelected([]);
}}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
<div>{i18n._(t`This action will delete the following:`)}</div>
{selected.map(question => (
<span key={question.variable}>
<strong>{question.question_name}</strong>
<br />
</span>
))}
</AlertModal>
);
let content;
if (isLoading) {
@ -105,6 +147,7 @@ function SurveyList({
canEdit={canEdit}
/>
))}
{isDeleteModalOpen && deleteModal}
{isPreviewModalOpen && (
<SurveyPreviewModal
isPreviewModalOpen={isPreviewModalOpen}
@ -112,7 +155,6 @@ function SurveyList({
questions={questions}
/>
)}
<Button
onClick={() => setIsPreviewModalOpen(true)}
variant="primary"
@ -123,51 +165,8 @@ function SurveyList({
</DataList>
);
}
if (isDeleteModalOpen) {
return (
<AlertModal
variant="danger"
title={
isAllSelected ? i18n._(t`Delete Survey`) : i18n._(t`Delete Questions`)
}
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setSelected([]);
}}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={() => {
setIsDeleteModalOpen(false);
setSelected([]);
}}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
<div>{i18n._(t`This action will delete the following:`)}</div>
{selected.map(question => (
<span key={question.variable}>
<strong>{question.question_name}</strong>
<br />
</span>
))}
</AlertModal>
);
}
if (!questions || questions?.length <= 0) {
if ((!questions || questions?.length <= 0) && !isLoading) {
return (
<EmptyState variant="full">
<EmptyStateIcon icon={CubesIcon} />
@ -193,49 +192,6 @@ function SurveyList({
onToggleDeleteModal={() => setIsDeleteModalOpen(true)}
/>
{content}
{isDeleteModalOpen && (
<AlertModal
variant="danger"
title={
isAllSelected
? i18n._(t`Delete Survey`)
: i18n._(t`Delete Questions`)
}
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
}}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={() => {
setIsDeleteModalOpen(false);
}}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
<div>{i18n._(t`This action will delete the following:`)}</div>
<ul>
{selected.map(question => (
<li key={question.variable}>
<strong>{question.question_name}</strong>
</li>
))}
</ul>
</AlertModal>
)}
</>
);
}

View File

@ -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;

View File

@ -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 }) {
)}
<Route path="/templates/:templateType/:id/survey" exact>
<SurveyList
isLoading={isLoading}
isLoading={isLoading || updateLoading}
survey={survey}
surveyEnabled={surveyEnabled}
toggleSurvey={toggleSurvey}

View File

@ -1,11 +1,13 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import TemplateSurvey from './TemplateSurvey';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api';
import mockJobTemplateData from './shared/data.job_template.json';
import mockWorkflowJobTemplateData from './shared/data.workflow_job_template.json';
jest.mock('../../api/models/JobTemplates');
jest.mock('../../api/models/WorkflowJobTemplates');
@ -27,99 +29,8 @@ describe('<TemplateSurvey />', () => {
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(
<TemplateSurvey template={mockJobTemplateData} />,
{
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(
<TemplateSurvey template={{ ...mockJobTemplateData, id: 'a' }} />
);
});
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(
<TemplateSurvey template={mockJobTemplateData} />,
{
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(
<TemplateSurvey template={mockJobTemplateData} canEdit />,
{
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('<TemplateSurvey />', () => {
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(
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={{ ...mockJobTemplateData, id: 'a' }} />
</Route>,
{
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(
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={mockJobTemplateData} canEdit />
</Route>,
{
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(
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={mockJobTemplateData} canEdit />
</Route>,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { templateType: 'job_template', id: 7 },
},
},
},
},
@ -144,7 +163,49 @@ describe('<TemplateSurvey />', () => {
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(
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={mockWorkflowJobTemplateData} canEdit />
</Route>,
{
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,
});
});

View File

@ -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 <ContentLoading />;
}
if (!hasRolesandTemplateLoading && contentError) {
return (
<PageSection>

View File

@ -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,