add facts views to host and inv host detail views and update enable fact storage checkbox option and detail language

This commit is contained in:
John Mitchell 2020-03-19 13:50:30 -04:00
parent 8a917a5b70
commit 4bec46a910
14 changed files with 2730 additions and 12 deletions

View File

@ -5,6 +5,10 @@ class Hosts extends Base {
super(http);
this.baseUrl = '/api/v2/hosts/';
}
readFacts(id) {
return this.http.get(`${this.baseUrl}${id}/ansible_facts/`);
}
}
export default Hosts;

View File

@ -17,7 +17,8 @@ const CodeMirror = styled(ReactCodeMirror)`
}
& > .CodeMirror {
height: ${props => props.rows * LINE_HEIGHT + PADDING}px;
height: ${props =>
props.fullHeight ? 'auto' : `${props.rows * LINE_HEIGHT + PADDING}px`};
font-family: var(--pf-global--FontFamily--monospace);
}
@ -63,6 +64,7 @@ function CodeMirrorInput({
readOnly,
hasErrors,
rows,
fullHeight,
className,
}) {
return (
@ -75,8 +77,10 @@ function CodeMirrorInput({
options={{
smartIndent: false,
lineNumbers: true,
lineWrapping: true,
readOnly,
}}
fullHeight={fullHeight}
rows={rows}
/>
);
@ -87,12 +91,14 @@ CodeMirrorInput.propTypes = {
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
readOnly: bool,
hasErrors: bool,
fullHeight: bool,
rows: number,
};
CodeMirrorInput.defaultProps = {
readOnly: false,
onChange: () => {},
rows: 6,
fullHeight: false,
hasErrors: false,
};

View File

@ -21,7 +21,7 @@ function getValueAsMode(value, mode) {
return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value);
}
function VariablesDetail({ value, label, rows }) {
function VariablesDetail({ value, label, rows, fullHeight }) {
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
const [currentValue, setCurrentValue] = useState(value || '---');
const [error, setError] = useState(null);
@ -75,6 +75,7 @@ function VariablesDetail({ value, label, rows }) {
value={currentValue}
readOnly
rows={rows}
fullHeight={fullHeight}
css="margin-top: 10px"
/>
{error && (

View File

@ -116,7 +116,7 @@ function Host({ i18n, setBreadcrumb }) {
<Route path="/hosts/:id/edit" key="edit">
<HostEdit host={host} />
</Route>,
<Route path="/hosts/:id/facts" key="facts">
<Route key="facts" path="/hosts/:id/facts">
<HostFacts host={host} />
</Route>,
<Route path="/hosts/:id/groups" key="groups">

View File

@ -1,10 +1,49 @@
import React, { Component } from 'react';
import React, { useCallback, useEffect } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Host } from '@types';
import { CardBody } from '@components/Card';
import { DetailList } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import useRequest from '@util/useRequest';
import { HostsAPI } from '@api';
class HostFacts extends Component {
render() {
return <CardBody>Coming soon :)</CardBody>;
function HostFacts({ i18n, host }) {
const { result: facts, isLoading, error, request: fetchFacts } = useRequest(
useCallback(async () => {
const [{ data: factsObj }] = await Promise.all([
HostsAPI.readFacts(host.id),
]);
return JSON.stringify(factsObj, null, 4);
}, [host]),
'{}'
);
useEffect(() => {
fetchFacts();
}, [fetchFacts]);
if (isLoading) {
return <ContentLoading />;
}
if (error) {
return <ContentError error={error} />;
}
return (
<CardBody>
<DetailList gutter="sm">
<VariablesDetail label={i18n._(t`Facts`)} fullHeight value={facts} />
</DetailList>
</CardBody>
);
}
export default HostFacts;
HostFacts.propTypes = {
host: Host.isRequired,
};
export default withI18n()(HostFacts);

View File

@ -0,0 +1,56 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import HostFacts from './HostFacts';
import { HostsAPI } from '@api';
import mockHost from '../data.host.json';
import mockHostFacts from '../data.hostFacts.json';
jest.mock('@api/models/Hosts');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
id: 1,
hostId: 1,
}),
}));
describe('<HostFacts />', () => {
let wrapper;
beforeEach(async () => {
HostsAPI.readFacts.mockResolvedValue({ data: mockHostFacts });
await act(async () => {
wrapper = mountWithContexts(<HostFacts host={mockHost} />);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('initially renders successfully ', () => {
expect(wrapper.find('HostFacts').length).toBe(1);
});
test('renders ContentError when facts GET fails', async () => {
HostsAPI.readFacts.mockRejectedValueOnce(
new Error({
response: {
config: {
method: 'get',
url: '/api/v2/hosts/1/ansible_facts',
},
data: 'An error occurred',
status: 500,
},
})
);
await act(async () => {
wrapper = mountWithContexts(<HostFacts host={mockHost} />);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ import RoutedTabs from '@components/RoutedTabs';
import JobList from '@components/JobList';
import InventoryHostDetail from '../InventoryHostDetail';
import InventoryHostEdit from '../InventoryHostEdit';
import InventoryHostFacts from '../InventoryHostFacts';
function InventoryHost({ i18n, setBreadcrumb, inventory }) {
const location = useLocation();
@ -140,6 +141,12 @@ function InventoryHost({ i18n, setBreadcrumb, inventory }) {
>
<InventoryHostEdit host={host} inventory={inventory} />
</Route>
<Route
key="facts"
path="/inventories/inventory/:id/hosts/:hostId/facts"
>
<InventoryHostFacts host={host} />
</Route>
<Route
key="completed-jobs"
path="/inventories/inventory/:id/hosts/:hostId/completed_jobs"

View File

@ -0,0 +1,49 @@
import React, { useCallback, useEffect } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Host } from '@types';
import { CardBody } from '@components/Card';
import { DetailList } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput';
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import useRequest from '@util/useRequest';
import { HostsAPI } from '@api';
function InventoryHostFacts({ i18n, host }) {
const { result: facts, isLoading, error, request: fetchFacts } = useRequest(
useCallback(async () => {
const [{ data: factsObj }] = await Promise.all([
HostsAPI.readFacts(host.id),
]);
return JSON.stringify(factsObj, null, 4);
}, [host]),
'{}'
);
useEffect(() => {
fetchFacts();
}, [fetchFacts]);
if (isLoading) {
return <ContentLoading />;
}
if (error) {
return <ContentError error={error} />;
}
return (
<CardBody>
<DetailList gutter="sm">
<VariablesDetail label={i18n._(t`Facts`)} fullHeight value={facts} />
</DetailList>
</CardBody>
);
}
InventoryHostFacts.propTypes = {
host: Host.isRequired,
};
export default withI18n()(InventoryHostFacts);

View File

@ -0,0 +1,56 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import InventoryHostFacts from './InventoryHostFacts';
import { HostsAPI } from '@api';
import mockHost from '../shared/data.host.json';
import mockHostFacts from '../shared/data.hostFacts.json';
jest.mock('@api/models/Hosts');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
id: 1,
hostId: 1,
}),
}));
describe('<InventoryHostFacts />', () => {
let wrapper;
beforeEach(async () => {
HostsAPI.readFacts.mockResolvedValue({ data: mockHostFacts });
await act(async () => {
wrapper = mountWithContexts(<InventoryHostFacts host={mockHost} />);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('initially renders successfully ', () => {
expect(wrapper.find('InventoryHostFacts').length).toBe(1);
});
test('renders ContentError when facts GET fails', async () => {
HostsAPI.readFacts.mockRejectedValueOnce(
new Error({
response: {
config: {
method: 'get',
url: '/api/v2/hosts/1/ansible_facts',
},
data: 'An error occurred',
status: 500,
},
})
);
await act(async () => {
wrapper = mountWithContexts(<InventoryHostFacts host={mockHost} />);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
});

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -127,7 +127,7 @@ function JobTemplateDetail({ i18n, template }) {
)}
{use_fact_cache && (
<TextListItem component={TextListItemVariants.li}>
{i18n._(t`Use Fact Cache`)}
{i18n._(t`Use Fact Storage`)}
</TextListItem>
)}
</TextList>

View File

@ -561,9 +561,10 @@ function JobTemplateForm({
<CheckboxField
id="option-fact-cache"
name="use_fact_cache"
label={i18n._(t`Fact Cache`)}
tooltip={i18n._(t`If enabled, use cached facts if available
and store discovered facts in the cache.`)}
label={i18n._(t`Enable Fact Storage`)}
tooltip={i18n._(
t`If enabled, this will store gathered facts so they can be viewed at the host level. Facts are persisted and injected into the fact cache at runtime.`
)}
/>
</FormCheckboxLayout>
</FormGroup>