mirror of
https://github.com/ansible/awx.git
synced 2026-03-03 01:38:50 -03:30
Uses existing kebabified workflow for run command
This commit is contained in:
@@ -1,26 +1,27 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useState, useContext } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory, useParams } 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 { Button, DropdownItem } from '@patternfly/react-core';
|
||||||
|
|
||||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||||
import { InventoriesAPI } from '../../api';
|
import { InventoriesAPI, CredentialTypesAPI } from '../../api';
|
||||||
|
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import ErrorDetail from '../ErrorDetail';
|
import ErrorDetail from '../ErrorDetail';
|
||||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||||
|
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||||
import ContentLoading from '../ContentLoading';
|
import ContentLoading from '../ContentLoading';
|
||||||
|
import ContentError from '../ContentError';
|
||||||
|
|
||||||
function AdHocCommands({
|
function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
||||||
onClose,
|
|
||||||
adHocItems,
|
|
||||||
itemId,
|
|
||||||
i18n,
|
|
||||||
moduleOptions,
|
|
||||||
credentialTypeId,
|
|
||||||
}) {
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
|
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||||
|
|
||||||
const verbosityOptions = [
|
const verbosityOptions = [
|
||||||
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
||||||
{ value: '1', key: '1', label: i18n._(t`1 (Verbose)`) },
|
{ value: '1', key: '1', label: i18n._(t`1 (Verbose)`) },
|
||||||
@@ -28,26 +29,51 @@ function AdHocCommands({
|
|||||||
{ 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)`) },
|
||||||
];
|
];
|
||||||
|
useEffect(() => {
|
||||||
|
if (isKebabified) {
|
||||||
|
onKebabModalChange(isWizardOpen);
|
||||||
|
}
|
||||||
|
}, [isKebabified, isWizardOpen, onKebabModalChange]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { moduleOptions, credentialTypeId, isAdHocDisabled },
|
||||||
|
request: fetchData,
|
||||||
|
error: fetchError,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const [options, cred] = await Promise.all([
|
||||||
|
InventoriesAPI.readAdHocOptions(id),
|
||||||
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
moduleOptions: options.data.actions.GET.module_name.choices,
|
||||||
|
credentialTypeId: cred.data.results[0].id,
|
||||||
|
isAdHocDisabled: !options.data.actions.POST,
|
||||||
|
};
|
||||||
|
}, [id]),
|
||||||
|
{ moduleOptions: [], isAdHocDisabled: true }
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
const {
|
const {
|
||||||
isloading: isLaunchLoading,
|
isloading: isLaunchLoading,
|
||||||
error,
|
error: launchError,
|
||||||
request: launchAdHocCommands,
|
request: launchAdHocCommands,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(
|
useCallback(
|
||||||
async values => {
|
async values => {
|
||||||
const { data } = await InventoriesAPI.launchAdHocCommands(
|
const { data } = await InventoriesAPI.launchAdHocCommands(id, values);
|
||||||
itemId,
|
|
||||||
values
|
|
||||||
);
|
|
||||||
history.push(`/jobs/command/${data.id}/output`);
|
history.push(`/jobs/command/${data.id}/output`);
|
||||||
},
|
},
|
||||||
|
|
||||||
[itemId, history]
|
[id, history]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { dismissError } = useDismissableError(error);
|
const { error, dismissError } = useDismissableError(
|
||||||
|
launchError || fetchError
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
const { credential, ...remainingValues } = values;
|
const { credential, ...remainingValues } = values;
|
||||||
@@ -64,7 +90,7 @@ function AdHocCommands({
|
|||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error && isWizardOpen) {
|
||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={error}
|
isOpen={error}
|
||||||
@@ -72,31 +98,63 @@ function AdHocCommands({
|
|||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
dismissError();
|
dismissError();
|
||||||
|
setIsWizardOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<>
|
{launchError ? (
|
||||||
{i18n._(t`Failed to launch job.`)}
|
<>
|
||||||
<ErrorDetail error={error} />
|
{i18n._(t`Failed to launch job.`)}
|
||||||
</>
|
<ErrorDetail error={error} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ContentError error={error} />
|
||||||
|
)}
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AdHocCommandsWizard
|
// render buttons for drop down and for toolbar
|
||||||
adHocItems={adHocItems}
|
// if modal is open render the modal
|
||||||
moduleOptions={moduleOptions}
|
<>
|
||||||
verbosityOptions={verbosityOptions}
|
{isKebabified ? (
|
||||||
credentialTypeId={credentialTypeId}
|
<DropdownItem
|
||||||
onCloseWizard={onClose}
|
key="cancel-job"
|
||||||
onLaunch={handleSubmit}
|
isDisabled={isAdHocDisabled || !hasListItems}
|
||||||
onDismissError={() => dismissError()}
|
component="button"
|
||||||
/>
|
aria-label={i18n._(t`Run Command`)}
|
||||||
|
onClick={() => setIsWizardOpen(true)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Run Command`)}
|
||||||
|
</DropdownItem>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={i18n._(t`Run Command`)}
|
||||||
|
onClick={() => setIsWizardOpen(true)}
|
||||||
|
isDisabled={isAdHocDisabled || !hasListItems}
|
||||||
|
>
|
||||||
|
{i18n._(t`Run Command`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isWizardOpen && (
|
||||||
|
<AdHocCommandsWizard
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
moduleOptions={moduleOptions}
|
||||||
|
verbosityOptions={verbosityOptions}
|
||||||
|
credentialTypeId={credentialTypeId}
|
||||||
|
onCloseWizard={() => setIsWizardOpen(false)}
|
||||||
|
onLaunch={handleSubmit}
|
||||||
|
onDismissError={() => dismissError()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdHocCommands.propTypes = {
|
AdHocCommands.propTypes = {
|
||||||
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
itemId: PropTypes.number.isRequired,
|
hasListItems: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(AdHocCommands);
|
export default withI18n()(AdHocCommands);
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import AdHocCommands from './AdHocCommands';
|
|||||||
jest.mock('../../api/models/CredentialTypes');
|
jest.mock('../../api/models/CredentialTypes');
|
||||||
jest.mock('../../api/models/Inventories');
|
jest.mock('../../api/models/Inventories');
|
||||||
jest.mock('../../api/models/Credentials');
|
jest.mock('../../api/models/Credentials');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useParams: () => ({
|
||||||
|
id: 1,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
const credentials = [
|
const credentials = [
|
||||||
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
||||||
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
||||||
@@ -18,10 +23,7 @@ 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',
|
||||||
@@ -30,6 +32,26 @@ const adHocItems = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe('<AdHocCommands />', () => {
|
describe('<AdHocCommands />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {
|
||||||
|
module_name: {
|
||||||
|
choices: [
|
||||||
|
['command', 'command'],
|
||||||
|
['shell', 'shell'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
|
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
||||||
|
});
|
||||||
|
});
|
||||||
let wrapper;
|
let wrapper;
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
@@ -39,19 +61,45 @@ describe('<AdHocCommands />', () => {
|
|||||||
test('mounts successfully', async () => {
|
test('mounts successfully', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||||
css="margin-right: 20px"
|
|
||||||
onClose={() => {}}
|
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
moduleOptions={moduleOptions}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 adHocItems={adHocItems} hasListItems />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should submit properly', async () => {
|
test('should submit properly', async () => {
|
||||||
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
@@ -62,17 +110,13 @@ describe('<AdHocCommands />', () => {
|
|||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||||
css="margin-right: 20px"
|
|
||||||
onClose={() => {}}
|
|
||||||
itemId={1}
|
|
||||||
credentialTypeId={1}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
moduleOptions={moduleOptions}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||||
|
);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
@@ -174,17 +218,13 @@ describe('<AdHocCommands />', () => {
|
|||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||||
css="margin-right: 20px"
|
|
||||||
onClose={() => {}}
|
|
||||||
credentialTypeId={1}
|
|
||||||
itemId={1}
|
|
||||||
adHocItems={adHocItems}
|
|
||||||
moduleOptions={moduleOptions}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||||
|
);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
@@ -237,4 +277,69 @@ describe('<AdHocCommands />', () => {
|
|||||||
|
|
||||||
await waitForElement(wrapper, 'ErrorDetail', el => el.length > 0);
|
await waitForElement(wrapper, 'ErrorDetail', el => el.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should disable run command button due to permissions', async () => {
|
||||||
|
InventoriesAPI.readHosts.mockResolvedValue({
|
||||||
|
data: { results: [], count: 0 },
|
||||||
|
});
|
||||||
|
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: { module_name: { choices: [['module']] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
const runCommandsButton = wrapper.find('button[aria-label="Run Command"]');
|
||||||
|
expect(runCommandsButton.prop('disabled')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable run command button due to lack of list items', async () => {
|
||||||
|
InventoriesAPI.readHosts.mockResolvedValue({
|
||||||
|
data: { results: [], count: 0 },
|
||||||
|
});
|
||||||
|
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: { module_name: { choices: [['module']] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AdHocCommands adHocItems={adHocItems} hasListItems={false} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
const runCommandsButton = wrapper.find('button[aria-label="Run Command"]');
|
||||||
|
expect(runCommandsButton.prop('disabled')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 adHocItems={adHocItems} hasListItems />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => wrapper.find('button').prop('onClick')());
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,8 +134,7 @@ const FormikApp = withFormik({
|
|||||||
|
|
||||||
FormikApp.propTypes = {
|
FormikApp.propTypes = {
|
||||||
onLaunch: PropTypes.func.isRequired,
|
onLaunch: PropTypes.func.isRequired,
|
||||||
moduleOptions: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string))
|
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
|
||||||
.isRequired,
|
|
||||||
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onCloseWizard: PropTypes.func.isRequired,
|
onCloseWizard: PropTypes.func.isRequired,
|
||||||
credentialTypeId: PropTypes.number.isRequired,
|
credentialTypeId: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
label={i18n._(t`Arguments`)}
|
label={i18n._(t`Arguments`)}
|
||||||
validated={isValid ? 'default' : 'error'}
|
validated={isValid ? 'default' : 'error'}
|
||||||
onBlur={() => argumentsHelpers.setTouched(true)}
|
onBlur={() => argumentsHelpers.setTouched(true)}
|
||||||
placeholder={i18n._(t`Enter arguments`)}
|
|
||||||
isRequired={
|
isRequired={
|
||||||
moduleNameField.value === 'command' ||
|
moduleNameField.value === 'command' ||
|
||||||
moduleNameField.value === 'shell'
|
moduleNameField.value === 'shell'
|
||||||
@@ -317,8 +316,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdHocDetailsStep.propTypes = {
|
AdHocDetailsStep.propTypes = {
|
||||||
moduleOptions: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string))
|
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
|
||||||
.isRequired,
|
|
||||||
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,8 @@ import React, { useEffect, useCallback, useState } from 'react';
|
|||||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
import { useHistory, useLocation, useParams } 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 {
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DropdownItem,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { getQSConfig, mergeParams, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, mergeParams, parseQueryString } from '../../../util/qs';
|
||||||
import { GroupsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||||
|
|
||||||
import useRequest, {
|
import useRequest, {
|
||||||
useDeleteItems,
|
useDeleteItems,
|
||||||
@@ -22,7 +16,6 @@ import ErrorDetail from '../../../components/ErrorDetail';
|
|||||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
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 AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import InventoryGroupHostListItem from './InventoryGroupHostListItem';
|
import InventoryGroupHostListItem from './InventoryGroupHostListItem';
|
||||||
import AddDropdown from '../shared/AddDropdown';
|
import AddDropdown from '../shared/AddDropdown';
|
||||||
@@ -35,7 +28,6 @@ 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();
|
||||||
@@ -47,9 +39,6 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
moduleOptions,
|
|
||||||
credentialTypeId,
|
|
||||||
isAdHocDisabled,
|
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -57,16 +46,9 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const [
|
const [response, actionsResponse] = await Promise.all([
|
||||||
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 {
|
||||||
@@ -79,9 +61,6 @@ 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]),
|
||||||
{
|
{
|
||||||
@@ -90,8 +69,6 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
actions: {},
|
actions: {},
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
searchableKeys: [],
|
searchableKeys: [],
|
||||||
moduleOptions: [],
|
|
||||||
isAdHocDisabled: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,40 +207,10 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
...(canAdd ? [addButton] : []),
|
...(canAdd ? [addButton] : []),
|
||||||
<Kebabified>
|
<AdHocCommands
|
||||||
{({ isKebabified }) =>
|
adHocItems={selected}
|
||||||
isKebabified ? (
|
hasListItems={hostCount > 0}
|
||||||
<DropdownItem
|
/>,
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={hostCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={hostCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Kebabified>,
|
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
onDisassociate={handleDisassociate}
|
onDisassociate={handleDisassociate}
|
||||||
@@ -301,16 +248,6 @@ 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}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { GroupsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -35,17 +35,6 @@ 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 />);
|
||||||
});
|
});
|
||||||
@@ -107,29 +96,6 @@ describe('<InventoryGroupHostList />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render enabled ad hoc commands button', async () => {
|
|
||||||
GroupsAPI.readAllHosts.mockResolvedValue({
|
|
||||||
data: { ...mockHosts },
|
|
||||||
});
|
|
||||||
InventoriesAPI.readHostsOptions.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: {},
|
|
||||||
POST: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'button[aria-label="Run command"]',
|
|
||||||
el => el.prop('disabled') === false
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show add dropdown button according to permissions', async () => {
|
test('should show add dropdown button according to permissions', async () => {
|
||||||
expect(wrapper.find('AddDropdown').length).toBe(1);
|
expect(wrapper.find('AddDropdown').length).toBe(1);
|
||||||
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -2,17 +2,11 @@ import React, { useCallback, useState, useEffect } 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';
|
||||||
import {
|
import { Button, Tooltip } from '@patternfly/react-core';
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DropdownItem,
|
|
||||||
ToolbarGroup,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
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, CredentialTypesAPI } from '../../../api';
|
import { InventoriesAPI, GroupsAPI } 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';
|
||||||
@@ -24,7 +18,6 @@ import InventoryGroupItem from './InventoryGroupItem';
|
|||||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||||
|
|
||||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('group', {
|
const QS_CONFIG = getQSConfig('group', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -52,7 +45,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();
|
||||||
@@ -64,9 +57,6 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
moduleOptions,
|
|
||||||
credentialTypeId,
|
|
||||||
isAdHocDisabled,
|
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -74,11 +64,9 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const [response, groupOptions, adHocOptions, cred] = await Promise.all([
|
const [response, groupOptions] = 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 {
|
||||||
@@ -91,9 +79,6 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
groupOptions.data.actions?.GET || {}
|
groupOptions.data.actions?.GET || {}
|
||||||
).filter(key => groupOptions.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]),
|
||||||
{
|
{
|
||||||
@@ -161,29 +146,6 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
};
|
};
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
const kebabedAdditionalControls = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropdownItem
|
|
||||||
key="run command"
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={groupCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
|
|
||||||
<DropdownItem
|
|
||||||
variant="danger"
|
|
||||||
aria-label={i18n._(t`Delete`)}
|
|
||||||
key="delete"
|
|
||||||
onClick={toggleModal}
|
|
||||||
isDisabled={selected.length === 0 || selected.some(cannotDelete)}
|
|
||||||
>
|
|
||||||
{i18n._(t`Delete`)}
|
|
||||||
</DropdownItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -253,57 +215,24 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
<Kebabified>
|
<AdHocCommands
|
||||||
{({ isKebabified }) => (
|
adHocItems={selected}
|
||||||
<>
|
hasListItems={groupCount > 0}
|
||||||
{isKebabified ? (
|
/>,
|
||||||
kebabedAdditionalControls()
|
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||||
) : (
|
<div>
|
||||||
<ToolbarGroup>
|
<Button
|
||||||
<ToolbarItem>
|
variant="danger"
|
||||||
<Tooltip
|
aria-label={i18n._(t`Delete`)}
|
||||||
content={i18n._(
|
onClick={toggleModal}
|
||||||
t`Select an inventory source by clicking the check box beside it. The inventory source can be a single group or a selection of multiple groups.`
|
isDisabled={
|
||||||
)}
|
selected.length === 0 || selected.some(cannotDelete)
|
||||||
position="top"
|
}
|
||||||
key="adhoc"
|
>
|
||||||
>
|
{i18n._(t`Delete`)}
|
||||||
<Button
|
</Button>
|
||||||
variant="secondary"
|
</div>
|
||||||
aria-label={i18n._(t`Run command`)}
|
</Tooltip>,
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={groupCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
<ToolbarItem>
|
|
||||||
<Tooltip
|
|
||||||
content={renderTooltip()}
|
|
||||||
position="top"
|
|
||||||
key="delete"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
aria-label={i18n._(t`Delete`)}
|
|
||||||
onClick={toggleModal}
|
|
||||||
isDisabled={
|
|
||||||
selected.length === 0 ||
|
|
||||||
selected.some(cannotDelete)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{i18n._(t`Delete`)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
</ToolbarGroup>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Kebabified>,
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -316,16 +245,6 @@ 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}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { InventoriesAPI, GroupsAPI, CredentialTypesAPI } from '../../../api';
|
import { InventoriesAPI, GroupsAPI } from '../../../api';
|
||||||
import InventoryGroupsList from './InventoryGroupsList';
|
import InventoryGroupsList from './InventoryGroupsList';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -71,17 +71,6 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: { module_name: { choices: [['module']] } },
|
|
||||||
POST: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
|
||||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
|
||||||
});
|
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/inventories/inventory/3/groups'],
|
initialEntries: ['/inventories/inventory/3/groups'],
|
||||||
});
|
});
|
||||||
@@ -158,13 +147,6 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
expect(el.props().checked).toBe(false);
|
expect(el.props().checked).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('should render enabled ad hoc commands button', async () => {
|
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'button[aria-label="Run command"]',
|
|
||||||
el => el.prop('disabled') === false
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('<InventoryGroupsList/> error handling', () => {
|
describe('<InventoryGroupsList/> error handling', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
@@ -194,16 +176,6 @@ describe('<InventoryGroupsList/> error handling', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: { module_name: { choices: [['module']] } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
|
||||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -230,21 +202,8 @@ describe('<InventoryGroupsList/> error handling', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should show error modal when group is not successfully deleted from api', async () => {
|
test('should show error modal when group is not successfully deleted from api', async () => {
|
||||||
const history = createMemoryHistory({
|
|
||||||
initialEntries: ['/inventories/inventory/3/groups'],
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(<InventoryGroupsList />);
|
||||||
<Route path="/inventories/inventory/:id/groups">
|
|
||||||
<InventoryGroupsList />
|
|
||||||
</Route>,
|
|
||||||
{
|
|
||||||
context: {
|
|
||||||
router: { history, route: { location: history.location } },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
|
||||||
@@ -281,27 +240,4 @@ describe('<InventoryGroupsList/> error handling', () => {
|
|||||||
.invoke('onClose')();
|
.invoke('onClose')();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('should render disabled ad hoc button', async () => {
|
|
||||||
const history = createMemoryHistory({
|
|
||||||
initialEntries: ['/inventories/inventory/3/groups'],
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
wrapper = mountWithContexts(
|
|
||||||
<Route path="/inventories/inventory/:id/groups">
|
|
||||||
<InventoryGroupsList />
|
|
||||||
</Route>,
|
|
||||||
{
|
|
||||||
context: {
|
|
||||||
router: { history, route: { location: history.location } },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
|
||||||
expect(
|
|
||||||
wrapper.find('button[aria-label="Run command"]').prop('disabled')
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,19 +2,13 @@ import React, { useState, useEffect, 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';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DropdownItem,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { getQSConfig, parseQueryString, mergeParams } from '../../../util/qs';
|
import { getQSConfig, parseQueryString, mergeParams } from '../../../util/qs';
|
||||||
import useRequest, {
|
import useRequest, {
|
||||||
useDismissableError,
|
useDismissableError,
|
||||||
useDeleteItems,
|
useDeleteItems,
|
||||||
} from '../../../util/useRequest';
|
} from '../../../util/useRequest';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
import { HostsAPI, InventoriesAPI } 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';
|
||||||
@@ -23,7 +17,6 @@ import PaginatedDataList, {
|
|||||||
} from '../../../components/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 AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
||||||
|
|
||||||
@@ -35,7 +28,6 @@ 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();
|
||||||
|
|
||||||
@@ -46,9 +38,6 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
moduleOptions,
|
|
||||||
isAdHocDisabled,
|
|
||||||
credentialTypeId,
|
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -62,13 +51,9 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
data: { count, results },
|
data: { count, results },
|
||||||
},
|
},
|
||||||
hostGroupOptions,
|
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 {
|
||||||
@@ -81,9 +66,6 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
hostGroupOptions.data.actions?.GET || {}
|
hostGroupOptions.data.actions?.GET || {}
|
||||||
).filter(key => hostGroupOptions.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
|
||||||
{
|
{
|
||||||
@@ -92,8 +74,6 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
actions: {},
|
actions: {},
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
searchableKeys: [],
|
searchableKeys: [],
|
||||||
moduleOptions: [],
|
|
||||||
isAdHocDisabled: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -222,40 +202,10 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
<Kebabified>
|
<AdHocCommands
|
||||||
{({ isKebabified }) =>
|
adHocItems={selected}
|
||||||
isKebabified ? (
|
hasListItems={itemCount > 0}
|
||||||
<DropdownItem
|
/>,
|
||||||
key="run command"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={itemCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<ToolbarItem>
|
|
||||||
<Tooltip
|
|
||||||
content={i18n._(
|
|
||||||
t`Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.`
|
|
||||||
)}
|
|
||||||
position="top"
|
|
||||||
key="adhoc"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
key="run command"
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={itemCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Kebabified>,
|
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
onDisassociate={handleDisassociate}
|
onDisassociate={handleDisassociate}
|
||||||
@@ -288,16 +238,6 @@ 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}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
import { HostsAPI, InventoriesAPI } from '../../../api';
|
||||||
import InventoryHostGroupsList from './InventoryHostGroupsList';
|
import InventoryHostGroupsList from './InventoryHostGroupsList';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -80,17 +80,6 @@ describe('<InventoryHostGroupsList />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
actions: {
|
|
||||||
GET: { module_name: { choices: [['module']] } },
|
|
||||||
POST: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
|
||||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
|
||||||
});
|
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/inventories/inventory/1/hosts/3/groups'],
|
initialEntries: ['/inventories/inventory/1/hosts/3/groups'],
|
||||||
});
|
});
|
||||||
@@ -283,11 +272,4 @@ describe('<InventoryHostGroupsList />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('AlertModal ErrorDetail').length).toBe(1);
|
expect(wrapper.find('AlertModal ErrorDetail').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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,14 +2,8 @@ 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';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DropdownItem,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import { InventoriesAPI, HostsAPI, CredentialTypesAPI } from '../../../api';
|
import { InventoriesAPI, HostsAPI } from '../../../api';
|
||||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
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';
|
||||||
@@ -18,7 +12,6 @@ import PaginatedDataList, {
|
|||||||
ToolbarAddButton,
|
ToolbarAddButton,
|
||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
} from '../../../components/PaginatedDataList';
|
} from '../../../components/PaginatedDataList';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
|
||||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import InventoryHostItem from './InventoryHostItem';
|
import InventoryHostItem from './InventoryHostItem';
|
||||||
|
|
||||||
@@ -29,7 +22,6 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryHostList({ i18n }) {
|
function InventoryHostList({ i18n }) {
|
||||||
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
@@ -41,9 +33,6 @@ function InventoryHostList({ i18n }) {
|
|||||||
actions,
|
actions,
|
||||||
relatedSearchableKeys,
|
relatedSearchableKeys,
|
||||||
searchableKeys,
|
searchableKeys,
|
||||||
moduleOptions,
|
|
||||||
credentialTypeId,
|
|
||||||
isAdHocDisabled,
|
|
||||||
},
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -51,11 +40,9 @@ function InventoryHostList({ i18n }) {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, search);
|
const params = parseQueryString(QS_CONFIG, search);
|
||||||
const [response, hostOptions, adHocOptions, cred] = await Promise.all([
|
const [response, hostOptions] = await Promise.all([
|
||||||
InventoriesAPI.readHosts(id, params),
|
InventoriesAPI.readHosts(id, params),
|
||||||
InventoriesAPI.readHostsOptions(id),
|
InventoriesAPI.readHostsOptions(id),
|
||||||
InventoriesAPI.readAdHocOptions(id),
|
|
||||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -68,9 +55,6 @@ function InventoryHostList({ i18n }) {
|
|||||||
searchableKeys: Object.keys(hostOptions.data.actions?.GET || {}).filter(
|
searchableKeys: Object.keys(hostOptions.data.actions?.GET || {}).filter(
|
||||||
key => hostOptions.data.actions?.GET[key].filterable
|
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]),
|
}, [id, search]),
|
||||||
{
|
{
|
||||||
@@ -79,8 +63,6 @@ function InventoryHostList({ i18n }) {
|
|||||||
actions: {},
|
actions: {},
|
||||||
relatedSearchableKeys: [],
|
relatedSearchableKeys: [],
|
||||||
searchableKeys: [],
|
searchableKeys: [],
|
||||||
moduleOptions: [],
|
|
||||||
isAdHocDisabled: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -162,40 +144,10 @@ function InventoryHostList({ i18n }) {
|
|||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
<Kebabified>
|
<AdHocCommands
|
||||||
{({ isKebabified }) =>
|
adHocItems={selected}
|
||||||
isKebabified ? (
|
hasListItems={hostCount > 0}
|
||||||
<DropdownItem
|
/>,
|
||||||
key="run command"
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={hostCount === 0 || isAdHocDisabled}
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
key="run command"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={hostCount === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Kebabified>,
|
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
onDelete={deleteHosts}
|
onDelete={deleteHosts}
|
||||||
@@ -224,16 +176,6 @@ function InventoryHostList({ i18n }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{isAdHocCommandsOpen && (
|
|
||||||
<AdHocCommands
|
|
||||||
css="margin-right: 20px"
|
|
||||||
adHocItems={selected}
|
|
||||||
onClose={() => setIsAdHocCommandsOpen(false)}
|
|
||||||
credentialTypeId={credentialTypeId}
|
|
||||||
moduleOptions={moduleOptions}
|
|
||||||
itemId={parseInt(id, 10)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{deletionError && (
|
{deletionError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { InventoriesAPI, HostsAPI, CredentialTypesAPI } from '../../../api';
|
import { InventoriesAPI, HostsAPI } from '../../../api';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -93,17 +93,6 @@ describe('<InventoryHostList />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
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(<InventoryHostList />);
|
wrapper = mountWithContexts(<InventoryHostList />);
|
||||||
});
|
});
|
||||||
@@ -276,13 +265,6 @@ 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.readHostsOptions.mockResolvedValueOnce({
|
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DropdownItem,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { useParams, useLocation, useHistory } from 'react-router-dom';
|
import { useParams, useLocation, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||||
@@ -18,7 +12,6 @@ import DataListToolbar from '../../../components/DataListToolbar';
|
|||||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||||
import InventoryGroupRelatedGroupListItem from './InventoryRelatedGroupListItem';
|
import InventoryGroupRelatedGroupListItem from './InventoryRelatedGroupListItem';
|
||||||
import AddDropdown from '../shared/AddDropdown';
|
import AddDropdown from '../shared/AddDropdown';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
|
||||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
import AssociateModal from '../../../components/AssociateModal';
|
import AssociateModal from '../../../components/AssociateModal';
|
||||||
import DisassociateButton from '../../../components/DisassociateButton';
|
import DisassociateButton from '../../../components/DisassociateButton';
|
||||||
@@ -157,56 +150,10 @@ function InventoryRelatedGroupList({ i18n }) {
|
|||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
...(canAdd ? [addButton] : []),
|
...(canAdd ? [addButton] : []),
|
||||||
<Kebabified>
|
<AdHocCommands
|
||||||
{({ isKebabified }) =>
|
adHocItems={selected}
|
||||||
isKebabified ? (
|
hasListItems={itemCount > 0}
|
||||||
<AdHocCommands
|
/>,
|
||||||
adHocItems={selected}
|
|
||||||
apiModule={InventoriesAPI}
|
|
||||||
itemId={parseInt(inventoryId, 10)}
|
|
||||||
>
|
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<DropdownItem
|
|
||||||
key="run command"
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={itemCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</AdHocCommands>
|
|
||||||
) : (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<AdHocCommands
|
|
||||||
css="margin-right: 20px"
|
|
||||||
adHocItems={selected}
|
|
||||||
apiModule={InventoriesAPI}
|
|
||||||
itemId={parseInt(inventoryId, 10)}
|
|
||||||
>
|
|
||||||
{({ openAdHocCommands, isDisabled }) => (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={openAdHocCommands}
|
|
||||||
isDisabled={itemCount === 0 || isDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</AdHocCommands>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Kebabified>,
|
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
onDisassociate={() => {}}
|
onDisassociate={() => {}}
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import React, { useEffect, useCallback, useState } from 'react';
|
import React, { useEffect, useCallback } 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';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DropdownItem,
|
|
||||||
ToolbarItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||||
import SmartInventoryHostListItem from './SmartInventoryHostListItem';
|
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, CredentialTypesAPI } from '../../../api';
|
import { InventoriesAPI } from '../../../api';
|
||||||
import { Inventory } from '../../../types';
|
import { Inventory } from '../../../types';
|
||||||
import { Kebabified } from '../../../contexts/Kebabified';
|
|
||||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('host', {
|
const QS_CONFIG = getQSConfig('host', {
|
||||||
@@ -27,35 +20,27 @@ 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, moduleOptions, credentialTypeId, isAdHocDisabled },
|
result: { hosts, count },
|
||||||
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 [hostResponse, adHocOptions, cred] = await Promise.all([
|
const {
|
||||||
InventoriesAPI.readHosts(inventory.id, params),
|
data: { results, count: hostCount },
|
||||||
InventoriesAPI.readAdHocOptions(inventory.id),
|
} = await InventoriesAPI.readHosts(inventory.id, params);
|
||||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hosts: hostResponse.data.results,
|
hosts: results,
|
||||||
count: hostResponse.data.count,
|
count: hostCount,
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,38 +95,10 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
|||||||
additionalControls={
|
additionalControls={
|
||||||
inventory?.summary_fields?.user_capabilities?.adhoc
|
inventory?.summary_fields?.user_capabilities?.adhoc
|
||||||
? [
|
? [
|
||||||
<Kebabified>
|
<AdHocCommands
|
||||||
{({ isKebabified }) =>
|
adHocItems={selected}
|
||||||
isKebabified ? (
|
hasListItems={count > 0}
|
||||||
<DropdownItem
|
/>,
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={count === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Run command`)}
|
|
||||||
onClick={() => setIsAdHocCommandsOpen(true)}
|
|
||||||
isDisabled={count === 0 || isAdHocDisabled}
|
|
||||||
>
|
|
||||||
{i18n._(t`Run command`)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ToolbarItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Kebabified>,
|
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
@@ -157,16 +114,6 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{isAdHocCommandsOpen && (
|
|
||||||
<AdHocCommands
|
|
||||||
css="margin-right: 20px"
|
|
||||||
adHocItems={selected}
|
|
||||||
itemId={parseInt(inventory.id, 10)}
|
|
||||||
onClose={() => setIsAdHocCommandsOpen(false)}
|
|
||||||
credentialTypeId={credentialTypeId}
|
|
||||||
moduleOptions={moduleOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
import { InventoriesAPI } from '../../../api';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -27,17 +27,6 @@ describe('<SmartInventoryHostList />', () => {
|
|||||||
InventoriesAPI.readHosts.mockResolvedValue({
|
InventoriesAPI.readHosts.mockResolvedValue({
|
||||||
data: mockHosts,
|
data: mockHosts,
|
||||||
});
|
});
|
||||||
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(
|
||||||
<SmartInventoryHostList inventory={clonedInventory} />
|
<SmartInventoryHostList inventory={clonedInventory} />
|
||||||
@@ -60,15 +49,6 @@ describe('<SmartInventoryHostList />', () => {
|
|||||||
expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3);
|
expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have run command button', () => {
|
|
||||||
wrapper.find('DataListCheck').forEach(el => {
|
|
||||||
expect(el.props().checked).toBe(false);
|
|
||||||
});
|
|
||||||
const runCommandsButton = wrapper.find('button[aria-label="Run command"]');
|
|
||||||
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 () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
|
wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
|
||||||
@@ -97,24 +77,4 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function AddDropdown({ dropdownItems, i18n }) {
|
|||||||
<div ref={element} key="add">
|
<div ref={element} key="add">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
position={DropdownPosition.right}
|
position={DropdownPosition.left}
|
||||||
toggle={
|
toggle={
|
||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
id="add"
|
id="add"
|
||||||
|
|||||||
Reference in New Issue
Block a user