mirror of
https://github.com/ansible/awx.git
synced 2026-01-15 03:40:42 -03:30
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:
parent
8a917a5b70
commit
4bec46a910
@ -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;
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
|
||||
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 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"
|
||||
|
||||
@ -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 && (
|
||||
<TextListItem component={TextListItemVariants.li}>
|
||||
{i18n._(t`Use Fact Cache`)}
|
||||
{i18n._(t`Use Fact Storage`)}
|
||||
</TextListItem>
|
||||
)}
|
||||
</TextList>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user