mirror of
https://github.com/ansible/awx.git
synced 2026-02-25 23:16:01 -03:30
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:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
56
awx/ui_next/src/screens/Host/HostFacts/HostFacts.test.jsx
Normal file
56
awx/ui_next/src/screens/Host/HostFacts/HostFacts.test.jsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
1249
awx/ui_next/src/screens/Host/data.hostFacts.json
Normal file
1249
awx/ui_next/src/screens/Host/data.hostFacts.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './InventoryHostFacts';
|
||||||
1249
awx/ui_next/src/screens/Inventory/shared/data.hostFacts.json
Normal file
1249
awx/ui_next/src/screens/Inventory/shared/data.hostFacts.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user