Merge pull request #6286 from jlmitch5/hostFacts

add facts views to host and inv host detail views

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-03-20 20:08:52 +00:00
committed by GitHub
14 changed files with 2730 additions and 12 deletions

View File

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

View File

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

View File

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

View File

@@ -116,7 +116,7 @@ function Host({ i18n, setBreadcrumb }) {
<Route path="/hosts/:id/edit" key="edit"> <Route path="/hosts/:id/edit" key="edit">
<HostEdit host={host} /> <HostEdit host={host} />
</Route>, </Route>,
<Route path="/hosts/:id/facts" key="facts"> <Route key="facts" path="/hosts/:id/facts">
<HostFacts host={host} /> <HostFacts host={host} />
</Route>, </Route>,
<Route path="/hosts/:id/groups" key="groups"> <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 { 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 { function HostFacts({ i18n, host }) {
render() { const { result: facts, isLoading, error, request: fetchFacts } = useRequest(
return <CardBody>Coming soon :)</CardBody>; 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 JobList from '@components/JobList';
import InventoryHostDetail from '../InventoryHostDetail'; import InventoryHostDetail from '../InventoryHostDetail';
import InventoryHostEdit from '../InventoryHostEdit'; import InventoryHostEdit from '../InventoryHostEdit';
import InventoryHostFacts from '../InventoryHostFacts';
function InventoryHost({ i18n, setBreadcrumb, inventory }) { function InventoryHost({ i18n, setBreadcrumb, inventory }) {
const location = useLocation(); const location = useLocation();
@@ -140,6 +141,12 @@ function InventoryHost({ i18n, setBreadcrumb, inventory }) {
> >
<InventoryHostEdit host={host} inventory={inventory} /> <InventoryHostEdit host={host} inventory={inventory} />
</Route> </Route>
<Route
key="facts"
path="/inventories/inventory/:id/hosts/:hostId/facts"
>
<InventoryHostFacts host={host} />
</Route>
<Route <Route
key="completed-jobs" key="completed-jobs"
path="/inventories/inventory/:id/hosts/:hostId/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 && ( {use_fact_cache && (
<TextListItem component={TextListItemVariants.li}> <TextListItem component={TextListItemVariants.li}>
{i18n._(t`Use Fact Cache`)} {i18n._(t`Use Fact Storage`)}
</TextListItem> </TextListItem>
)} )}
</TextList> </TextList>

View File

@@ -561,9 +561,10 @@ function JobTemplateForm({
<CheckboxField <CheckboxField
id="option-fact-cache" id="option-fact-cache"
name="use_fact_cache" name="use_fact_cache"
label={i18n._(t`Fact Cache`)} label={i18n._(t`Enable Fact Storage`)}
tooltip={i18n._(t`If enabled, use cached facts if available tooltip={i18n._(
and store discovered facts in the cache.`)} 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> </FormCheckboxLayout>
</FormGroup> </FormGroup>