mirror of
https://github.com/ansible/awx.git
synced 2026-02-25 15:06:02 -03:30
Merge pull request #7085 from marshmalien/inv-src-detail-sync-btn
Add inventory source detail sync button Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -8,14 +8,19 @@ import AlertModal from '../../../components/AlertModal';
|
|||||||
import { CardBody, CardActionsRow } from '../../../components/Card';
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
import ChipGroup from '../../../components/ChipGroup';
|
import ChipGroup from '../../../components/ChipGroup';
|
||||||
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
import CredentialChip from '../../../components/CredentialChip';
|
import CredentialChip from '../../../components/CredentialChip';
|
||||||
import DeleteButton from '../../../components/DeleteButton';
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
|
import { FieldTooltip } from '../../../components/FormField';
|
||||||
|
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
|
||||||
import {
|
import {
|
||||||
DetailList,
|
DetailList,
|
||||||
Detail,
|
Detail,
|
||||||
UserDateDetail,
|
UserDateDetail,
|
||||||
} from '../../../components/DetailList';
|
} from '../../../components/DetailList';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
|
import useRequest from '../../../util/useRequest';
|
||||||
import { InventorySourcesAPI } from '../../../api';
|
import { InventorySourcesAPI } from '../../../api';
|
||||||
|
|
||||||
function InventorySourceDetail({ inventorySource, i18n }) {
|
function InventorySourceDetail({ inventorySource, i18n }) {
|
||||||
@@ -53,12 +58,28 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const isMounted = useRef(null);
|
const isMounted = useRef(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: sourceChoices,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
request: fetchSourceChoices,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await InventorySourcesAPI.readOptions();
|
||||||
|
return Object.assign(
|
||||||
|
...data.actions.GET.source.choices.map(([key, val]) => ({ [key]: val }))
|
||||||
|
);
|
||||||
|
}, []),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isMounted.current = true;
|
isMounted.current = true;
|
||||||
|
fetchSourceChoices();
|
||||||
return () => {
|
return () => {
|
||||||
isMounted.current = false;
|
isMounted.current = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [fetchSourceChoices]);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -68,9 +89,9 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
InventorySourcesAPI.destroy(id),
|
InventorySourcesAPI.destroy(id),
|
||||||
]);
|
]);
|
||||||
history.push(`/inventories/inventory/${inventory.id}/sources`);
|
history.push(`/inventories/inventory/${inventory.id}/sources`);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
setDeletionError(error);
|
setDeletionError(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -90,24 +111,87 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
) {
|
) {
|
||||||
optionsList = (
|
optionsList = (
|
||||||
<List>
|
<List>
|
||||||
{overwrite && <ListItem>{i18n._(t`Overwrite`)}</ListItem>}
|
{overwrite && (
|
||||||
{overwrite_vars && (
|
<ListItem>
|
||||||
<ListItem>{i18n._(t`Overwrite variables`)}</ListItem>
|
{i18n._(t`Overwrite`)}
|
||||||
|
<FieldTooltip
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
{i18n._(t`If checked, any hosts and groups that were
|
||||||
|
previously present on the external source but are now removed
|
||||||
|
will be removed from the Tower inventory. Hosts and groups
|
||||||
|
that were not managed by the inventory source will be promoted
|
||||||
|
to the next manually created group or if there is no manually
|
||||||
|
created group to promote them into, they will be left in the "all"
|
||||||
|
default group for the inventory.`)}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{i18n._(t`When not checked, local child
|
||||||
|
hosts and groups not found on the external source will remain
|
||||||
|
untouched by the inventory update process.`)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
{overwrite_vars && (
|
||||||
|
<ListItem>
|
||||||
|
{i18n._(t`Overwrite variables`)}
|
||||||
|
<FieldTooltip
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
{i18n._(t`If checked, all variables for child groups
|
||||||
|
and hosts will be removed and replaced by those found
|
||||||
|
on the external source.`)}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{i18n._(t`When not checked, a merge will be performed,
|
||||||
|
combining local variables with those found on the
|
||||||
|
external source.`)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
{update_on_launch && (
|
||||||
|
<ListItem>
|
||||||
|
{i18n._(t`Update on launch`)}
|
||||||
|
<FieldTooltip
|
||||||
|
content={i18n._(t`Each time a job runs using this inventory,
|
||||||
|
refresh the inventory from the selected source before
|
||||||
|
executing job tasks.`)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{update_on_launch && <ListItem>{i18n._(t`Update on launch`)}</ListItem>}
|
|
||||||
{update_on_project_update && (
|
{update_on_project_update && (
|
||||||
<ListItem>{i18n._(t`Update on project update`)}</ListItem>
|
<ListItem>
|
||||||
|
{i18n._(t`Update on project update`)}
|
||||||
|
<FieldTooltip
|
||||||
|
content={i18n._(t`After every project update where the SCM revision
|
||||||
|
changes, refresh the inventory from the selected source
|
||||||
|
before executing job tasks. This is intended for static content,
|
||||||
|
like the Ansible inventory .ini file format.`)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ContentError error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList>
|
<DetailList>
|
||||||
<Detail label={i18n._(t`Name`)} value={name} />
|
<Detail label={i18n._(t`Name`)} value={name} />
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
<Detail label={i18n._(t`Source`)} value={source} />
|
<Detail label={i18n._(t`Source`)} value={sourceChoices[source]} />
|
||||||
{organization && (
|
{organization && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Organization`)}
|
label={i18n._(t`Organization`)}
|
||||||
@@ -132,7 +216,10 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Detail label={i18n._(t`Inventory file`)} value={source_path} />
|
<Detail
|
||||||
|
label={i18n._(t`Inventory file`)}
|
||||||
|
value={source_path === '' ? i18n._(t`/ (project root)`) : source_path}
|
||||||
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Custom inventory script`)}
|
label={i18n._(t`Custom inventory script`)}
|
||||||
value={source_script?.name}
|
value={source_script?.name}
|
||||||
@@ -233,6 +320,9 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
{i18n._(t`Edit`)}
|
{i18n._(t`Edit`)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{user_capabilities?.start && (
|
||||||
|
<InventorySourceSyncButton source={inventorySource} icon={false} />
|
||||||
|
)}
|
||||||
{user_capabilities?.delete && (
|
{user_capabilities?.delete && (
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
name={name}
|
name={name}
|
||||||
|
|||||||
@@ -10,6 +10,30 @@ import mockInvSource from '../shared/data.inventory_source.json';
|
|||||||
import { InventorySourcesAPI } from '../../../api';
|
import { InventorySourcesAPI } from '../../../api';
|
||||||
|
|
||||||
jest.mock('../../../api/models/InventorySources');
|
jest.mock('../../../api/models/InventorySources');
|
||||||
|
InventorySourcesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {
|
||||||
|
source: {
|
||||||
|
choices: [
|
||||||
|
['file', 'File, Directory or Script'],
|
||||||
|
['scm', 'Sourced from a Project'],
|
||||||
|
['ec2', 'Amazon EC2'],
|
||||||
|
['gce', 'Google Compute Engine'],
|
||||||
|
['azure_rm', 'Microsoft Azure Resource Manager'],
|
||||||
|
['vmware', 'VMware vCenter'],
|
||||||
|
['satellite6', 'Red Hat Satellite 6'],
|
||||||
|
['cloudforms', 'Red Hat CloudForms'],
|
||||||
|
['openstack', 'OpenStack'],
|
||||||
|
['rhv', 'Red Hat Virtualization'],
|
||||||
|
['tower', 'Ansible Tower'],
|
||||||
|
['custom', 'Custom Script'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function assertDetail(wrapper, label, value) {
|
function assertDetail(wrapper, label, value) {
|
||||||
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||||
@@ -24,14 +48,17 @@ describe('InventorySourceDetail', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render expected details', () => {
|
test('should render expected details', async () => {
|
||||||
wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<InventorySourceDetail inventorySource={mockInvSource} />
|
wrapper = mountWithContexts(
|
||||||
);
|
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
expect(wrapper.find('InventorySourceDetail')).toHaveLength(1);
|
expect(wrapper.find('InventorySourceDetail')).toHaveLength(1);
|
||||||
assertDetail(wrapper, 'Name', 'mock inv source');
|
assertDetail(wrapper, 'Name', 'mock inv source');
|
||||||
assertDetail(wrapper, 'Description', 'mock description');
|
assertDetail(wrapper, 'Description', 'mock description');
|
||||||
assertDetail(wrapper, 'Source', 'scm');
|
assertDetail(wrapper, 'Source', 'Sourced from a Project');
|
||||||
assertDetail(wrapper, 'Organization', 'Mock Org');
|
assertDetail(wrapper, 'Organization', 'Mock Org');
|
||||||
assertDetail(wrapper, 'Ansible environment', '/venv/custom');
|
assertDetail(wrapper, 'Ansible environment', '/venv/custom');
|
||||||
assertDetail(wrapper, 'Project', 'Mock Project');
|
assertDetail(wrapper, 'Project', 'Mock Project');
|
||||||
@@ -69,44 +96,51 @@ describe('InventorySourceDetail', () => {
|
|||||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||||
'---\nfoo: bar'
|
'---\nfoo: bar'
|
||||||
);
|
);
|
||||||
expect(
|
wrapper.find('Detail[label="Options"] li').forEach(option => {
|
||||||
wrapper
|
expect([
|
||||||
.find('Detail[label="Options"]')
|
'Overwrite',
|
||||||
.containsAllMatchingElements([
|
'Overwrite variables',
|
||||||
<li>Overwrite</li>,
|
'Update on launch',
|
||||||
<li>Overwrite variables</li>,
|
'Update on project update',
|
||||||
<li>Update on launch</li>,
|
]).toContain(option.text());
|
||||||
<li>Update on project update</li>,
|
});
|
||||||
])
|
|
||||||
).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show edit and delete button for users with permissions', () => {
|
test('should display expected action buttons for users with permissions', async () => {
|
||||||
wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<InventorySourceDetail inventorySource={mockInvSource} />
|
wrapper = mountWithContexts(
|
||||||
);
|
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
const editButton = wrapper.find('Button[aria-label="edit"]');
|
const editButton = wrapper.find('Button[aria-label="edit"]');
|
||||||
expect(editButton.text()).toEqual('Edit');
|
expect(editButton.text()).toEqual('Edit');
|
||||||
expect(editButton.prop('to')).toBe(
|
expect(editButton.prop('to')).toBe(
|
||||||
'/inventories/inventory/2/sources/123/edit'
|
'/inventories/inventory/2/sources/123/edit'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('DeleteButton')).toHaveLength(1);
|
expect(wrapper.find('DeleteButton')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('InventorySourceSyncButton')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should hide edit and delete button for users without permissions', () => {
|
test('should hide expected action buttons for users without permissions', async () => {
|
||||||
const userCapabilities = {
|
const userCapabilities = {
|
||||||
edit: false,
|
edit: false,
|
||||||
delete: false,
|
delete: false,
|
||||||
|
start: false,
|
||||||
};
|
};
|
||||||
const invSource = {
|
const invSource = {
|
||||||
...mockInvSource,
|
...mockInvSource,
|
||||||
summary_fields: { ...userCapabilities },
|
summary_fields: { ...userCapabilities },
|
||||||
};
|
};
|
||||||
wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<InventorySourceDetail inventorySource={invSource} />
|
wrapper = mountWithContexts(
|
||||||
);
|
<InventorySourceDetail inventorySource={invSource} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
expect(wrapper.find('Button[aria-label="edit"]')).toHaveLength(0);
|
expect(wrapper.find('Button[aria-label="edit"]')).toHaveLength(0);
|
||||||
expect(wrapper.find('DeleteButton')).toHaveLength(0);
|
expect(wrapper.find('DeleteButton')).toHaveLength(0);
|
||||||
|
expect(wrapper.find('InventorySourceSyncButton')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('expected api call is made for delete', async () => {
|
test('expected api call is made for delete', async () => {
|
||||||
@@ -135,13 +169,33 @@ describe('InventorySourceDetail', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Content error shown for failed options request', async () => {
|
||||||
|
InventorySourcesAPI.readOptions.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
expect(InventorySourcesAPI.readOptions).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(InventorySourcesAPI.readOptions).toHaveBeenCalledTimes(1);
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
expect(wrapper.find('ContentError Title').text()).toEqual(
|
||||||
|
'Something went wrong...'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('Error dialog shown for failed deletion', async () => {
|
test('Error dialog shown for failed deletion', async () => {
|
||||||
InventorySourcesAPI.destroy.mockImplementationOnce(() =>
|
InventorySourcesAPI.destroy.mockImplementationOnce(() =>
|
||||||
Promise.reject(new Error())
|
Promise.reject(new Error())
|
||||||
);
|
);
|
||||||
wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<InventorySourceDetail inventorySource={mockInvSource} />
|
wrapper = mountWithContexts(
|
||||||
);
|
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
expect(wrapper.find('Modal[title="Error!"]')).toHaveLength(0);
|
expect(wrapper.find('Modal[title="Error!"]')).toHaveLength(0);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('DeleteButton').invoke('onConfirm')();
|
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import {
|
|||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
import StatusIcon from '../../../components/StatusIcon';
|
import StatusIcon from '../../../components/StatusIcon';
|
||||||
|
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
|
||||||
import InventorySourceSyncButton from './InventorySourceSyncButton';
|
|
||||||
|
|
||||||
function InventorySourceListItem({
|
function InventorySourceListItem({
|
||||||
source,
|
source,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import AlertModal from '../../../components/AlertModal/AlertModal';
|
|||||||
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
|
||||||
import { InventoryUpdatesAPI, InventorySourcesAPI } from '../../../api';
|
import { InventoryUpdatesAPI, InventorySourcesAPI } from '../../../api';
|
||||||
|
|
||||||
function InventorySourceSyncButton({ source, i18n }) {
|
function InventorySourceSyncButton({ source, icon, i18n }) {
|
||||||
const {
|
const {
|
||||||
isLoading: startSyncLoading,
|
isLoading: startSyncLoading,
|
||||||
error: startSyncError,
|
error: startSyncError,
|
||||||
@@ -43,21 +43,26 @@ function InventorySourceSyncButton({ source, i18n }) {
|
|||||||
}, [source.id])
|
}, [source.id])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(
|
const {
|
||||||
cancelSyncError || startSyncError
|
error: startError,
|
||||||
);
|
dismissError: dismissStartError,
|
||||||
|
} = useDismissableError(startSyncError);
|
||||||
|
const {
|
||||||
|
error: cancelError,
|
||||||
|
dismissError: dismissCancelError,
|
||||||
|
} = useDismissableError(cancelSyncError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{source.status === 'pending' ? (
|
{['running', 'pending', 'updating'].includes(source.status) ? (
|
||||||
<Tooltip content={i18n._(t`Cancel sync process`)} position="top">
|
<Tooltip content={i18n._(t`Cancel sync process`)} position="top">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={cancelSyncLoading || startSyncLoading}
|
isDisabled={cancelSyncLoading || startSyncLoading}
|
||||||
aria-label={i18n._(t`Cancel sync source`)}
|
aria-label={i18n._(t`Cancel sync source`)}
|
||||||
variant="plain"
|
variant={icon ? 'plain' : 'secondary'}
|
||||||
onClick={cancelSyncProcess}
|
onClick={cancelSyncProcess}
|
||||||
>
|
>
|
||||||
<MinusCircleIcon />
|
{icon ? <MinusCircleIcon /> : i18n._(t`Cancel sync`)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -65,24 +70,33 @@ function InventorySourceSyncButton({ source, i18n }) {
|
|||||||
<Button
|
<Button
|
||||||
isDisabled={cancelSyncLoading || startSyncLoading}
|
isDisabled={cancelSyncLoading || startSyncLoading}
|
||||||
aria-label={i18n._(t`Start sync source`)}
|
aria-label={i18n._(t`Start sync source`)}
|
||||||
variant="plain"
|
variant={icon ? 'plain' : 'secondary'}
|
||||||
onClick={startSyncProcess}
|
onClick={startSyncProcess}
|
||||||
>
|
>
|
||||||
<SyncIcon />
|
{icon ? <SyncIcon /> : i18n._(t`Sync`)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{startError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={error}
|
isOpen={startError}
|
||||||
variant="error"
|
variant="error"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={dismissError}
|
onClose={dismissStartError}
|
||||||
>
|
>
|
||||||
{startSyncError
|
{i18n._(t`Failed to sync inventory source.`)}
|
||||||
? i18n._(t`Failed to sync inventory source.`)
|
<ErrorDetail error={startError} />
|
||||||
: i18n._(t`Failed to cancel inventory source sync.`)}
|
</AlertModal>
|
||||||
<ErrorDetail error={error} />
|
)}
|
||||||
|
{cancelError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={cancelError}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissCancelError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to cancel inventory source sync.`)}
|
||||||
|
<ErrorDetail error={cancelError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -91,10 +105,12 @@ function InventorySourceSyncButton({ source, i18n }) {
|
|||||||
|
|
||||||
InventorySourceSyncButton.defaultProps = {
|
InventorySourceSyncButton.defaultProps = {
|
||||||
source: {},
|
source: {},
|
||||||
|
icon: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
InventorySourceSyncButton.propTypes = {
|
InventorySourceSyncButton.propTypes = {
|
||||||
source: PropTypes.shape({}),
|
source: PropTypes.shape({}),
|
||||||
|
icon: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(InventorySourceSyncButton);
|
export default withI18n()(InventorySourceSyncButton);
|
||||||
@@ -25,18 +25,19 @@ describe('<InventorySourceSyncButton />', () => {
|
|||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('should mount properly', async () => {
|
|
||||||
|
test('should mount properly', () => {
|
||||||
expect(wrapper.find('InventorySourceSyncButton').length).toBe(1);
|
expect(wrapper.find('InventorySourceSyncButton').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render start sync button', async () => {
|
test('should render start sync button', () => {
|
||||||
expect(wrapper.find('SyncIcon').length).toBe(1);
|
expect(wrapper.find('SyncIcon').length).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('Button[aria-label="Start sync source"]').prop('isDisabled')
|
wrapper.find('Button[aria-label="Start sync source"]').prop('isDisabled')
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render cancel sync button', async () => {
|
test('should render cancel sync button', () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<InventorySourceSyncButton
|
<InventorySourceSyncButton
|
||||||
source={{ status: 'pending', ...source }}
|
source={{ status: 'pending', ...source }}
|
||||||
@@ -57,6 +58,7 @@ describe('<InventorySourceSyncButton />', () => {
|
|||||||
);
|
);
|
||||||
expect(InventorySourcesAPI.createSyncStart).toBeCalledWith(1);
|
expect(InventorySourcesAPI.createSyncStart).toBeCalledWith(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should cancel sync properly', async () => {
|
test('should cancel sync properly', async () => {
|
||||||
InventorySourcesAPI.readDetail.mockResolvedValue({
|
InventorySourcesAPI.readDetail.mockResolvedValue({
|
||||||
data: { summary_fields: { current_update: { id: 120 } } },
|
data: { summary_fields: { current_update: { id: 120 } } },
|
||||||
@@ -83,6 +85,7 @@ describe('<InventorySourceSyncButton />', () => {
|
|||||||
expect(InventorySourcesAPI.readDetail).toBeCalledWith(1);
|
expect(InventorySourcesAPI.readDetail).toBeCalledWith(1);
|
||||||
expect(InventoryUpdatesAPI.createSyncCancel).toBeCalledWith(120);
|
expect(InventoryUpdatesAPI.createSyncCancel).toBeCalledWith(120);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error on sync start properly', async () => {
|
test('should throw error on sync start properly', async () => {
|
||||||
InventorySourcesAPI.createSyncStart.mockRejectedValueOnce(
|
InventorySourcesAPI.createSyncStart.mockRejectedValueOnce(
|
||||||
new Error({
|
new Error({
|
||||||
Reference in New Issue
Block a user