mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
Adds spinner to loadiing and updating states
This commit is contained in:
parent
766b2f774d
commit
47c1dc8171
@ -58,7 +58,7 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <ContentLoading error={error} />;
|
||||
return <ContentLoading />;
|
||||
}
|
||||
return (
|
||||
<Form>
|
||||
|
||||
@ -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;
|
||||
|
||||
23
awx/ui_next/src/components/LoadingSpinner/LoadingSpinner.jsx
Normal file
23
awx/ui_next/src/components/LoadingSpinner/LoadingSpinner.jsx
Normal 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;
|
||||
1
awx/ui_next/src/components/LoadingSpinner/index.js
Normal file
1
awx/ui_next/src/components/LoadingSpinner/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './LoadingSpinner';
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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`) }} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user