mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 03:10:42 -03:30
Add inventory source details
This commit is contained in:
parent
ff573e06b3
commit
4e8bbdaae7
@ -72,6 +72,22 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
async readSourceDetail(inventoryId, sourceId) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await this.http.get(
|
||||
`${this.baseUrl}${inventoryId}/inventory_sources/?id=${sourceId}`
|
||||
);
|
||||
|
||||
if (Array.isArray(results) && results.length) {
|
||||
return results[0];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`How did you get here? Source not found for Inventory ID: ${inventoryId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Inventories;
|
||||
|
||||
@ -7,6 +7,8 @@ class InventorySources extends LaunchUpdateMixin(Base) {
|
||||
this.baseUrl = '/api/v2/inventory_sources/';
|
||||
|
||||
this.createSyncStart = this.createSyncStart.bind(this);
|
||||
this.destroyGroups = this.destroyGroups.bind(this);
|
||||
this.destroyHosts = this.destroyHosts.bind(this);
|
||||
}
|
||||
|
||||
createSyncStart(sourceId, extraVars) {
|
||||
@ -14,5 +16,13 @@ class InventorySources extends LaunchUpdateMixin(Base) {
|
||||
extra_vars: extraVars,
|
||||
});
|
||||
}
|
||||
|
||||
destroyGroups(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/groups/`);
|
||||
}
|
||||
|
||||
destroyHosts(id) {
|
||||
return this.http.delete(`${this.baseUrl}${id}/hosts/`);
|
||||
}
|
||||
}
|
||||
export default InventorySources;
|
||||
|
||||
@ -77,6 +77,8 @@ class Inventories extends Component {
|
||||
|
||||
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
||||
[`${inventorySourcesPath}/add`]: i18n._(t`Create New Source`),
|
||||
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
||||
@ -93,7 +93,7 @@ function Inventory({ i18n, setBreadcrumb }) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
{['edit', 'add', 'groups/', 'hosts/'].some(name =>
|
||||
{['edit', 'add', 'groups/', 'hosts/', 'sources/'].some(name =>
|
||||
location.pathname.includes(name)
|
||||
) ? null : (
|
||||
<TabbedCardHeader>
|
||||
@ -138,7 +138,10 @@ function Inventory({ i18n, setBreadcrumb }) {
|
||||
/>
|
||||
</Route>,
|
||||
<Route path="/inventories/inventory/:id/sources" key="sources">
|
||||
<InventorySources inventory={inventory} />
|
||||
<InventorySources
|
||||
inventory={inventory}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
/>
|
||||
</Route>,
|
||||
<Route
|
||||
path="/inventories/inventory/:id/completed_jobs"
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Link,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
useRouteMatch,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import useRequest from '@util/useRequest';
|
||||
|
||||
import { InventoriesAPI } from '@api';
|
||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import { CardActions } from '@patternfly/react-core';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import InventorySourceDetail from '../InventorySourceDetail';
|
||||
|
||||
function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch('/inventories/inventory/:id/sources/:sourceId');
|
||||
const sourceListUrl = `/inventories/inventory/${inventory.id}/sources`;
|
||||
|
||||
const { result: source, error, isLoading, request: fetchSource } = useRequest(
|
||||
useCallback(async () => {
|
||||
return InventoriesAPI.readSourceDetail(
|
||||
inventory.id,
|
||||
match.params.sourceId
|
||||
);
|
||||
}, [inventory.id, match.params.sourceId]),
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSource();
|
||||
}, [fetchSource, match.params.sourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inventory && source) {
|
||||
setBreadcrumb(inventory, source);
|
||||
}
|
||||
}, [inventory, source, setBreadcrumb]);
|
||||
|
||||
const tabsArray = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<CaretLeftIcon />
|
||||
{i18n._(t`Back to Sources`)}
|
||||
</>
|
||||
),
|
||||
link: `${sourceListUrl}`,
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
link: `${match.url}/details`,
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Notifications`),
|
||||
link: `${match.url}/notifications`,
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Schedules`),
|
||||
link: `${match.url}/schedules`,
|
||||
id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{['edit'].some(name => location.pathname.includes(name)) ? null : (
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<CardActions>
|
||||
<CardCloseButton linkTo={sourceListUrl} />
|
||||
</CardActions>
|
||||
</TabbedCardHeader>
|
||||
)}
|
||||
|
||||
{isLoading && <ContentLoading />}
|
||||
|
||||
{!isLoading && source && (
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/inventory/:id/sources/:sourceId"
|
||||
to="/inventories/inventory/:id/sources/:sourceId/details"
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/inventory/:id/sources/:sourceId/details"
|
||||
>
|
||||
<InventorySourceDetail inventorySource={source} />
|
||||
</Route>
|
||||
<Route key="not-found" path="*">
|
||||
<ContentError isNotFound>
|
||||
<Link to={`${match.url}/details`}>
|
||||
{i18n._(`View inventory source details`)}
|
||||
</Link>
|
||||
</ContentError>
|
||||
</Route>
|
||||
</Switch>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(InventorySource);
|
||||
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { InventoriesAPI } from '@api';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import mockInventorySource from '../shared/data.inventory_source.json';
|
||||
import InventorySource from './InventorySource';
|
||||
|
||||
jest.mock('@api/models/Inventories');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useRouteMatch: () => ({
|
||||
url: '/inventories/inventory/2/sources/123',
|
||||
params: { id: 2, sourceId: 123 },
|
||||
}),
|
||||
}));
|
||||
|
||||
InventoriesAPI.readSourceDetail.mockResolvedValue({
|
||||
data: { ...mockInventorySource },
|
||||
});
|
||||
|
||||
const mockInventory = {
|
||||
id: 2,
|
||||
name: 'Mock Inventory',
|
||||
};
|
||||
|
||||
describe('<InventorySource />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySource inventory={mockInventory} setBreadcrumb={() => {}} />
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should render expected tabs', () => {
|
||||
const expectedTabs = [
|
||||
'Back to Sources',
|
||||
'Details',
|
||||
'Notifications',
|
||||
'Schedules',
|
||||
];
|
||||
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
InventoriesAPI.readSourceDetail.mockRejectedValueOnce(new Error());
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySource inventory={mockInventory} setBreadcrumb={() => {}} />
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
expect(wrapper.find('ContentError Title').text()).toEqual(
|
||||
'Something went wrong...'
|
||||
);
|
||||
});
|
||||
|
||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/2/sources/1/foobar'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySource inventory={mockInventory} setBreadcrumb={() => {}} />,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
expect(wrapper.find('ContentError Title').text()).toEqual('Not Found');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventorySource';
|
||||
@ -0,0 +1,255 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
List,
|
||||
ListItem,
|
||||
} from '@patternfly/react-core';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import { CardBody, CardActionsRow } from '@components/Card';
|
||||
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||
import CredentialChip from '@components/CredentialChip';
|
||||
import DeleteButton from '@components/DeleteButton';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import { InventorySourcesAPI } from '@api';
|
||||
|
||||
function InventorySourceDetail({ inventorySource, i18n }) {
|
||||
const {
|
||||
created,
|
||||
custom_virtualenv,
|
||||
description,
|
||||
group_by,
|
||||
id,
|
||||
instance_filters,
|
||||
modified,
|
||||
name,
|
||||
overwrite,
|
||||
overwrite_vars,
|
||||
source,
|
||||
source_path,
|
||||
source_regions,
|
||||
source_vars,
|
||||
update_cache_timeout,
|
||||
update_on_launch,
|
||||
update_on_project_update,
|
||||
verbosity,
|
||||
summary_fields: {
|
||||
created_by,
|
||||
credentials,
|
||||
inventory,
|
||||
modified_by,
|
||||
organization,
|
||||
source_project,
|
||||
source_script,
|
||||
user_capabilities,
|
||||
},
|
||||
} = inventorySource;
|
||||
const [deletionError, setDeletionError] = useState(false);
|
||||
const history = useHistory();
|
||||
const isMounted = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
InventorySourcesAPI.destroyHosts(id),
|
||||
InventorySourcesAPI.destroyGroups(id),
|
||||
InventorySourcesAPI.destroy(id),
|
||||
]);
|
||||
history.push(`/inventories/inventory/${inventory.id}/sources`);
|
||||
} catch (error) {
|
||||
if (isMounted.current) {
|
||||
setDeletionError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const VERBOSITY = {
|
||||
0: i18n._(t`0 (Warning)`),
|
||||
1: i18n._(t`1 (Info)`),
|
||||
2: i18n._(t`2 (Debug)`),
|
||||
};
|
||||
|
||||
let optionsList = '';
|
||||
if (
|
||||
overwrite ||
|
||||
overwrite_vars ||
|
||||
update_on_launch ||
|
||||
update_on_project_update
|
||||
) {
|
||||
optionsList = (
|
||||
<List>
|
||||
{overwrite && <ListItem>{i18n._(t`Overwrite`)}</ListItem>}
|
||||
{overwrite_vars && (
|
||||
<ListItem>{i18n._(t`Overwrite variables`)}</ListItem>
|
||||
)}
|
||||
{update_on_launch && <ListItem>{i18n._(t`Update on launch`)}</ListItem>}
|
||||
{update_on_project_update && (
|
||||
<ListItem>{i18n._(t`Update on project update`)}</ListItem>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<DetailList>
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<Detail label={i18n._(t`Source`)} value={source} />
|
||||
{organization && (
|
||||
<Detail
|
||||
label={i18n._(t`Organization`)}
|
||||
value={
|
||||
<Link to={`/organizations/${organization.id}/details`}>
|
||||
{organization.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`Ansible environment`)}
|
||||
value={custom_virtualenv}
|
||||
/>
|
||||
{source_project && (
|
||||
<Detail
|
||||
label={i18n._(t`Project`)}
|
||||
value={
|
||||
<Link to={`/projects/${source_project.id}/details`}>
|
||||
{source_project.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Inventory file`)} value={source_path} />
|
||||
<Detail
|
||||
label={i18n._(t`Custom inventory script`)}
|
||||
value={source_script?.name}
|
||||
/>
|
||||
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
|
||||
<Detail
|
||||
label={i18n._(t`Cache timeout`)}
|
||||
value={`${update_cache_timeout} ${i18n._(t`seconds`)}`}
|
||||
/>
|
||||
{credentials?.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Credential`)}
|
||||
value={credentials.map(cred => (
|
||||
<CredentialChip key={cred?.id} credential={cred} isReadOnly />
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
{source_regions && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Regions`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{source_regions.split(',').map(region => (
|
||||
<Chip key={region} isReadOnly>
|
||||
{region}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{instance_filters && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Instance filters`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{instance_filters.split(',').map(filter => (
|
||||
<Chip key={filter} isReadOnly>
|
||||
{filter}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{group_by && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Only group by`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{group_by.split(',').map(group => (
|
||||
<Chip key={group} isReadOnly>
|
||||
{group}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{optionsList && (
|
||||
<Detail fullWidth label={i18n._(t`Options`)} value={optionsList} />
|
||||
)}
|
||||
{source_vars && (
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Source variables`)}
|
||||
rows={4}
|
||||
value={source_vars}
|
||||
/>
|
||||
)}
|
||||
<UserDateDetail
|
||||
date={created}
|
||||
label={i18n._(t`Created`)}
|
||||
user={created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
date={modified}
|
||||
label={i18n._(t`Last modified`)}
|
||||
user={modified_by}
|
||||
/>
|
||||
</DetailList>
|
||||
<CardActionsRow>
|
||||
{user_capabilities?.edit && (
|
||||
<Button
|
||||
component={Link}
|
||||
aria-label={i18n._(t`edit`)}
|
||||
to={`/inventories/inventory/${inventory.id}/source/${id}/edit`}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{user_capabilities?.delete && (
|
||||
<DeleteButton
|
||||
name={name}
|
||||
modalTitle={i18n._(t`Delete inventory source`)}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
</CardActionsRow>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
isOpen={deletionError}
|
||||
onClose={() => setDeletionError(false)}
|
||||
>
|
||||
{i18n._(t`Failed to delete inventory source ${name}.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
export default withI18n()(InventorySourceDetail);
|
||||
@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import InventorySourceDetail from './InventorySourceDetail';
|
||||
import mockInvSource from '../shared/data.inventory_source.json';
|
||||
import { InventorySourcesAPI } from '@api';
|
||||
|
||||
jest.mock('@api/models/InventorySources');
|
||||
|
||||
function assertDetail(wrapper, label, value) {
|
||||
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||
}
|
||||
|
||||
describe('InventorySourceDetail', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render expected details', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||
);
|
||||
expect(wrapper.find('InventorySourceDetail')).toHaveLength(1);
|
||||
assertDetail(wrapper, 'Name', 'mock inv source');
|
||||
assertDetail(wrapper, 'Description', 'mock description');
|
||||
assertDetail(wrapper, 'Source', 'scm');
|
||||
assertDetail(wrapper, 'Organization', 'Mock Org');
|
||||
assertDetail(wrapper, 'Ansible environment', '/venv/custom');
|
||||
assertDetail(wrapper, 'Project', 'Mock Project');
|
||||
assertDetail(wrapper, 'Inventory file', 'foo');
|
||||
assertDetail(wrapper, 'Custom inventory script', 'Mock Script');
|
||||
assertDetail(wrapper, 'Verbosity', '2 (Debug)');
|
||||
assertDetail(wrapper, 'Cache timeout', '2 seconds');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Regions"]')
|
||||
.containsAllMatchingElements([
|
||||
<span>us-east-1</span>,
|
||||
<span>us-east-2</span>,
|
||||
])
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Instance filters"]')
|
||||
.containsAllMatchingElements([
|
||||
<span>filter1</span>,
|
||||
<span>filter2</span>,
|
||||
<span>filter3</span>,
|
||||
])
|
||||
).toEqual(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Only group by"]')
|
||||
.containsAllMatchingElements([
|
||||
<span>group1</span>,
|
||||
<span>group2</span>,
|
||||
<span>group3</span>,
|
||||
])
|
||||
).toEqual(true);
|
||||
expect(wrapper.find('CredentialChip').text()).toBe('Cloud: mock cred');
|
||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||
'---\nfoo: bar'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Options"]')
|
||||
.containsAllMatchingElements([
|
||||
<li>Overwrite</li>,
|
||||
<li>Overwrite variables</li>,
|
||||
<li>Update on launch</li>,
|
||||
<li>Update on project update</li>,
|
||||
])
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('should show edit and delete button for users with permissions', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||
);
|
||||
const editButton = wrapper.find('Button[aria-label="edit"]');
|
||||
expect(editButton.text()).toEqual('Edit');
|
||||
expect(editButton.prop('to')).toBe(
|
||||
'/inventories/inventory/2/source/123/edit'
|
||||
);
|
||||
expect(wrapper.find('DeleteButton')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should hide edit and delete button for users without permissions', () => {
|
||||
const userCapabilities = {
|
||||
edit: false,
|
||||
delete: false,
|
||||
};
|
||||
const invSource = {
|
||||
...mockInvSource,
|
||||
summary_fields: { ...userCapabilities },
|
||||
};
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySourceDetail inventorySource={invSource} />
|
||||
);
|
||||
expect(wrapper.find('Button[aria-label="edit"]')).toHaveLength(0);
|
||||
expect(wrapper.find('DeleteButton')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('expected api call is made for delete', async () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/2/sources/123/details'],
|
||||
});
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySourceDetail inventorySource={mockInvSource} />,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(history.location.pathname).toEqual(
|
||||
'/inventories/inventory/2/sources/123/details'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||
});
|
||||
expect(InventorySourcesAPI.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(InventorySourcesAPI.destroyHosts).toHaveBeenCalledTimes(1);
|
||||
expect(InventorySourcesAPI.destroyGroups).toHaveBeenCalledTimes(1);
|
||||
expect(history.location.pathname).toEqual(
|
||||
'/inventories/inventory/2/sources'
|
||||
);
|
||||
});
|
||||
|
||||
test('Error dialog shown for failed deletion', async () => {
|
||||
InventorySourcesAPI.destroy.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
wrapper = mountWithContexts(
|
||||
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||
);
|
||||
expect(wrapper.find('Modal[title="Error!"]')).toHaveLength(0);
|
||||
await act(async () => {
|
||||
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'Modal[title="Error!"]',
|
||||
el => el.length === 1
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('Modal[title="Error!"]').invoke('onClose')();
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'Modal[title="Error!"]',
|
||||
el => el.length === 0
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventorySourceDetail';
|
||||
@ -1,14 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import InventorySource from '../InventorySource';
|
||||
import InventorySourceAdd from '../InventorySourceAdd';
|
||||
import InventorySourceList from './InventorySourceList';
|
||||
|
||||
function InventorySources() {
|
||||
function InventorySources({ inventory, setBreadcrumb }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route key="add" path="/inventories/inventory/:id/sources/add">
|
||||
<InventorySourceAdd />
|
||||
</Route>
|
||||
<Route path="/inventories/inventory/:id/sources/:sourceId">
|
||||
<InventorySource inventory={inventory} setBreadcrumb={setBreadcrumb} />
|
||||
</Route>
|
||||
<Route path="/inventories/:inventoryType/:id/sources">
|
||||
<InventorySourceList />
|
||||
</Route>
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
{
|
||||
"id":123,
|
||||
"type":"inventory_source",
|
||||
"url":"/api/v2/inventory_sources/123/",
|
||||
"related":{
|
||||
"named_url":"/api/v2/inventory_sources/src++Demo Inventory++Default/",
|
||||
"created_by":"/api/v2/users/1/",
|
||||
"modified_by":"/api/v2/users/1/",
|
||||
"update":"/api/v2/inventory_sources/123/update/",
|
||||
"inventory_updates":"/api/v2/inventory_sources/123/inventory_updates/",
|
||||
"schedules":"/api/v2/inventory_sources/123/schedules/",
|
||||
"activity_stream":"/api/v2/inventory_sources/123/activity_stream/",
|
||||
"hosts":"/api/v2/inventory_sources/123/hosts/",
|
||||
"groups":"/api/v2/inventory_sources/123/groups/",
|
||||
"notification_templates_started":"/api/v2/inventory_sources/123/notification_templates_started/",
|
||||
"notification_templates_success":"/api/v2/inventory_sources/123/notification_templates_success/",
|
||||
"notification_templates_error":"/api/v2/inventory_sources/123/notification_templates_error/",
|
||||
"inventory":"/api/v2/inventories/1/",
|
||||
"source_project":"/api/v2/projects/8/",
|
||||
"credentials":"/api/v2/inventory_sources/123/credentials/"
|
||||
},
|
||||
"summary_fields":{
|
||||
"organization":{
|
||||
"id":1,
|
||||
"name":"Mock Org",
|
||||
"description":""
|
||||
},
|
||||
"inventory":{
|
||||
"id":2,
|
||||
"name":"Mock Inventory",
|
||||
"description":"",
|
||||
"has_active_failures":false,
|
||||
"total_hosts":1,
|
||||
"hosts_with_active_failures":0,
|
||||
"total_groups":2,
|
||||
"has_inventory_sources":true,
|
||||
"total_inventory_sources":5,
|
||||
"inventory_sources_with_failures":0,
|
||||
"organization_id":1,
|
||||
"kind":""
|
||||
},
|
||||
"source_project":{
|
||||
"id":8,
|
||||
"name":"Mock Project",
|
||||
"description":"",
|
||||
"status":"never updated",
|
||||
"scm_type":"git"
|
||||
},
|
||||
"source_script": {
|
||||
"name": "Mock Script",
|
||||
"description": ""
|
||||
},
|
||||
"created_by":{
|
||||
"id":1,
|
||||
"username":"admin",
|
||||
"first_name":"",
|
||||
"last_name":""
|
||||
},
|
||||
"modified_by":{
|
||||
"id":1,
|
||||
"username":"admin",
|
||||
"first_name":"",
|
||||
"last_name":""
|
||||
},
|
||||
"user_capabilities":{
|
||||
"edit":true,
|
||||
"delete":true,
|
||||
"start":true,
|
||||
"schedule":true
|
||||
},
|
||||
"credential": {
|
||||
"id": 8,
|
||||
"name": "mock cred",
|
||||
"description": "",
|
||||
"kind": "vmware",
|
||||
"cloud": true,
|
||||
"credential_type_id": 7
|
||||
},
|
||||
"credentials":[
|
||||
{
|
||||
"id": 8,
|
||||
"name": "mock cred",
|
||||
"description": "",
|
||||
"kind": "vmware",
|
||||
"cloud": true,
|
||||
"credential_type_id": 7
|
||||
}
|
||||
]
|
||||
},
|
||||
"created":"2020-04-02T18:59:08.474167Z",
|
||||
"modified":"2020-04-02T19:52:23.924252Z",
|
||||
"name":"mock inv source",
|
||||
"description":"mock description",
|
||||
"source":"scm",
|
||||
"source_path": "foo",
|
||||
"source_script": "Mock Script",
|
||||
"source_vars":"---\nfoo: bar",
|
||||
"credential": 8,
|
||||
"source_regions": "us-east-1,us-east-2",
|
||||
"instance_filters": "filter1,filter2,filter3",
|
||||
"group_by": "group1,group2,group3",
|
||||
"overwrite":true,
|
||||
"overwrite_vars":true,
|
||||
"custom_virtualenv":"/venv/custom",
|
||||
"timeout":0,
|
||||
"verbosity":2,
|
||||
"last_job_run":null,
|
||||
"last_job_failed":false,
|
||||
"next_job_run":null,
|
||||
"status":"never updated",
|
||||
"inventory":1,
|
||||
"update_on_launch":true,
|
||||
"update_cache_timeout":2,
|
||||
"source_project":8,
|
||||
"update_on_project_update":true,
|
||||
"last_update_failed": true,
|
||||
"last_updated":null
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user