mirror of
https://github.com/ansible/awx.git
synced 2026-04-14 14:39:26 -02:30
fixes bug with disappearing modal and arguments field validation
This commit is contained in:
@@ -12,7 +12,9 @@ class Groups extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
associateHost(id, hostId) {
|
associateHost(id, hostId) {
|
||||||
return this.http.post(`${this.baseUrl}${id}/hosts/`, { id: hostId });
|
return this.http.post(`${this.baseUrl}${id}/hosts/`, {
|
||||||
|
id: hostId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createHost(id, data) {
|
createHost(id, data) {
|
||||||
@@ -20,7 +22,9 @@ class Groups extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readAllHosts(id, params) {
|
readAllHosts(id, params) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/all_hosts/`, { params });
|
return this.http.get(`${this.baseUrl}${id}/all_hosts/`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disassociateHost(id, host) {
|
disassociateHost(id, host) {
|
||||||
@@ -29,6 +33,10 @@ class Groups extends Base {
|
|||||||
disassociate: true,
|
disassociate: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readChildren(id, params) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/children/`, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Groups;
|
export default Groups;
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import React, { useState, Fragment, useCallback, useEffect } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||||
|
import { InventoriesAPI } from '../../api';
|
||||||
|
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import { CredentialTypesAPI } from '../../api';
|
|
||||||
import ErrorDetail from '../ErrorDetail';
|
import ErrorDetail from '../ErrorDetail';
|
||||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||||
import ContentLoading from '../ContentLoading';
|
import ContentLoading from '../ContentLoading';
|
||||||
import ContentError from '../ContentError';
|
|
||||||
|
|
||||||
function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
function AdHocCommands({
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
onClose,
|
||||||
|
adHocItems,
|
||||||
|
itemId,
|
||||||
|
i18n,
|
||||||
|
moduleOptions,
|
||||||
|
credentialTypeId,
|
||||||
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const verbosityOptions = [
|
const verbosityOptions = [
|
||||||
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
||||||
@@ -22,59 +28,26 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
||||||
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
||||||
];
|
];
|
||||||
const {
|
|
||||||
error: fetchError,
|
|
||||||
request: fetchModuleOptions,
|
|
||||||
result: { moduleOptions, credentialTypeId, isDisabled },
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
|
||||||
const [choices, credId] = await Promise.all([
|
|
||||||
apiModule.readAdHocOptions(itemId),
|
|
||||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
|
||||||
]);
|
|
||||||
const itemObject = (item, index) => {
|
|
||||||
return {
|
|
||||||
key: index,
|
|
||||||
value: item,
|
|
||||||
label: `${item}`,
|
|
||||||
isDisabled: false,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = choices.data.actions.GET.module_name.choices.map(
|
|
||||||
(choice, index) => itemObject(choice[0], index)
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
moduleOptions: [itemObject('', -1), ...options],
|
|
||||||
credentialTypeId: credId.data.results[0].id,
|
|
||||||
isDisabled: !choices.data.actions.POST,
|
|
||||||
};
|
|
||||||
}, [itemId, apiModule]),
|
|
||||||
{ moduleOptions: [], isDisabled: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchModuleOptions();
|
|
||||||
}, [fetchModuleOptions]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isloading: isLaunchLoading,
|
isloading: isLaunchLoading,
|
||||||
error: launchError,
|
error,
|
||||||
request: launchAdHocCommands,
|
request: launchAdHocCommands,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(
|
useCallback(
|
||||||
async values => {
|
async values => {
|
||||||
const { data } = await apiModule.launchAdHocCommands(itemId, values);
|
const { data } = await InventoriesAPI.launchAdHocCommands(
|
||||||
|
itemId,
|
||||||
|
values
|
||||||
|
);
|
||||||
history.push(`/jobs/command/${data.id}/output`);
|
history.push(`/jobs/command/${data.id}/output`);
|
||||||
},
|
},
|
||||||
|
|
||||||
[apiModule, itemId, history]
|
[itemId, history]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(
|
const { dismissError } = useDismissableError(error);
|
||||||
launchError || fetchError
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
const { credential, ...remainingValues } = values;
|
const { credential, ...remainingValues } = values;
|
||||||
@@ -85,14 +58,13 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
...remainingValues,
|
...remainingValues,
|
||||||
};
|
};
|
||||||
await launchAdHocCommands(manipulatedValues);
|
await launchAdHocCommands(manipulatedValues);
|
||||||
setIsWizardOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLaunchLoading) {
|
if (isLaunchLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && isWizardOpen) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={error}
|
isOpen={error}
|
||||||
@@ -100,44 +72,29 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
dismissError();
|
dismissError();
|
||||||
setIsWizardOpen(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{launchError ? (
|
<>
|
||||||
<>
|
{i18n._(t`Failed to launch job.`)}
|
||||||
{i18n._(t`Failed to launch job.`)}
|
<ErrorDetail error={error} />
|
||||||
<ErrorDetail error={error} />
|
</>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ContentError error={error} />
|
|
||||||
)}
|
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<AdHocCommandsWizard
|
||||||
{children({
|
adHocItems={adHocItems}
|
||||||
openAdHocCommands: () => setIsWizardOpen(true),
|
moduleOptions={moduleOptions}
|
||||||
isDisabled,
|
verbosityOptions={verbosityOptions}
|
||||||
})}
|
credentialTypeId={credentialTypeId}
|
||||||
|
onCloseWizard={onClose}
|
||||||
{isWizardOpen && (
|
onLaunch={handleSubmit}
|
||||||
<AdHocCommandsWizard
|
onDismissError={() => dismissError()}
|
||||||
adHocItems={adHocItems}
|
/>
|
||||||
moduleOptions={moduleOptions}
|
|
||||||
verbosityOptions={verbosityOptions}
|
|
||||||
credentialTypeId={credentialTypeId}
|
|
||||||
onCloseWizard={() => setIsWizardOpen(false)}
|
|
||||||
onLaunch={handleSubmit}
|
|
||||||
onDismissError={() => dismissError()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdHocCommands.propTypes = {
|
AdHocCommands.propTypes = {
|
||||||
children: PropTypes.func.isRequired,
|
|
||||||
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
itemId: PropTypes.number.isRequired,
|
itemId: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const credentials = [
|
|||||||
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
||||||
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||||
];
|
];
|
||||||
|
const moduleOptions = [
|
||||||
|
['command', 'command'],
|
||||||
|
['shell', 'shell'],
|
||||||
|
];
|
||||||
const adHocItems = [
|
const adHocItems = [
|
||||||
{
|
{
|
||||||
name: 'Inventory 1 Org 0',
|
name: 'Inventory 1 Org 0',
|
||||||
@@ -25,14 +29,6 @@ const adHocItems = [
|
|||||||
{ name: 'Inventory 2 Org 0' },
|
{ name: 'Inventory 2 Org 0' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const children = ({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isDisabled}
|
|
||||||
onClick={() => openAdHocCommands()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('<AdHocCommands />', () => {
|
describe('<AdHocCommands />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -44,111 +40,38 @@ describe('<AdHocCommands />', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
apiModule={InventoriesAPI}
|
css="margin-right: 20px"
|
||||||
adHocItems={adHocItems}
|
onClose={() => {}}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
credentialTypeId={1}
|
credentialTypeId={1}
|
||||||
>
|
adHocItems={adHocItems}
|
||||||
{children}
|
moduleOptions={moduleOptions}
|
||||||
</AdHocCommands>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||||
});
|
});
|
||||||
test('calls api on Mount', async () => {
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<AdHocCommands
|
|
||||||
apiModule={InventoriesAPI}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AdHocCommands>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
|
||||||
expect(InventoriesAPI.readAdHocOptions).toBeCalledWith(1);
|
|
||||||
expect(CredentialTypesAPI.read).toBeCalledWith({ namespace: 'ssh' });
|
|
||||||
});
|
|
||||||
test('should open the wizard', async () => {
|
|
||||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: {
|
|
||||||
module_name: {
|
|
||||||
choices: [
|
|
||||||
['command', 'command'],
|
|
||||||
['foo', 'foo'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
verbosity: { choices: [[1], [2]] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
|
||||||
data: { results: [{ id: 1 }] },
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<AdHocCommands
|
|
||||||
apiModule={InventoriesAPI}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AdHocCommands>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await act(async () => wrapper.find('button').prop('onClick')());
|
|
||||||
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should submit properly', async () => {
|
test('should submit properly', async () => {
|
||||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: {
|
|
||||||
module_name: {
|
|
||||||
choices: [
|
|
||||||
['command', 'command'],
|
|
||||||
['foo', 'foo'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
verbosity: { choices: [[1], [2]] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
|
||||||
data: { results: [{ id: 1 }] },
|
|
||||||
});
|
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
results: credentials,
|
results: credentials,
|
||||||
count: 5,
|
count: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
apiModule={InventoriesAPI}
|
css="margin-right: 20px"
|
||||||
adHocItems={adHocItems}
|
onClose={() => {}}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
credentialTypeId={1}
|
credentialTypeId={1}
|
||||||
>
|
adHocItems={adHocItems}
|
||||||
{children}
|
moduleOptions={moduleOptions}
|
||||||
</AdHocCommands>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await act(async () => wrapper.find('button').prop('onClick')());
|
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
@@ -181,6 +104,7 @@ describe('<AdHocCommands />', () => {
|
|||||||
);
|
);
|
||||||
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
// second step of wizard
|
// second step of wizard
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('input[aria-labelledby="check-action-item-4"]')
|
.find('input[aria-labelledby="check-action-item-4"]')
|
||||||
@@ -209,10 +133,6 @@ describe('<AdHocCommands />', () => {
|
|||||||
module_name: 'command',
|
module_name: 'command',
|
||||||
verbosity: 1,
|
verbosity: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error on submission properly', async () => {
|
test('should throw error on submission properly', async () => {
|
||||||
@@ -255,16 +175,15 @@ describe('<AdHocCommands />', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
apiModule={InventoriesAPI}
|
css="margin-right: 20px"
|
||||||
adHocItems={adHocItems}
|
onClose={() => {}}
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
credentialTypeId={1}
|
||||||
>
|
itemId={1}
|
||||||
{children}
|
adHocItems={adHocItems}
|
||||||
</AdHocCommands>
|
moduleOptions={moduleOptions}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await act(async () => wrapper.find('button').prop('onClick')());
|
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
@@ -316,58 +235,6 @@ describe('<AdHocCommands />', () => {
|
|||||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
);
|
);
|
||||||
|
|
||||||
waitForElement(wrapper, 'ErrorDetail', el => el.length > 0);
|
await waitForElement(wrapper, 'ErrorDetail', el => el.length > 0);
|
||||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(0);
|
|
||||||
});
|
|
||||||
test('should open alert modal when error on fetching data', async () => {
|
|
||||||
InventoriesAPI.readAdHocOptions.mockRejectedValue(
|
|
||||||
new Error({
|
|
||||||
response: {
|
|
||||||
config: {
|
|
||||||
method: 'options',
|
|
||||||
url: '/api/v2/inventories/1/',
|
|
||||||
},
|
|
||||||
data: 'An error occurred',
|
|
||||||
status: 403,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<AdHocCommands
|
|
||||||
apiModule={InventoriesAPI}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AdHocCommands>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await act(async () => wrapper.find('button').prop('onClick')());
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
|
||||||
});
|
|
||||||
test('should disable button', async () => {
|
|
||||||
const isDisabled = true;
|
|
||||||
const newChild = ({ openAdHocCommands }) => (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isDisabled}
|
|
||||||
onClick={() => openAdHocCommands()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<AdHocCommands
|
|
||||||
apiModule={InventoriesAPI}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
|
||||||
>
|
|
||||||
{newChild}
|
|
||||||
</AdHocCommands>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,21 +27,31 @@ const TooltipWrapper = styled.div`
|
|||||||
// in failing tests.
|
// in failing tests.
|
||||||
const brandName = BrandName;
|
const brandName = BrandName;
|
||||||
|
|
||||||
function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
||||||
const [module_nameField, module_nameMeta, module_nameHelpers] = useField({
|
const [moduleNameField, moduleNameMeta, moduleNameHelpers] = useField({
|
||||||
name: 'module_name',
|
name: 'module_name',
|
||||||
validate: required(null, i18n),
|
validate: required(null, i18n),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [variablesField] = useField('extra_vars');
|
const [variablesField] = useField('extra_vars');
|
||||||
const [diff_modeField, , diff_modeHelpers] = useField('diff_mode');
|
const [diffModeField, , diffModeHelpers] = useField('diff_mode');
|
||||||
const [become_enabledField, , become_enabledHelpers] = useField(
|
const [becomeEnabledField, , becomeEnabledHelpers] = useField(
|
||||||
'become_enabled'
|
'become_enabled'
|
||||||
);
|
);
|
||||||
const [verbosityField, verbosityMeta, verbosityHelpers] = useField({
|
const [verbosityField, verbosityMeta, verbosityHelpers] = useField({
|
||||||
name: 'verbosity',
|
name: 'verbosity',
|
||||||
validate: required(null, i18n),
|
validate: required(null, i18n),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const argumentsRequired =
|
||||||
|
moduleNameField.value === 'command' || moduleNameField.value === 'shell';
|
||||||
|
const [, argumentsMeta, argumentsHelpers] = useField({
|
||||||
|
name: 'module_args',
|
||||||
|
validate: argumentsRequired ? required(null, i18n) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValid = !argumentsMeta.error || !argumentsMeta.touched;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<FormColumnLayout>
|
<FormColumnLayout>
|
||||||
@@ -50,9 +60,9 @@ function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
fieldId="module_name"
|
fieldId="module_name"
|
||||||
label={i18n._(t`Module`)}
|
label={i18n._(t`Module`)}
|
||||||
isRequired
|
isRequired
|
||||||
helperTextInvalid={module_nameMeta.error}
|
helperTextInvalid={moduleNameMeta.error}
|
||||||
validated={
|
validated={
|
||||||
!module_nameMeta.touched || !module_nameMeta.error
|
!moduleNameMeta.touched || !moduleNameMeta.error
|
||||||
? 'default'
|
? 'default'
|
||||||
: 'error'
|
: 'error'
|
||||||
}
|
}
|
||||||
@@ -65,12 +75,28 @@ function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
{...module_nameField}
|
{...moduleNameField}
|
||||||
isValid={!module_nameMeta.touched || !module_nameMeta.error}
|
placeHolder={i18n._(t`Select a module`)}
|
||||||
|
isValid={!moduleNameMeta.touched || !moduleNameMeta.error}
|
||||||
id="module_name"
|
id="module_name"
|
||||||
data={moduleOptions || []}
|
data={[
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
key: '',
|
||||||
|
label: i18n._(t`Choose a module`),
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
...moduleOptions.map(value => ({
|
||||||
|
value: value[0],
|
||||||
|
label: value[0],
|
||||||
|
key: value[0],
|
||||||
|
})),
|
||||||
|
]}
|
||||||
onChange={(event, value) => {
|
onChange={(event, value) => {
|
||||||
module_nameHelpers.setValue(value);
|
if (value !== 'command' && value !== 'shell') {
|
||||||
|
argumentsHelpers.setTouched(false);
|
||||||
|
}
|
||||||
|
moduleNameHelpers.setValue(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -79,19 +105,20 @@ function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
name="module_args"
|
name="module_args"
|
||||||
type="text"
|
type="text"
|
||||||
label={i18n._(t`Arguments`)}
|
label={i18n._(t`Arguments`)}
|
||||||
validate={required(null, i18n)}
|
validated={isValid ? 'default' : 'error'}
|
||||||
|
placeholder={i18n._(t`Enter arguments`)}
|
||||||
isRequired={
|
isRequired={
|
||||||
module_nameField.value === 'command' ||
|
moduleNameField.value === 'command' ||
|
||||||
module_nameField.value === 'shell'
|
moduleNameField.value === 'shell'
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
module_nameField.value ? (
|
moduleNameField.value ? (
|
||||||
<>
|
<>
|
||||||
{i18n._(
|
{i18n._(
|
||||||
t`These arguments are used with the specified module. You can find information about ${module_nameField.value} by clicking `
|
t`These arguments are used with the specified module. You can find information about ${moduleNameField.value} by clicking `
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
href={`https://docs.ansible.com/ansible/latest/modules/${module_nameField.value}_module.html`}
|
href={`https://docs.ansible.com/ansible/latest/modules/${moduleNameField.value}_module.html`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@@ -189,9 +216,9 @@ function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
id="diff_mode"
|
id="diff_mode"
|
||||||
label={i18n._(t`On`)}
|
label={i18n._(t`On`)}
|
||||||
labelOff={i18n._(t`Off`)}
|
labelOff={i18n._(t`Off`)}
|
||||||
isChecked={diff_modeField.value}
|
isChecked={diffModeField.value}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
diff_modeHelpers.setValue(!diff_modeField.value);
|
diffModeHelpers.setValue(!diffModeField.value);
|
||||||
}}
|
}}
|
||||||
aria-label={i18n._(t`toggle changes`)}
|
aria-label={i18n._(t`toggle changes`)}
|
||||||
/>
|
/>
|
||||||
@@ -222,9 +249,9 @@ function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
id="become_enabled"
|
id="become_enabled"
|
||||||
isChecked={become_enabledField.value}
|
isChecked={becomeEnabledField.value}
|
||||||
onChange={checked => {
|
onChange={checked => {
|
||||||
become_enabledHelpers.setValue(checked);
|
becomeEnabledHelpers.setValue(checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormCheckboxLayout>
|
</FormCheckboxLayout>
|
||||||
@@ -280,9 +307,9 @@ function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CredentialStep.propTypes = {
|
AdHocDetailsStep.propTypes = {
|
||||||
moduleOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
moduleOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(CredentialStep);
|
export default withI18n()(AdHocDetailsStep);
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ const verbosityOptions = [
|
|||||||
{ key: 1, value: 1, label: '1', isDisabled: false },
|
{ key: 1, value: 1, label: '1', isDisabled: false },
|
||||||
];
|
];
|
||||||
const moduleOptions = [
|
const moduleOptions = [
|
||||||
{ key: -1, value: '', label: '', isDisabled: false },
|
['command', 'command'],
|
||||||
{ key: 0, value: 'command', label: 'command', isDisabled: false },
|
['shell', 'shell'],
|
||||||
{ key: 1, value: 'shell', label: 'shell', isDisabled: false },
|
|
||||||
];
|
];
|
||||||
const onLimitChange = jest.fn();
|
const onLimitChange = jest.fn();
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { getQSConfig, mergeParams, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, mergeParams, parseQueryString } from '../../../util/qs';
|
||||||
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
import { GroupsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||||
|
|
||||||
import useRequest, {
|
import useRequest, {
|
||||||
useDeleteItems,
|
useDeleteItems,
|
||||||
@@ -23,7 +23,7 @@ import PaginatedDataList from '../../../components/PaginatedDataList';
|
|||||||
import AssociateModal from '../../../components/AssociateModal';
|
import AssociateModal from '../../../components/AssociateModal';
|
||||||
import DisassociateButton from '../../../components/DisassociateButton';
|
import DisassociateButton from '../../../components/DisassociateButton';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
import { Kebabified } from '../../../contexts/Kebabified';
|
||||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import InventoryGroupHostListItem from './InventoryGroupHostListItem';
|
import InventoryGroupHostListItem from './InventoryGroupHostListItem';
|
||||||
import AddHostDropdown from './AddHostDropdown';
|
import AddHostDropdown from './AddHostDropdown';
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
|
|
||||||
function InventoryGroupHostList({ i18n }) {
|
function InventoryGroupHostList({ i18n }) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||||
const { id: inventoryId, groupId } = useParams();
|
const { id: inventoryId, groupId } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -46,6 +47,9 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
|
moduleOptions,
|
||||||
|
credentialTypeId,
|
||||||
|
isAdHocDisabled,
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -53,9 +57,16 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const [response, actionsResponse] = await Promise.all([
|
const [
|
||||||
|
response,
|
||||||
|
actionsResponse,
|
||||||
|
adHocOptions,
|
||||||
|
cred,
|
||||||
|
] = await Promise.all([
|
||||||
GroupsAPI.readAllHosts(groupId, params),
|
GroupsAPI.readAllHosts(groupId, params),
|
||||||
InventoriesAPI.readHostsOptions(inventoryId),
|
InventoriesAPI.readHostsOptions(inventoryId),
|
||||||
|
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||||
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -68,6 +79,9 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
|
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||||
|
credentialTypeId: cred.data.results[0].id,
|
||||||
|
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||||
};
|
};
|
||||||
}, [groupId, inventoryId, location.search]),
|
}, [groupId, inventoryId, location.search]),
|
||||||
{
|
{
|
||||||
@@ -76,6 +90,8 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
actions: {},
|
actions: {},
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
searchableKeys: [],
|
searchableKeys: [],
|
||||||
|
moduleOptions: [],
|
||||||
|
isAdHocDisabled: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -206,47 +222,32 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
<Kebabified>
|
<Kebabified>
|
||||||
{({ isKebabified }) =>
|
{({ isKebabified }) =>
|
||||||
isKebabified ? (
|
isKebabified ? (
|
||||||
<AdHocCommandsButton
|
<DropdownItem
|
||||||
adHocItems={selected}
|
variant="secondary"
|
||||||
apiModule={InventoriesAPI}
|
aria-label={i18n._(t`Run command`)}
|
||||||
itemId={parseInt(inventoryId, 10)}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
|
isDisabled={hostCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<DropdownItem
|
</DropdownItem>
|
||||||
key="run command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={hostCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
) : (
|
) : (
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={i18n._(
|
content={i18n._(
|
||||||
t`Select an inventory source by clicking the check box beside it. The inventory source can be a single host or a selection of multiple hosts.`
|
t`Select an inventory source by clicking the check box beside it.
|
||||||
|
The inventory source can be a single host or a selection of multiple hosts.`
|
||||||
)}
|
)}
|
||||||
position="top"
|
position="top"
|
||||||
key="adhoc"
|
key="adhoc"
|
||||||
>
|
>
|
||||||
<AdHocCommandsButton
|
<Button
|
||||||
css="margin-right: 20px"
|
variant="secondary"
|
||||||
adHocItems={selected}
|
aria-label={i18n._(t`Run command`)}
|
||||||
apiModule={InventoriesAPI}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
itemId={parseInt(inventoryId, 10)}
|
isDisabled={hostCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<Button
|
</Button>
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={hostCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
)
|
)
|
||||||
@@ -279,6 +280,7 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
emptyStateControls={
|
emptyStateControls={
|
||||||
canAdd && (
|
canAdd && (
|
||||||
<AddHostDropdown
|
<AddHostDropdown
|
||||||
|
key="associate"
|
||||||
onAddExisting={() => setIsModalOpen(true)}
|
onAddExisting={() => setIsModalOpen(true)}
|
||||||
onAddNew={() => history.push(addFormUrl)}
|
onAddNew={() => history.push(addFormUrl)}
|
||||||
/>
|
/>
|
||||||
@@ -296,6 +298,16 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
title={i18n._(t`Select Hosts`)}
|
title={i18n._(t`Select Hosts`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isAdHocCommandsOpen && (
|
||||||
|
<AdHocCommands
|
||||||
|
css="margin-right: 20px"
|
||||||
|
adHocItems={selected}
|
||||||
|
itemId={parseInt(inventoryId, 10)}
|
||||||
|
onClose={() => setIsAdHocCommandsOpen(false)}
|
||||||
|
credentialTypeId={credentialTypeId}
|
||||||
|
moduleOptions={moduleOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{associateError && (
|
{associateError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={associateError}
|
isOpen={associateError}
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ describe('<InventoryGroupHostList />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: { module_name: { choices: [['module']] } },
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
|
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
||||||
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||||
});
|
});
|
||||||
@@ -108,31 +119,8 @@ describe('<InventoryGroupHostList />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: { module_name: { choices: [['module']] } },
|
|
||||||
POST: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
|
||||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
|
||||||
});
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||||
<InventoryGroupHostList>
|
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="run-command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InventoryGroupHostList>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitForElement(
|
await waitForElement(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import { InventoriesAPI, GroupsAPI } from '../../../api';
|
import { InventoriesAPI, GroupsAPI, CredentialTypesAPI } from '../../../api';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
@@ -22,7 +22,8 @@ import PaginatedDataList, {
|
|||||||
|
|
||||||
import InventoryGroupItem from './InventoryGroupItem';
|
import InventoryGroupItem from './InventoryGroupItem';
|
||||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
|
||||||
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
import { Kebabified } from '../../../contexts/Kebabified';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('group', {
|
const QS_CONFIG = getQSConfig('group', {
|
||||||
@@ -51,6 +52,7 @@ const useModal = () => {
|
|||||||
function InventoryGroupsList({ i18n }) {
|
function InventoryGroupsList({ i18n }) {
|
||||||
const [deletionError, setDeletionError] = useState(null);
|
const [deletionError, setDeletionError] = useState(null);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isModalOpen, toggleModal } = useModal();
|
const { isModalOpen, toggleModal } = useModal();
|
||||||
const { id: inventoryId } = useParams();
|
const { id: inventoryId } = useParams();
|
||||||
@@ -62,27 +64,36 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
|
moduleOptions,
|
||||||
|
credentialTypeId,
|
||||||
|
isAdHocDisabled,
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchGroups,
|
request: fetchData,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const [response, actionsResponse] = await Promise.all([
|
const [response, groupOptions, adHocOptions, cred] = await Promise.all([
|
||||||
InventoriesAPI.readGroups(inventoryId, params),
|
InventoriesAPI.readGroups(inventoryId, params),
|
||||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||||
|
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||||
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups: response.data.results,
|
groups: response.data.results,
|
||||||
groupCount: response.data.count,
|
groupCount: response.data.count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: groupOptions.data.actions,
|
||||||
relatedSearchableKeys: (
|
relatedSearchableKeys: (
|
||||||
actionsResponse?.data?.related_search_fields || []
|
groupOptions?.data?.related_search_fields || []
|
||||||
).map(val => val.slice(0, -8)),
|
).map(val => val.slice(0, -8)),
|
||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
groupOptions.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => groupOptions.data.actions?.GET[key].filterable),
|
||||||
|
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||||
|
credentialTypeId: cred.data.results[0].id,
|
||||||
|
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||||
};
|
};
|
||||||
}, [inventoryId, location]),
|
}, [inventoryId, location]),
|
||||||
{
|
{
|
||||||
@@ -95,8 +106,8 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchGroups();
|
fetchData();
|
||||||
}, [fetchGroups]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
||||||
groups
|
groups
|
||||||
@@ -144,7 +155,7 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleModal();
|
toggleModal();
|
||||||
fetchGroups();
|
fetchData();
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
};
|
};
|
||||||
@@ -153,21 +164,14 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
const kebabedAdditionalControls = () => {
|
const kebabedAdditionalControls = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdHocCommandsButton
|
<DropdownItem
|
||||||
adHocItems={selected}
|
key="run command"
|
||||||
apiModule={InventoriesAPI}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
itemId={parseInt(inventoryId, 10)}
|
isDisabled={groupCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<DropdownItem
|
</DropdownItem>
|
||||||
key="run command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={groupCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
aria-label={i18n._(t`Delete`)}
|
aria-label={i18n._(t`Delete`)}
|
||||||
@@ -264,23 +268,14 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
position="top"
|
position="top"
|
||||||
key="adhoc"
|
key="adhoc"
|
||||||
>
|
>
|
||||||
<AdHocCommandsButton
|
<Button
|
||||||
css="margin-right: 20px"
|
variant="secondary"
|
||||||
adHocItems={selected}
|
aria-label={i18n._(t`Run command`)}
|
||||||
apiModule={InventoriesAPI}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
itemId={parseInt(inventoryId, 10)}
|
isDisabled={groupCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<Button
|
</Button>
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={groupCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
@@ -321,6 +316,16 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{isAdHocCommandsOpen && (
|
||||||
|
<AdHocCommands
|
||||||
|
css="margin-right: 20px"
|
||||||
|
adHocItems={selected}
|
||||||
|
itemId={parseInt(inventoryId, 10)}
|
||||||
|
onClose={() => setIsAdHocCommandsOpen(false)}
|
||||||
|
credentialTypeId={credentialTypeId}
|
||||||
|
moduleOptions={moduleOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{deletionError && (
|
{deletionError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
|
|||||||
@@ -88,17 +88,7 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Route path="/inventories/inventory/:id/groups">
|
<Route path="/inventories/inventory/:id/groups">
|
||||||
<InventoryGroupsList>
|
<InventoryGroupsList />
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="run-command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InventoryGroupsList>
|
|
||||||
</Route>,
|
</Route>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
@@ -299,17 +289,7 @@ describe('<InventoryGroupsList/> error handling', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<Route path="/inventories/inventory/:id/groups">
|
<Route path="/inventories/inventory/:id/groups">
|
||||||
<InventoryGroupsList>
|
<InventoryGroupsList />
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="run-command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InventoryGroupsList>
|
|
||||||
</Route>,
|
</Route>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import useRequest, {
|
|||||||
useDeleteItems,
|
useDeleteItems,
|
||||||
} from '../../../util/useRequest';
|
} from '../../../util/useRequest';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import { HostsAPI, InventoriesAPI } from '../../../api';
|
import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
@@ -24,7 +24,7 @@ import PaginatedDataList, {
|
|||||||
import AssociateModal from '../../../components/AssociateModal';
|
import AssociateModal from '../../../components/AssociateModal';
|
||||||
import DisassociateButton from '../../../components/DisassociateButton';
|
import DisassociateButton from '../../../components/DisassociateButton';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
import { Kebabified } from '../../../contexts/Kebabified';
|
||||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('group', {
|
const QS_CONFIG = getQSConfig('group', {
|
||||||
@@ -35,6 +35,7 @@ const QS_CONFIG = getQSConfig('group', {
|
|||||||
|
|
||||||
function InventoryHostGroupsList({ i18n }) {
|
function InventoryHostGroupsList({ i18n }) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||||
const { hostId, id: invId } = useParams();
|
const { hostId, id: invId } = useParams();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
@@ -45,6 +46,9 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
|
moduleOptions,
|
||||||
|
isAdHocDisabled,
|
||||||
|
credentialTypeId,
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -57,22 +61,29 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
{
|
{
|
||||||
data: { count, results },
|
data: { count, results },
|
||||||
},
|
},
|
||||||
actionsResponse,
|
hostGroupOptions,
|
||||||
|
adHocOptions,
|
||||||
|
cred,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
HostsAPI.readAllGroups(hostId, params),
|
HostsAPI.readAllGroups(hostId, params),
|
||||||
HostsAPI.readGroupsOptions(hostId),
|
HostsAPI.readGroupsOptions(hostId),
|
||||||
|
InventoriesAPI.readAdHocOptions(invId),
|
||||||
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups: results,
|
groups: results,
|
||||||
itemCount: count,
|
itemCount: count,
|
||||||
actions: actionsResponse.data.actions,
|
actions: hostGroupOptions.data.actions,
|
||||||
relatedSearchableKeys: (
|
relatedSearchableKeys: (
|
||||||
actionsResponse?.data?.related_search_fields || []
|
hostGroupOptions?.data?.related_search_fields || []
|
||||||
).map(val => val.slice(0, -8)),
|
).map(val => val.slice(0, -8)),
|
||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
hostGroupOptions.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => hostGroupOptions.data.actions?.GET[key].filterable),
|
||||||
|
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||||
|
credentialTypeId: cred.data.results[0].id,
|
||||||
|
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||||
};
|
};
|
||||||
}, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps
|
}, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
{
|
{
|
||||||
@@ -81,6 +92,8 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
actions: {},
|
actions: {},
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
searchableKeys: [],
|
searchableKeys: [],
|
||||||
|
moduleOptions: [],
|
||||||
|
isAdHocDisabled: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -212,21 +225,14 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
<Kebabified>
|
<Kebabified>
|
||||||
{({ isKebabified }) =>
|
{({ isKebabified }) =>
|
||||||
isKebabified ? (
|
isKebabified ? (
|
||||||
<AdHocCommandsButton
|
<DropdownItem
|
||||||
adHocItems={selected}
|
key="run command"
|
||||||
apiModule={InventoriesAPI}
|
aria-label={i18n._(t`Run command`)}
|
||||||
itemId={parseInt(invId, 10)}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
|
isDisabled={itemCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<DropdownItem
|
</DropdownItem>
|
||||||
key="run command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={itemCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
) : (
|
) : (
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -236,23 +242,15 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
position="top"
|
position="top"
|
||||||
key="adhoc"
|
key="adhoc"
|
||||||
>
|
>
|
||||||
<AdHocCommandsButton
|
<Button
|
||||||
css="margin-right: 20px"
|
key="run command"
|
||||||
adHocItems={selected}
|
variant="secondary"
|
||||||
apiModule={InventoriesAPI}
|
aria-label={i18n._(t`Run command`)}
|
||||||
itemId={parseInt(invId, 10)}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
|
isDisabled={itemCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<Button
|
</Button>
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={itemCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
)
|
)
|
||||||
@@ -290,6 +288,16 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
title={i18n._(t`Select Groups`)}
|
title={i18n._(t`Select Groups`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isAdHocCommandsOpen && (
|
||||||
|
<AdHocCommands
|
||||||
|
css="margin-right: 20px"
|
||||||
|
adHocItems={selected}
|
||||||
|
itemId={parseInt(invId, 10)}
|
||||||
|
onClose={() => setIsAdHocCommandsOpen(false)}
|
||||||
|
credentialTypeId={credentialTypeId}
|
||||||
|
moduleOptions={moduleOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={error}
|
isOpen={error}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { useParams, useLocation } from 'react-router-dom';
|
import { useParams, useLocation } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import { InventoriesAPI, HostsAPI } from '../../../api';
|
import { InventoriesAPI, HostsAPI, CredentialTypesAPI } from '../../../api';
|
||||||
|
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
@@ -19,7 +19,7 @@ import PaginatedDataList, {
|
|||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
} from '../../../components/PaginatedDataList';
|
} from '../../../components/PaginatedDataList';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
import { Kebabified } from '../../../contexts/Kebabified';
|
||||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import InventoryHostItem from './InventoryHostItem';
|
import InventoryHostItem from './InventoryHostItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('host', {
|
const QS_CONFIG = getQSConfig('host', {
|
||||||
@@ -29,48 +29,64 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryHostList({ i18n }) {
|
function InventoryHostList({ i18n }) {
|
||||||
const [actions, setActions] = useState(null);
|
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||||
const [contentError, setContentError] = useState(null);
|
|
||||||
const [deletionError, setDeletionError] = useState(null);
|
|
||||||
const [hostCount, setHostCount] = useState(0);
|
|
||||||
const [hosts, setHosts] = useState([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
const fetchHosts = (hostId, queryString) => {
|
const {
|
||||||
const params = parseQueryString(QS_CONFIG, queryString);
|
result: {
|
||||||
return InventoriesAPI.readHosts(hostId, params);
|
hosts,
|
||||||
};
|
hostCount,
|
||||||
|
actions,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
moduleOptions,
|
||||||
|
credentialTypeId,
|
||||||
|
isAdHocDisabled,
|
||||||
|
},
|
||||||
|
error: contentError,
|
||||||
|
isLoading,
|
||||||
|
request: fetchData,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, search);
|
||||||
|
const [response, hostOptions, adHocOptions, cred] = await Promise.all([
|
||||||
|
InventoriesAPI.readHosts(id, params),
|
||||||
|
InventoriesAPI.readHostsOptions(id),
|
||||||
|
InventoriesAPI.readAdHocOptions(id),
|
||||||
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hosts: response.data.results,
|
||||||
|
hostCount: response.data.count,
|
||||||
|
actions: hostOptions.data.actions,
|
||||||
|
relatedSearchableKeys: (
|
||||||
|
hostOptions?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
searchableKeys: Object.keys(hostOptions.data.actions?.GET || {}).filter(
|
||||||
|
key => hostOptions.data.actions?.GET[key].filterable
|
||||||
|
),
|
||||||
|
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||||
|
credentialTypeId: cred.data.results[0].id,
|
||||||
|
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||||
|
};
|
||||||
|
}, [id, search]),
|
||||||
|
{
|
||||||
|
hosts: [],
|
||||||
|
hostCount: 0,
|
||||||
|
actions: {},
|
||||||
|
relatedSearchableKeys: [],
|
||||||
|
searchableKeys: [],
|
||||||
|
moduleOptions: [],
|
||||||
|
isAdHocDisabled: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
{
|
|
||||||
data: { count, results },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: { actions: optionActions },
|
|
||||||
},
|
|
||||||
] = await Promise.all([
|
|
||||||
fetchHosts(id, search),
|
|
||||||
InventoriesAPI.readOptions(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setHosts(results);
|
|
||||||
setHostCount(count);
|
|
||||||
setActions(optionActions);
|
|
||||||
} catch (error) {
|
|
||||||
setContentError(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id, search]);
|
}, [fetchData]);
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
setSelected(isSelected ? [...hosts] : []);
|
setSelected(isSelected ? [...hosts] : []);
|
||||||
@@ -83,30 +99,17 @@ function InventoryHostList({ i18n }) {
|
|||||||
setSelected(selected.concat(row));
|
setSelected(selected.concat(row));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const {
|
||||||
const handleDelete = async () => {
|
isLoading: isDeleteLoading,
|
||||||
setIsLoading(true);
|
deleteItems: deleteHosts,
|
||||||
|
deletionError,
|
||||||
try {
|
clearDeletionError,
|
||||||
|
} = useDeleteItems(
|
||||||
|
useCallback(async () => {
|
||||||
await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
|
await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
|
||||||
} catch (error) {
|
}, [selected]),
|
||||||
setDeletionError(error);
|
{ qsConfig: QS_CONFIG, fetchItems: fetchData }
|
||||||
} finally {
|
);
|
||||||
setSelected([]);
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: { count, results },
|
|
||||||
} = await fetchHosts(id, search);
|
|
||||||
|
|
||||||
setHosts(results);
|
|
||||||
setHostCount(count);
|
|
||||||
} catch (error) {
|
|
||||||
setContentError(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
@@ -116,7 +119,7 @@ function InventoryHostList({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading || isDeleteLoading}
|
||||||
items={hosts}
|
items={hosts}
|
||||||
itemCount={hostCount}
|
itemCount={hostCount}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
@@ -141,6 +144,8 @@ function InventoryHostList({ i18n }) {
|
|||||||
isNumeric: true,
|
isNumeric: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
renderToolbar={props => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
{...props}
|
{...props}
|
||||||
@@ -160,21 +165,14 @@ function InventoryHostList({ i18n }) {
|
|||||||
<Kebabified>
|
<Kebabified>
|
||||||
{({ isKebabified }) =>
|
{({ isKebabified }) =>
|
||||||
isKebabified ? (
|
isKebabified ? (
|
||||||
<AdHocCommandsButton
|
<DropdownItem
|
||||||
adHocItems={selected}
|
key="run command"
|
||||||
apiModule={InventoriesAPI}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
itemId={parseInt(id, 10)}
|
isDisabled={hostCount === 0 || isAdHocDisabled}
|
||||||
|
aria-label={i18n._(t`Run command`)}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<DropdownItem
|
</DropdownItem>
|
||||||
key="run command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={hostCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
) : (
|
) : (
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -184,23 +182,15 @@ function InventoryHostList({ i18n }) {
|
|||||||
position="top"
|
position="top"
|
||||||
key="adhoc"
|
key="adhoc"
|
||||||
>
|
>
|
||||||
<AdHocCommandsButton
|
<Button
|
||||||
css="margin-right: 20px"
|
variant="secondary"
|
||||||
adHocItems={selected}
|
key="run command"
|
||||||
apiModule={InventoriesAPI}
|
aria-label={i18n._(t`Run command`)}
|
||||||
itemId={parseInt(id, 10)}
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
|
isDisabled={hostCount === 0 || isAdHocDisabled}
|
||||||
>
|
>
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
{i18n._(t`Run command`)}
|
||||||
<Button
|
</Button>
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={hostCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
)
|
)
|
||||||
@@ -208,7 +198,7 @@ function InventoryHostList({ i18n }) {
|
|||||||
</Kebabified>,
|
</Kebabified>,
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
onDelete={handleDelete}
|
onDelete={deleteHosts}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
/>,
|
/>,
|
||||||
@@ -234,12 +224,22 @@ function InventoryHostList({ i18n }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{isAdHocCommandsOpen && (
|
||||||
|
<AdHocCommands
|
||||||
|
css="margin-right: 20px"
|
||||||
|
adHocItems={selected}
|
||||||
|
onClose={() => setIsAdHocCommandsOpen(false)}
|
||||||
|
credentialTypeId={credentialTypeId}
|
||||||
|
moduleOptions={moduleOptions}
|
||||||
|
itemId={id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{deletionError && (
|
{deletionError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
variant="error"
|
variant="error"
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => setDeletionError(null)}
|
onClose={clearDeletionError}
|
||||||
>
|
>
|
||||||
{i18n._(t`Failed to delete one or more hosts.`)}
|
{i18n._(t`Failed to delete one or more hosts.`)}
|
||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ describe('<InventoryHostList />', () => {
|
|||||||
results: mockHosts,
|
results: mockHosts,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InventoriesAPI.readOptions.mockResolvedValue({
|
InventoriesAPI.readHostsOptions.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
actions: {
|
actions: {
|
||||||
GET: {},
|
GET: {},
|
||||||
@@ -276,8 +276,15 @@ describe('<InventoryHostList />', () => {
|
|||||||
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should render enabled ad hoc commands button', async () => {
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'button[aria-label="Run command"]',
|
||||||
|
el => el.prop('disabled') === false
|
||||||
|
);
|
||||||
|
});
|
||||||
test('should hide Add button for users without ability to POST', async () => {
|
test('should hide Add button for users without ability to POST', async () => {
|
||||||
InventoriesAPI.readOptions.mockResolvedValueOnce({
|
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
actions: {
|
actions: {
|
||||||
GET: {},
|
GET: {},
|
||||||
@@ -294,7 +301,7 @@ describe('<InventoryHostList />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error when api throws error on initial render', async () => {
|
test('should show content error when api throws error on initial render', async () => {
|
||||||
InventoriesAPI.readOptions.mockImplementation(() =>
|
InventoriesAPI.readHostsOptions.mockImplementation(() =>
|
||||||
Promise.reject(new Error())
|
Promise.reject(new Error())
|
||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -304,11 +311,4 @@ describe('<InventoryHostList />', () => {
|
|||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
test('should render enabled ad hoc commands button', async () => {
|
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'button[aria-label="Run command"]',
|
|
||||||
el => el.prop('disabled') === false
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -14,10 +14,10 @@ import SmartInventoryHostListItem from './SmartInventoryHostListItem';
|
|||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import { InventoriesAPI } from '../../../api';
|
import { InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||||
import { Inventory } from '../../../types';
|
import { Inventory } from '../../../types';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
import { Kebabified } from '../../../contexts/Kebabified';
|
||||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('host', {
|
const QS_CONFIG = getQSConfig('host', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -27,24 +27,35 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
|
|
||||||
function SmartInventoryHostList({ i18n, inventory }) {
|
function SmartInventoryHostList({ i18n, inventory }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { hosts, count },
|
result: { hosts, count, moduleOptions, credentialTypeId, isAdHocDisabled },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchHosts,
|
request: fetchHosts,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const { data } = await InventoriesAPI.readHosts(inventory.id, params);
|
const [hostResponse, adHocOptions, cred] = await Promise.all([
|
||||||
|
InventoriesAPI.readHosts(inventory.id, params),
|
||||||
|
InventoriesAPI.readAdHocOptions(inventory.id),
|
||||||
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hosts: data.results,
|
hosts: hostResponse.data.results,
|
||||||
count: data.count,
|
count: hostResponse.data.count,
|
||||||
|
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||||
|
credentialTypeId: cred.data.results[0].id,
|
||||||
|
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||||
};
|
};
|
||||||
}, [location.search, inventory.id]),
|
}, [location.search, inventory.id]),
|
||||||
{
|
{
|
||||||
hosts: [],
|
hosts: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
|
moduleOptions: [],
|
||||||
|
isAdHocDisabled: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -57,109 +68,106 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
|||||||
}, [fetchHosts]);
|
}, [fetchHosts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaginatedDataList
|
<>
|
||||||
contentError={contentError}
|
<PaginatedDataList
|
||||||
hasContentLoading={isLoading}
|
contentError={contentError}
|
||||||
items={hosts}
|
hasContentLoading={isLoading}
|
||||||
itemCount={count}
|
items={hosts}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
itemCount={count}
|
||||||
qsConfig={QS_CONFIG}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
onRowClick={handleSelect}
|
qsConfig={QS_CONFIG}
|
||||||
toolbarSearchColumns={[
|
onRowClick={handleSelect}
|
||||||
{
|
toolbarSearchColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'name',
|
name: i18n._(t`Name`),
|
||||||
isDefault: true,
|
key: 'name',
|
||||||
},
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: i18n._(t`Created by (username)`),
|
{
|
||||||
key: 'created_by__username',
|
name: i18n._(t`Created by (username)`),
|
||||||
},
|
key: 'created_by__username',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Modified by (username)`),
|
{
|
||||||
key: 'modified_by__username',
|
name: i18n._(t`Modified by (username)`),
|
||||||
},
|
key: 'modified_by__username',
|
||||||
]}
|
},
|
||||||
toolbarSortColumns={[
|
]}
|
||||||
{
|
toolbarSortColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'name',
|
name: i18n._(t`Name`),
|
||||||
},
|
key: 'name',
|
||||||
]}
|
},
|
||||||
renderToolbar={props => (
|
]}
|
||||||
<DataListToolbar
|
renderToolbar={props => (
|
||||||
{...props}
|
<DataListToolbar
|
||||||
showSelectAll
|
{...props}
|
||||||
isAllSelected={isAllSelected}
|
showSelectAll
|
||||||
onSelectAll={isSelected => setSelected(isSelected ? [...hosts] : [])}
|
isAllSelected={isAllSelected}
|
||||||
qsConfig={QS_CONFIG}
|
onSelectAll={isSelected =>
|
||||||
additionalControls={
|
setSelected(isSelected ? [...hosts] : [])
|
||||||
inventory?.summary_fields?.user_capabilities?.adhoc
|
}
|
||||||
? [
|
qsConfig={QS_CONFIG}
|
||||||
<Kebabified>
|
additionalControls={
|
||||||
{({ isKebabified }) =>
|
inventory?.summary_fields?.user_capabilities?.adhoc
|
||||||
isKebabified ? (
|
? [
|
||||||
<AdHocCommandsButton
|
<Kebabified>
|
||||||
adHocItems={selected}
|
{({ isKebabified }) =>
|
||||||
apiModule={InventoriesAPI}
|
isKebabified ? (
|
||||||
itemId={parseInt(inventory.id, 10)}
|
<DropdownItem
|
||||||
>
|
aria-label={i18n._(t`Run command`)}
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
<DropdownItem
|
isDisabled={count === 0 || isAdHocDisabled}
|
||||||
key="run command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={count === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</AdHocCommandsButton>
|
|
||||||
) : (
|
|
||||||
<ToolbarItem>
|
|
||||||
<Tooltip
|
|
||||||
content={i18n._(
|
|
||||||
t`Select an inventory source by clicking the check box beside it. The inventory source can be a single host or a selection of multiple hosts.`
|
|
||||||
)}
|
|
||||||
position="top"
|
|
||||||
key="adhoc"
|
|
||||||
>
|
>
|
||||||
<AdHocCommandsButton
|
{i18n._(t`Run command`)}
|
||||||
css="margin-right: 20px"
|
</DropdownItem>
|
||||||
adHocItems={selected}
|
) : (
|
||||||
apiModule={InventoriesAPI}
|
<ToolbarItem>
|
||||||
itemId={parseInt(inventory.id, 10)}
|
<Tooltip
|
||||||
>
|
content={i18n._(
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
t`Select an inventory source by clicking the check box beside it. The inventory source can be a single host or a selection of multiple hosts.`
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={count === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</AdHocCommandsButton>
|
position="top"
|
||||||
</Tooltip>
|
key="adhoc"
|
||||||
</ToolbarItem>
|
>
|
||||||
)
|
<Button
|
||||||
}
|
variant="secondary"
|
||||||
</Kebabified>,
|
aria-label={i18n._(t`Run command`)}
|
||||||
]
|
onClick={() => setIsAdHocCommandsOpen(true)}
|
||||||
: []
|
isDisabled={count === 0 || isAdHocDisabled}
|
||||||
}
|
>
|
||||||
|
{i18n._(t`Run command`)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</ToolbarItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Kebabified>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={host => (
|
||||||
|
<SmartInventoryHostListItem
|
||||||
|
key={host.id}
|
||||||
|
host={host}
|
||||||
|
detailUrl={`/inventories/smart_inventory/${inventory.id}/hosts/${host.id}/details`}
|
||||||
|
isSelected={selected.some(row => row.id === host.id)}
|
||||||
|
onSelect={() => handleSelect(host)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{isAdHocCommandsOpen && (
|
||||||
|
<AdHocCommands
|
||||||
|
css="margin-right: 20px"
|
||||||
|
adHocItems={selected}
|
||||||
|
itemId={parseInt(inventory.id, 10)}
|
||||||
|
onClose={() => setIsAdHocCommandsOpen(false)}
|
||||||
|
credentialTypeId={credentialTypeId}
|
||||||
|
moduleOptions={moduleOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderItem={host => (
|
</>
|
||||||
<SmartInventoryHostListItem
|
|
||||||
key={host.id}
|
|
||||||
host={host}
|
|
||||||
detailUrl={`/inventories/smart_inventory/${inventory.id}/hosts/${host.id}/details`}
|
|
||||||
isSelected={selected.some(row => row.id === host.id)}
|
|
||||||
onSelect={() => handleSelect(host)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import mockHosts from '../shared/data.hosts.json';
|
|||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
|
|
||||||
describe('<SmartInventoryHostList />', () => {
|
describe('<SmartInventoryHostList />', () => {
|
||||||
// describe('User has adhoc permissions', () => {
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const clonedInventory = {
|
const clonedInventory = {
|
||||||
...mockInventory,
|
...mockInventory,
|
||||||
@@ -41,17 +40,7 @@ describe('<SmartInventoryHostList />', () => {
|
|||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<SmartInventoryHostList inventory={clonedInventory}>
|
<SmartInventoryHostList inventory={clonedInventory} />
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="run-command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SmartInventoryHostList>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
@@ -77,6 +66,7 @@ describe('<SmartInventoryHostList />', () => {
|
|||||||
});
|
});
|
||||||
const runCommandsButton = wrapper.find('button[aria-label="Run command"]');
|
const runCommandsButton = wrapper.find('button[aria-label="Run command"]');
|
||||||
expect(runCommandsButton.length).toBe(1);
|
expect(runCommandsButton.length).toBe(1);
|
||||||
|
expect(runCommandsButton.prop('disabled')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should select and deselect all items', async () => {
|
test('should select and deselect all items', async () => {
|
||||||
@@ -96,14 +86,6 @@ describe('<SmartInventoryHostList />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render enabled ad hoc commands button', async () => {
|
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'button[aria-label="Run command"]',
|
|
||||||
el => el.prop('disabled') === false
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show content error when api throws an error', async () => {
|
test('should show content error when api throws an error', async () => {
|
||||||
InventoriesAPI.readHosts.mockImplementation(() =>
|
InventoriesAPI.readHosts.mockImplementation(() =>
|
||||||
Promise.reject(new Error())
|
Promise.reject(new Error())
|
||||||
@@ -115,4 +97,24 @@ describe('<SmartInventoryHostList />', () => {
|
|||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
|
test('should disable run commands button', async () => {
|
||||||
|
InventoriesAPI.readHosts.mockResolvedValue({
|
||||||
|
data: { results: [], count: 0 },
|
||||||
|
});
|
||||||
|
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: { module_name: { choices: [['module']] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryHostList inventory={clonedInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
const runCommandsButton = wrapper.find('button[aria-label="Run command"]');
|
||||||
|
expect(runCommandsButton.prop('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user