mirror of
https://github.com/ansible/awx.git
synced 2026-01-20 14:11:24 -03:30
Merge pull request #8263 from AlexSCorey/8238-AddRelatedGroups
8238 add related groups Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
6bd573cf07
@ -35,7 +35,7 @@ class Groups extends Base {
|
||||
}
|
||||
|
||||
readChildren(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/children/`, params);
|
||||
return this.http.get(`${this.baseUrl}${id}/children/`, { params });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import React, { useCallback, useEffect, useState, useContext } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, DropdownItem } from '@patternfly/react-core';
|
||||
|
||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||
import { InventoriesAPI } from '../../api';
|
||||
import { InventoriesAPI, CredentialTypesAPI } from '../../api';
|
||||
|
||||
import AlertModal from '../AlertModal';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import ContentError from '../ContentError';
|
||||
|
||||
function AdHocCommands({
|
||||
onClose,
|
||||
adHocItems,
|
||||
itemId,
|
||||
i18n,
|
||||
moduleOptions,
|
||||
credentialTypeId,
|
||||
}) {
|
||||
function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
||||
const history = useHistory();
|
||||
const { id } = useParams();
|
||||
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||
|
||||
const verbosityOptions = [
|
||||
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
||||
{ 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: '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 {
|
||||
isloading: isLaunchLoading,
|
||||
error,
|
||||
error: launchError,
|
||||
request: launchAdHocCommands,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async values => {
|
||||
const { data } = await InventoriesAPI.launchAdHocCommands(
|
||||
itemId,
|
||||
values
|
||||
);
|
||||
const { data } = await InventoriesAPI.launchAdHocCommands(id, values);
|
||||
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 { credential, ...remainingValues } = values;
|
||||
@ -64,7 +90,7 @@ function AdHocCommands({
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (error && isWizardOpen) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
@ -72,31 +98,63 @@ function AdHocCommands({
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => {
|
||||
dismissError();
|
||||
setIsWizardOpen(false);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</>
|
||||
{launchError ? (
|
||||
<>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</>
|
||||
) : (
|
||||
<ContentError error={error} />
|
||||
)}
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AdHocCommandsWizard
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
credentialTypeId={credentialTypeId}
|
||||
onCloseWizard={onClose}
|
||||
onLaunch={handleSubmit}
|
||||
onDismissError={() => dismissError()}
|
||||
/>
|
||||
// render buttons for drop down and for toolbar
|
||||
// if modal is open render the modal
|
||||
<>
|
||||
{isKebabified ? (
|
||||
<DropdownItem
|
||||
key="cancel-job"
|
||||
isDisabled={isAdHocDisabled || !hasListItems}
|
||||
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 = {
|
||||
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
itemId: PropTypes.number.isRequired,
|
||||
hasListItems: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(AdHocCommands);
|
||||
|
||||
@ -10,7 +10,12 @@ import AdHocCommands from './AdHocCommands';
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 1,
|
||||
}),
|
||||
}));
|
||||
const credentials = [
|
||||
{ id: 1, kind: 'cloud', name: 'Cred 1', 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: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||
];
|
||||
const moduleOptions = [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
];
|
||||
|
||||
const adHocItems = [
|
||||
{
|
||||
name: 'Inventory 1 Org 0',
|
||||
@ -30,6 +32,26 @@ const adHocItems = [
|
||||
];
|
||||
|
||||
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;
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
@ -39,19 +61,45 @@ describe('<AdHocCommands />', () => {
|
||||
test('mounts successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
css="margin-right: 20px"
|
||||
onClose={() => {}}
|
||||
itemId={1}
|
||||
credentialTypeId={1}
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={moduleOptions}
|
||||
/>
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
);
|
||||
});
|
||||
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 () => {
|
||||
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
@ -62,17 +110,13 @@ describe('<AdHocCommands />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
css="margin-right: 20px"
|
||||
onClose={() => {}}
|
||||
itemId={1}
|
||||
credentialTypeId={1}
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={moduleOptions}
|
||||
/>
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
@ -174,17 +218,13 @@ describe('<AdHocCommands />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
css="margin-right: 20px"
|
||||
onClose={() => {}}
|
||||
credentialTypeId={1}
|
||||
itemId={1}
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={moduleOptions}
|
||||
/>
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
@ -237,4 +277,69 @@ describe('<AdHocCommands />', () => {
|
||||
|
||||
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 = {
|
||||
onLaunch: PropTypes.func.isRequired,
|
||||
moduleOptions: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string))
|
||||
.isRequired,
|
||||
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
|
||||
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onCloseWizard: PropTypes.func.isRequired,
|
||||
credentialTypeId: PropTypes.number.isRequired,
|
||||
|
||||
@ -59,7 +59,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
||||
<FormFullWidthLayout>
|
||||
<FormGroup
|
||||
fieldId="module_name"
|
||||
aria-label={i18n._(t`Module`)}
|
||||
aria-label={i18n._(t`select module`)}
|
||||
label={i18n._(t`Module`)}
|
||||
isRequired
|
||||
helperTextInvalid={moduleNameMeta.error}
|
||||
@ -110,7 +110,6 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
||||
label={i18n._(t`Arguments`)}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
onBlur={() => argumentsHelpers.setTouched(true)}
|
||||
placeholder={i18n._(t`Enter arguments`)}
|
||||
isRequired={
|
||||
moduleNameField.value === 'command' ||
|
||||
moduleNameField.value === 'shell'
|
||||
@ -137,7 +136,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="verbosity"
|
||||
aria-label={i18n._(t`Verbosity`)}
|
||||
aria-label={i18n._(t`select verbosity`)}
|
||||
label={i18n._(t`Verbosity`)}
|
||||
isRequired
|
||||
validated={
|
||||
@ -317,8 +316,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) {
|
||||
}
|
||||
|
||||
AdHocDetailsStep.propTypes = {
|
||||
moduleOptions: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string))
|
||||
.isRequired,
|
||||
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
|
||||
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { arrayOf, func, object, string } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import { Button, Tooltip, DropdownItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||
|
||||
import AlertModal from '../AlertModal';
|
||||
|
||||
const ModalNote = styled.div`
|
||||
@ -19,12 +21,19 @@ function DisassociateButton({
|
||||
verifyCannotDisassociate = true,
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||
|
||||
function handleDisassociate() {
|
||||
onDisassociate();
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isKebabified) {
|
||||
onKebabModalChange(isOpen);
|
||||
}
|
||||
}, [isKebabified, isOpen, onKebabModalChange]);
|
||||
|
||||
function cannotDisassociate(item) {
|
||||
return !item.summary_fields?.user_capabilities?.delete;
|
||||
}
|
||||
@ -67,18 +76,30 @@ function DisassociateButton({
|
||||
// See: https://github.com/patternfly/patternfly-react/issues/1894
|
||||
return (
|
||||
<>
|
||||
<Tooltip content={renderTooltip()} position="top">
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Disassociate`)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{i18n._(t`Disassociate`)}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isKebabified ? (
|
||||
<DropdownItem
|
||||
key="add"
|
||||
aria-label={i18n._(t`disassociate`)}
|
||||
isDisabled={isDisabled}
|
||||
component="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
{i18n._(t`Disassociate`)}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<Tooltip content={renderTooltip()} position="top">
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Disassociate`)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{i18n._(t`Disassociate`)}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<AlertModal
|
||||
|
||||
@ -17,6 +17,7 @@ import ContentLoading from '../../../components/ContentLoading';
|
||||
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
||||
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
||||
import InventoryGroupHosts from '../InventoryGroupHosts';
|
||||
import InventoryGroupsRelatedGroup from '../InventoryRelatedGroups';
|
||||
|
||||
import { GroupsAPI } from '../../../api';
|
||||
|
||||
@ -129,6 +130,12 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
|
||||
>
|
||||
<InventoryGroupHosts inventoryGroup={inventoryGroup} />
|
||||
</Route>,
|
||||
<Route
|
||||
key="relatedGroups"
|
||||
path="/inventories/inventory/:id/groups/:groupId/nested_groups"
|
||||
>
|
||||
<InventoryGroupsRelatedGroup />
|
||||
</Route>,
|
||||
]}
|
||||
<Route key="not-found" path="*">
|
||||
<ContentError>
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownPosition,
|
||||
DropdownToggle,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
function AddHostDropdown({ i18n, onAddNew, onAddExisting }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownItems = [
|
||||
<DropdownItem
|
||||
key="add-new"
|
||||
aria-label="add new host"
|
||||
component="button"
|
||||
onClick={onAddNew}
|
||||
>
|
||||
{i18n._(t`Add New Host`)}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="add-existing"
|
||||
aria-label="add existing host"
|
||||
component="button"
|
||||
onClick={onAddExisting}
|
||||
>
|
||||
{i18n._(t`Add Existing Host`)}
|
||||
</DropdownItem>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
position={DropdownPosition.right}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="add-host-dropdown"
|
||||
aria-label="add host"
|
||||
isPrimary
|
||||
onToggle={() => setIsOpen(prevState => !prevState)}
|
||||
>
|
||||
{i18n._(t`Add`)}
|
||||
</DropdownToggle>
|
||||
}
|
||||
dropdownItems={dropdownItems}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AddHostDropdown.propTypes = {
|
||||
onAddNew: func.isRequired,
|
||||
onAddExisting: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(AddHostDropdown);
|
||||
@ -2,14 +2,8 @@ import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
DropdownItem,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { getQSConfig, mergeParams, parseQueryString } from '../../../util/qs';
|
||||
import { GroupsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||
|
||||
import useRequest, {
|
||||
useDeleteItems,
|
||||
@ -22,10 +16,9 @@ import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||
import AssociateModal from '../../../components/AssociateModal';
|
||||
import DisassociateButton from '../../../components/DisassociateButton';
|
||||
import { Kebabified } from '../../../contexts/Kebabified';
|
||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||
import InventoryGroupHostListItem from './InventoryGroupHostListItem';
|
||||
import AddHostDropdown from './AddHostDropdown';
|
||||
import AddDropdown from '../shared/AddDropdown';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
page: 1,
|
||||
@ -35,7 +28,6 @@ const QS_CONFIG = getQSConfig('host', {
|
||||
|
||||
function InventoryGroupHostList({ i18n }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||
const { id: inventoryId, groupId } = useParams();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
@ -47,9 +39,6 @@ function InventoryGroupHostList({ i18n }) {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
credentialTypeId,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -57,16 +46,9 @@ function InventoryGroupHostList({ i18n }) {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [
|
||||
response,
|
||||
actionsResponse,
|
||||
adHocOptions,
|
||||
cred,
|
||||
] = await Promise.all([
|
||||
const [response, actionsResponse] = await Promise.all([
|
||||
GroupsAPI.readAllHosts(groupId, params),
|
||||
InventoriesAPI.readHostsOptions(inventoryId),
|
||||
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -79,9 +61,6 @@ function InventoryGroupHostList({ i18n }) {
|
||||
searchableKeys: Object.keys(
|
||||
actionsResponse.data.actions?.GET || {}
|
||||
).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]),
|
||||
{
|
||||
@ -90,8 +69,6 @@ function InventoryGroupHostList({ i18n }) {
|
||||
actions: {},
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -166,7 +143,26 @@ function InventoryGroupHostList({ i18n }) {
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`;
|
||||
const addButtonOptions = [];
|
||||
|
||||
if (canAdd) {
|
||||
addButtonOptions.push(
|
||||
{
|
||||
onAdd: () => setIsModalOpen(true),
|
||||
title: i18n._(t`Add existing host`),
|
||||
label: i18n._(t`host`),
|
||||
key: 'existing',
|
||||
},
|
||||
{
|
||||
onAdd: () => history.push(addFormUrl),
|
||||
title: i18n._(t`Add new host`),
|
||||
label: i18n._(t`host`),
|
||||
key: 'new',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const addButton = <AddDropdown key="add" dropdownItems={addButtonOptions} />;
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
@ -210,49 +206,11 @@ function InventoryGroupHostList({ i18n }) {
|
||||
}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
? [
|
||||
<AddHostDropdown
|
||||
key="associate"
|
||||
onAddExisting={() => setIsModalOpen(true)}
|
||||
onAddNew={() => history.push(addFormUrl)}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<Kebabified>
|
||||
{({ isKebabified }) =>
|
||||
isKebabified ? (
|
||||
<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>,
|
||||
...(canAdd ? [addButton] : []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
/>,
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={handleDisassociate}
|
||||
@ -277,15 +235,7 @@ function InventoryGroupHostList({ i18n }) {
|
||||
onSelect={() => handleSelect(o)}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd && (
|
||||
<AddHostDropdown
|
||||
key="associate"
|
||||
onAddExisting={() => setIsModalOpen(true)}
|
||||
onAddNew={() => history.push(addFormUrl)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
emptyStateControls={canAdd && addButton}
|
||||
/>
|
||||
{isModalOpen && (
|
||||
<AssociateModal
|
||||
@ -298,16 +248,6 @@ function InventoryGroupHostList({ i18n }) {
|
||||
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 && (
|
||||
<AlertModal
|
||||
isOpen={associateError}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { GroupsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
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 () => {
|
||||
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||
});
|
||||
@ -107,31 +96,8 @@ 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 () => {
|
||||
expect(wrapper.find('AddHostDropdown').length).toBe(1);
|
||||
expect(wrapper.find('AddDropdown').length).toBe(1);
|
||||
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
actions: {
|
||||
@ -143,7 +109,7 @@ describe('<InventoryGroupHostList />', () => {
|
||||
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('AddHostDropdown').length).toBe(0);
|
||||
expect(wrapper.find('AddDropdown').length).toBe(0);
|
||||
});
|
||||
|
||||
test('expected api calls are made for multi-delete', async () => {
|
||||
@ -190,11 +156,11 @@ describe('<InventoryGroupHostList />', () => {
|
||||
|
||||
test('should show associate host modal when adding an existing host', () => {
|
||||
const dropdownToggle = wrapper.find(
|
||||
'DropdownToggle button[aria-label="add host"]'
|
||||
'DropdownToggle button[aria-label="add"]'
|
||||
);
|
||||
dropdownToggle.simulate('click');
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="add existing host"]')
|
||||
.find('DropdownItem[aria-label="Add existing host"]')
|
||||
.simulate('click');
|
||||
expect(wrapper.find('AssociateModal').length).toBe(1);
|
||||
wrapper.find('ModalBoxCloseButton').simulate('click');
|
||||
@ -209,12 +175,10 @@ describe('<InventoryGroupHostList />', () => {
|
||||
results: [{ id: 123, name: 'foo', url: '/api/v2/hosts/123/' }],
|
||||
},
|
||||
});
|
||||
wrapper
|
||||
.find('DropdownToggle button[aria-label="add host"]')
|
||||
.simulate('click');
|
||||
wrapper.find('DropdownToggle button[aria-label="add"]').simulate('click');
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="add existing host"]')
|
||||
.find('DropdownItem[aria-label="Add existing host"]')
|
||||
.simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
@ -241,12 +205,10 @@ describe('<InventoryGroupHostList />', () => {
|
||||
results: [{ id: 123, name: 'foo', url: '/api/v2/hosts/123/' }],
|
||||
},
|
||||
});
|
||||
wrapper
|
||||
.find('DropdownToggle button[aria-label="add host"]')
|
||||
.simulate('click');
|
||||
wrapper.find('DropdownToggle button[aria-label="add"]').simulate('click');
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DropdownItem[aria-label="add existing host"]')
|
||||
.find('DropdownItem[aria-label="Add existing host"]')
|
||||
.simulate('click');
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
@ -288,10 +250,10 @@ describe('<InventoryGroupHostList />', () => {
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const dropdownToggle = wrapper.find(
|
||||
'DropdownToggle button[aria-label="add host"]'
|
||||
'DropdownToggle button[aria-label="add"]'
|
||||
);
|
||||
dropdownToggle.simulate('click');
|
||||
wrapper.find('DropdownItem[aria-label="add new host"]').simulate('click');
|
||||
wrapper.find('DropdownItem[aria-label="Add new host"]').simulate('click');
|
||||
expect(history.location.pathname).toEqual(
|
||||
'/inventories/inventory/1/groups/2/nested_hosts/add'
|
||||
);
|
||||
|
||||
@ -2,17 +2,11 @@ import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
DropdownItem,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import useSelected from '../../../util/useSelected';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { InventoriesAPI, GroupsAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { InventoriesAPI, GroupsAPI } from '../../../api';
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
@ -24,7 +18,6 @@ import InventoryGroupItem from './InventoryGroupItem';
|
||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||
|
||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||
import { Kebabified } from '../../../contexts/Kebabified';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
page: 1,
|
||||
@ -52,7 +45,7 @@ const useModal = () => {
|
||||
function InventoryGroupsList({ i18n }) {
|
||||
const [deletionError, setDeletionError] = useState(null);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
const { isModalOpen, toggleModal } = useModal();
|
||||
const { id: inventoryId } = useParams();
|
||||
@ -64,9 +57,6 @@ function InventoryGroupsList({ i18n }) {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
credentialTypeId,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -74,11 +64,9 @@ function InventoryGroupsList({ i18n }) {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
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.readGroupsOptions(inventoryId),
|
||||
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -91,9 +79,6 @@ function InventoryGroupsList({ i18n }) {
|
||||
searchableKeys: Object.keys(
|
||||
groupOptions.data.actions?.GET || {}
|
||||
).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]),
|
||||
{
|
||||
@ -161,29 +146,6 @@ function InventoryGroupsList({ i18n }) {
|
||||
};
|
||||
const canAdd =
|
||||
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 (
|
||||
<>
|
||||
@ -253,57 +215,24 @@ function InventoryGroupsList({ i18n }) {
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<Kebabified>
|
||||
{({ isKebabified }) => (
|
||||
<>
|
||||
{isKebabified ? (
|
||||
kebabedAdditionalControls()
|
||||
) : (
|
||||
<ToolbarGroup>
|
||||
<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 a selection of multiple groups.`
|
||||
)}
|
||||
position="top"
|
||||
key="adhoc"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Run command`)}
|
||||
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>,
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={groupCount > 0}
|
||||
/>,
|
||||
<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>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@ -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 && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { InventoriesAPI, GroupsAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { InventoriesAPI, GroupsAPI } from '../../../api';
|
||||
import InventoryGroupsList from './InventoryGroupsList';
|
||||
|
||||
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({
|
||||
initialEntries: ['/inventories/inventory/3/groups'],
|
||||
});
|
||||
@ -158,13 +147,6 @@ describe('<InventoryGroupsList />', () => {
|
||||
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', () => {
|
||||
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(() => {
|
||||
jest.clearAllMocks();
|
||||
@ -230,21 +202,8 @@ describe('<InventoryGroupsList/> error handling', () => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route path="/inventories/inventory/:id/groups">
|
||||
<InventoryGroupsList />
|
||||
</Route>,
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
},
|
||||
}
|
||||
);
|
||||
wrapper = mountWithContexts(<InventoryGroupsList />);
|
||||
});
|
||||
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
|
||||
@ -281,27 +240,4 @@ describe('<InventoryGroupsList/> error handling', () => {
|
||||
.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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
DropdownItem,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '../../../util/qs';
|
||||
import useRequest, {
|
||||
useDismissableError,
|
||||
useDeleteItems,
|
||||
} from '../../../util/useRequest';
|
||||
import useSelected from '../../../util/useSelected';
|
||||
import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { HostsAPI, InventoriesAPI } from '../../../api';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import ErrorDetail from '../../../components/ErrorDetail';
|
||||
@ -23,7 +17,6 @@ import PaginatedDataList, {
|
||||
} from '../../../components/PaginatedDataList';
|
||||
import AssociateModal from '../../../components/AssociateModal';
|
||||
import DisassociateButton from '../../../components/DisassociateButton';
|
||||
import { Kebabified } from '../../../contexts/Kebabified';
|
||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||
import InventoryHostGroupItem from './InventoryHostGroupItem';
|
||||
|
||||
@ -35,7 +28,6 @@ const QS_CONFIG = getQSConfig('group', {
|
||||
|
||||
function InventoryHostGroupsList({ i18n }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||
const { hostId, id: invId } = useParams();
|
||||
const { search } = useLocation();
|
||||
|
||||
@ -46,9 +38,6 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
isAdHocDisabled,
|
||||
credentialTypeId,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -62,13 +51,9 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
data: { count, results },
|
||||
},
|
||||
hostGroupOptions,
|
||||
adHocOptions,
|
||||
cred,
|
||||
] = await Promise.all([
|
||||
HostsAPI.readAllGroups(hostId, params),
|
||||
HostsAPI.readGroupsOptions(hostId),
|
||||
InventoriesAPI.readAdHocOptions(invId),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -81,9 +66,6 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
searchableKeys: Object.keys(
|
||||
hostGroupOptions.data.actions?.GET || {}
|
||||
).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
|
||||
{
|
||||
@ -92,8 +74,6 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
actions: {},
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -222,40 +202,10 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<Kebabified>
|
||||
{({ isKebabified }) =>
|
||||
isKebabified ? (
|
||||
<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>,
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
/>,
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={handleDisassociate}
|
||||
@ -288,16 +238,6 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
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 && (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { HostsAPI, InventoriesAPI } from '../../../api';
|
||||
import InventoryHostGroupsList from './InventoryHostGroupsList';
|
||||
|
||||
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({
|
||||
initialEntries: ['/inventories/inventory/1/hosts/3/groups'],
|
||||
});
|
||||
@ -283,11 +272,4 @@ describe('<InventoryHostGroupsList />', () => {
|
||||
wrapper.update();
|
||||
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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
DropdownItem,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import { InventoriesAPI, HostsAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { InventoriesAPI, HostsAPI } from '../../../api';
|
||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||
import AlertModal from '../../../components/AlertModal';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
@ -18,7 +12,6 @@ import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
ToolbarDeleteButton,
|
||||
} from '../../../components/PaginatedDataList';
|
||||
import { Kebabified } from '../../../contexts/Kebabified';
|
||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||
import InventoryHostItem from './InventoryHostItem';
|
||||
|
||||
@ -29,7 +22,6 @@ const QS_CONFIG = getQSConfig('host', {
|
||||
});
|
||||
|
||||
function InventoryHostList({ i18n }) {
|
||||
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const { id } = useParams();
|
||||
const { search } = useLocation();
|
||||
@ -41,9 +33,6 @@ function InventoryHostList({ i18n }) {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
credentialTypeId,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -51,11 +40,9 @@ function InventoryHostList({ i18n }) {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
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.readHostsOptions(id),
|
||||
InventoriesAPI.readAdHocOptions(id),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -68,9 +55,6 @@ function InventoryHostList({ i18n }) {
|
||||
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]),
|
||||
{
|
||||
@ -79,8 +63,6 @@ function InventoryHostList({ i18n }) {
|
||||
actions: {},
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -162,40 +144,10 @@ function InventoryHostList({ i18n }) {
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<Kebabified>
|
||||
{({ isKebabified }) =>
|
||||
isKebabified ? (
|
||||
<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>,
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
/>,
|
||||
<ToolbarDeleteButton
|
||||
key="delete"
|
||||
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 && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { InventoriesAPI, HostsAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { InventoriesAPI, HostsAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
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 () => {
|
||||
wrapper = mountWithContexts(<InventoryHostList />);
|
||||
});
|
||||
@ -276,13 +265,6 @@ describe('<InventoryHostList />', () => {
|
||||
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 () => {
|
||||
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
|
||||
@ -0,0 +1,202 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useParams, useLocation, useHistory } from 'react-router-dom';
|
||||
|
||||
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '../../../util/qs';
|
||||
import useSelected from '../../../util/useSelected';
|
||||
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||
import InventoryGroupRelatedGroupListItem from './InventoryRelatedGroupListItem';
|
||||
import AddDropdown from '../shared/AddDropdown';
|
||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||
import AssociateModal from '../../../components/AssociateModal';
|
||||
import DisassociateButton from '../../../components/DisassociateButton';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
order_by: 'name',
|
||||
});
|
||||
function InventoryRelatedGroupList({ i18n }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { id: inventoryId, groupId } = useParams();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const {
|
||||
request: fetchRelated,
|
||||
result: {
|
||||
groups,
|
||||
itemCount,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
canAdd,
|
||||
},
|
||||
isLoading,
|
||||
error: contentError,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [response, actions] = await Promise.all([
|
||||
GroupsAPI.readChildren(groupId, params),
|
||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
]);
|
||||
|
||||
return {
|
||||
groups: response.data.results,
|
||||
itemCount: response.data.count,
|
||||
relatedSearchableKeys: (
|
||||
actions?.data?.related_search_fields || []
|
||||
).map(val => val.slice(0, -8)),
|
||||
searchableKeys: Object.keys(actions.data.actions?.GET || {}).filter(
|
||||
key => actions.data.actions?.GET[key].filterable
|
||||
),
|
||||
canAdd:
|
||||
actions.data.actions &&
|
||||
Object.prototype.hasOwnProperty.call(actions.data.actions, 'POST'),
|
||||
};
|
||||
}, [groupId, location.search, inventoryId]),
|
||||
{ groups: [], itemCount: 0, canAdd: false }
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchRelated();
|
||||
}, [fetchRelated]);
|
||||
|
||||
const fetchGroupsToAssociate = useCallback(
|
||||
params => {
|
||||
return InventoriesAPI.readGroups(
|
||||
inventoryId,
|
||||
mergeParams(params, { not__id: inventoryId, not__parents: inventoryId })
|
||||
);
|
||||
},
|
||||
[inventoryId]
|
||||
);
|
||||
|
||||
const fetchGroupsOptions = useCallback(
|
||||
() => InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
[inventoryId]
|
||||
);
|
||||
|
||||
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
||||
groups
|
||||
);
|
||||
|
||||
const addFormUrl = `/home`;
|
||||
const addButtonOptions = [];
|
||||
|
||||
if (canAdd) {
|
||||
addButtonOptions.push(
|
||||
{
|
||||
onAdd: () => setIsModalOpen(true),
|
||||
title: i18n._(t`Add existing group`),
|
||||
label: i18n._(t`group`),
|
||||
key: 'existing',
|
||||
},
|
||||
{
|
||||
onAdd: () => history.push(addFormUrl),
|
||||
title: i18n._(t`Add new group`),
|
||||
label: i18n._(t`group`),
|
||||
key: 'new',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const addButton = <AddDropdown key="add" dropdownItems={addButtonOptions} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={i18n._(t`Related Groups`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
onRowClick={handleSelect}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username__icontains',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username__icontains',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
toolbarSearchableKeys={searchableKeys}
|
||||
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
showSelectAll
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={isSelected =>
|
||||
setSelected(isSelected ? [...groups] : [])
|
||||
}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd ? [addButton] : []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
/>,
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={() => {}}
|
||||
itemsToDisassociate={selected}
|
||||
modalTitle={i18n._(t`Disassociate related group(s)?`)}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
renderItem={o => (
|
||||
<InventoryGroupRelatedGroupListItem
|
||||
key={o.id}
|
||||
group={o}
|
||||
detailUrl={`/inventories/inventory/${inventoryId}/groups/${o.id}/details`}
|
||||
editUrl={`/inventories/inventory/${inventoryId}/groups/${o.id}/edit`}
|
||||
isSelected={selected.some(row => row.id === o.id)}
|
||||
onSelect={() => handleSelect(o)}
|
||||
/>
|
||||
)}
|
||||
emptyStateControls={
|
||||
canAdd && (
|
||||
<AddDropdown
|
||||
onAddExisting={() => setIsModalOpen(true)}
|
||||
onAddNew={() => history.push(addFormUrl)}
|
||||
newTitle={i18n._(t`Add new group`)}
|
||||
existingTitle={i18n._(t`Add existing group`)}
|
||||
label={i18n._(t`group`)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isModalOpen && (
|
||||
<AssociateModal
|
||||
header={i18n._(t`Groups`)}
|
||||
fetchRequest={fetchGroupsToAssociate}
|
||||
optionsRequest={fetchGroupsOptions}
|
||||
isModalOpen={isModalOpen}
|
||||
onAssociate={() => {}}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={i18n._(t`Select Groups`)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default withI18n()(InventoryRelatedGroupList);
|
||||
@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { GroupsAPI, InventoriesAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import InventoryRelatedGroupList from './InventoryRelatedGroupList';
|
||||
import mockRelatedGroups from '../shared/data.relatedGroups.json';
|
||||
|
||||
jest.mock('../../../api/models/Groups');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
jest.mock('../../../api/models/CredentialTypes');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
id: 1,
|
||||
groupId: 2,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('<InventoryRelatedGroupList />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
GroupsAPI.readChildren.mockResolvedValue({
|
||||
data: { ...mockRelatedGroups },
|
||||
});
|
||||
InventoriesAPI.readGroupsOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
POST: {},
|
||||
},
|
||||
related_search_fields: [
|
||||
'parents__search',
|
||||
'inventory__search',
|
||||
'inventory_sources__search',
|
||||
'created_by__search',
|
||||
'children__search',
|
||||
'modified_by__search',
|
||||
'hosts__search',
|
||||
],
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryRelatedGroupList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('initially renders successfully ', () => {
|
||||
expect(wrapper.find('InventoryRelatedGroupList').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should fetch inventory group hosts from api and render them in the list', () => {
|
||||
expect(GroupsAPI.readChildren).toHaveBeenCalled();
|
||||
expect(InventoriesAPI.readGroupsOptions).toHaveBeenCalled();
|
||||
expect(wrapper.find('InventoryRelatedGroupListItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-2"]').props().checked
|
||||
).toBe(false);
|
||||
await act(async () => {
|
||||
wrapper.find('DataListCheck[id="select-group-2"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-2"]').props().checked
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper.find('DataListCheck[id="select-group-2"]').invoke('onChange')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('DataListCheck[id="select-group-2"]').props().checked
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should check all row items when select all is checked', async () => {
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(true);
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('DataListCheck').forEach(el => {
|
||||
expect(el.props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
GroupsAPI.readChildren.mockResolvedValueOnce({
|
||||
data: { ...mockRelatedGroups },
|
||||
});
|
||||
InventoriesAPI.readGroupsOptions.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryRelatedGroupList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should show add dropdown button according to permissions', async () => {
|
||||
GroupsAPI.readChildren.mockResolvedValueOnce({
|
||||
data: { ...mockRelatedGroups },
|
||||
});
|
||||
InventoriesAPI.readGroupsOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {},
|
||||
},
|
||||
related_search_fields: [
|
||||
'parents__search',
|
||||
'inventory__search',
|
||||
'inventory_sources__search',
|
||||
'created_by__search',
|
||||
'children__search',
|
||||
'modified_by__search',
|
||||
'hosts__search',
|
||||
],
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryRelatedGroupList />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('AddDropdown').length).toBe(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,90 @@
|
||||
import 'styled-components/macro';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { string, bool, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DataListAction as _DataListAction,
|
||||
DataListCheck,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import DataListCell from '../../../components/DataListCell';
|
||||
|
||||
import { Group } from '../../../types';
|
||||
|
||||
const DataListAction = styled(_DataListAction)`
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-gap: 24px;
|
||||
grid-template-columns: min-content 40px;
|
||||
`;
|
||||
|
||||
function InventoryRelatedGroupListItem({
|
||||
i18n,
|
||||
detailUrl,
|
||||
editUrl,
|
||||
group,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}) {
|
||||
const labelId = `check-action-${group.id}`;
|
||||
|
||||
return (
|
||||
<DataListItem key={group.id} aria-labelledby={labelId} id={`${group.id}`}>
|
||||
<DataListItemRow>
|
||||
<DataListCheck
|
||||
id={`select-group-${group.id}`}
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="name">
|
||||
<Link to={`${detailUrl}`}>
|
||||
<b>{group.name}</b>
|
||||
</Link>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
{group.summary_fields.user_capabilities?.edit && (
|
||||
<Tooltip content={i18n._(t`Edit Group`)} position="top">
|
||||
<Button
|
||||
aria-label={i18n._(t`Edit Group`)}
|
||||
css="grid-column: 2"
|
||||
variant="plain"
|
||||
component={Link}
|
||||
to={`${editUrl}`}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
InventoryRelatedGroupListItem.propTypes = {
|
||||
detailUrl: string.isRequired,
|
||||
editUrl: string.isRequired,
|
||||
group: Group.isRequired,
|
||||
isSelected: bool.isRequired,
|
||||
onSelect: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryRelatedGroupListItem);
|
||||
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import InventoryRelatedGroupListItem from './InventoryRelatedGroupListItem';
|
||||
import mockRelatedGroups from '../shared/data.relatedGroups.json';
|
||||
|
||||
jest.mock('../../../api');
|
||||
|
||||
describe('<InventoryRelatedGroupListItem />', () => {
|
||||
let wrapper;
|
||||
const mockGroup = mockRelatedGroups.results[0];
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryRelatedGroupListItem
|
||||
detailUrl="/group/1"
|
||||
editUrl="/group/1"
|
||||
group={mockGroup}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should display expected row item content', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('DataListCell')
|
||||
.first()
|
||||
.text()
|
||||
).toBe(' Group 2 Inventory 0');
|
||||
});
|
||||
|
||||
test('edit button shown to users with edit capabilities', () => {
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('edit button hidden from users without edit capabilities', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryRelatedGroupListItem
|
||||
detailUrl="/group/1"
|
||||
editUrl="/group/1"
|
||||
group={mockRelatedGroups.results[2]}
|
||||
isSelected={false}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './InventoryRelatedGroupList';
|
||||
@ -1,22 +1,15 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
DropdownItem,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import PaginatedDataList from '../../../components/PaginatedDataList';
|
||||
import SmartInventoryHostListItem from './SmartInventoryHostListItem';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import useSelected from '../../../util/useSelected';
|
||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||
import { InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import { Inventory } from '../../../types';
|
||||
import { Kebabified } from '../../../contexts/Kebabified';
|
||||
import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands';
|
||||
|
||||
const QS_CONFIG = getQSConfig('host', {
|
||||
@ -27,35 +20,27 @@ const QS_CONFIG = getQSConfig('host', {
|
||||
|
||||
function SmartInventoryHostList({ i18n, inventory }) {
|
||||
const location = useLocation();
|
||||
const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false);
|
||||
|
||||
const {
|
||||
result: { hosts, count, moduleOptions, credentialTypeId, isAdHocDisabled },
|
||||
result: { hosts, count },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchHosts,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [hostResponse, adHocOptions, cred] = await Promise.all([
|
||||
InventoriesAPI.readHosts(inventory.id, params),
|
||||
InventoriesAPI.readAdHocOptions(inventory.id),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
const {
|
||||
data: { results, count: hostCount },
|
||||
} = await InventoriesAPI.readHosts(inventory.id, params);
|
||||
|
||||
return {
|
||||
hosts: hostResponse.data.results,
|
||||
count: hostResponse.data.count,
|
||||
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||
credentialTypeId: cred.data.results[0].id,
|
||||
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||
hosts: results,
|
||||
count: hostCount,
|
||||
};
|
||||
}, [location.search, inventory.id]),
|
||||
{
|
||||
hosts: [],
|
||||
count: 0,
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -110,38 +95,10 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
||||
additionalControls={
|
||||
inventory?.summary_fields?.user_capabilities?.adhoc
|
||||
? [
|
||||
<Kebabified>
|
||||
{({ isKebabified }) =>
|
||||
isKebabified ? (
|
||||
<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>,
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={count > 0}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
@ -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 { act } from 'react-dom/test-utils';
|
||||
import { InventoriesAPI, CredentialTypesAPI } from '../../../api';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -27,17 +27,6 @@ describe('<SmartInventoryHostList />', () => {
|
||||
InventoriesAPI.readHosts.mockResolvedValue({
|
||||
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 () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SmartInventoryHostList inventory={clonedInventory} />
|
||||
@ -60,15 +49,6 @@ describe('<SmartInventoryHostList />', () => {
|
||||
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 () => {
|
||||
act(() => {
|
||||
wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
|
||||
@ -97,24 +77,4 @@ describe('<SmartInventoryHostList />', () => {
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
88
awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx
Normal file
88
awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React, { useState, useRef, useEffect, Fragment } from 'react';
|
||||
import { func, string, arrayOf, shape } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownPosition,
|
||||
DropdownToggle,
|
||||
} from '@patternfly/react-core';
|
||||
import { useKebabifiedMenu } from '../../../contexts/Kebabified';
|
||||
|
||||
function AddDropdown({ dropdownItems, i18n }) {
|
||||
const { isKebabified } = useKebabifiedMenu();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const element = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const toggle = e => {
|
||||
if (!isKebabified && (!element || !element.current.contains(e.target))) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', toggle, false);
|
||||
return () => {
|
||||
document.removeEventListener('click', toggle);
|
||||
};
|
||||
}, [isKebabified]);
|
||||
|
||||
if (isKebabified) {
|
||||
return (
|
||||
<Fragment>
|
||||
{dropdownItems.map(item => (
|
||||
<DropdownItem
|
||||
key={item.key}
|
||||
aria-label={item.title}
|
||||
onClick={item.onAdd}
|
||||
>
|
||||
{item.title}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={element} key="add">
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
position={DropdownPosition.left}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="add"
|
||||
aria-label="add"
|
||||
isPrimary
|
||||
onToggle={() => setIsOpen(prevState => !prevState)}
|
||||
>
|
||||
{i18n._(t`Add`)}
|
||||
</DropdownToggle>
|
||||
}
|
||||
dropdownItems={dropdownItems.map(item => (
|
||||
<DropdownItem
|
||||
className="pf-c-dropdown__menu-item"
|
||||
key={item.key}
|
||||
aria-label={item.title}
|
||||
onClick={item.onAdd}
|
||||
>
|
||||
{item.title}
|
||||
</DropdownItem>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AddDropdown.propTypes = {
|
||||
dropdownItems: arrayOf(
|
||||
shape({
|
||||
label: string.isRequired,
|
||||
onAdd: func.isRequired,
|
||||
key: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export { AddDropdown as _AddDropdown };
|
||||
export default withI18n()(AddDropdown);
|
||||
@ -1,17 +1,27 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import AddHostDropdown from './AddHostDropdown';
|
||||
import AddDropdown from './AddDropdown';
|
||||
|
||||
describe('<AddHostDropdown />', () => {
|
||||
describe('<AddDropdown />', () => {
|
||||
let wrapper;
|
||||
let dropdownToggle;
|
||||
const onAddNew = jest.fn();
|
||||
const onAddExisting = jest.fn();
|
||||
const dropdownItems = [
|
||||
{
|
||||
onAdd: () => {},
|
||||
title: 'Add existing group',
|
||||
label: 'group',
|
||||
key: 'existing',
|
||||
},
|
||||
{
|
||||
onAdd: () => {},
|
||||
title: 'Add new group',
|
||||
label: 'group',
|
||||
key: 'new',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<AddHostDropdown onAddNew={onAddNew} onAddExisting={onAddExisting} />
|
||||
);
|
||||
wrapper = mountWithContexts(<AddDropdown dropdownItems={dropdownItems} />);
|
||||
dropdownToggle = wrapper.find('DropdownToggle button');
|
||||
});
|
||||
|
||||
181
awx/ui_next/src/screens/Inventory/shared/data.relatedGroups.json
Normal file
181
awx/ui_next/src/screens/Inventory/shared/data.relatedGroups.json
Normal file
@ -0,0 +1,181 @@
|
||||
{
|
||||
"count": 3,
|
||||
"results": [{
|
||||
"id": 2,
|
||||
"type": "group",
|
||||
"url": "/api/v2/groups/2/",
|
||||
"related": {
|
||||
"created_by": "/api/v2/users/10/",
|
||||
"modified_by": "/api/v2/users/14/",
|
||||
"variable_data": "/api/v2/groups/2/variable_data/",
|
||||
"hosts": "/api/v2/groups/2/hosts/",
|
||||
"potential_children": "/api/v2/groups/2/potential_children/",
|
||||
"children": "/api/v2/groups/2/children/",
|
||||
"all_hosts": "/api/v2/groups/2/all_hosts/",
|
||||
"job_events": "/api/v2/groups/2/job_events/",
|
||||
"job_host_summaries": "/api/v2/groups/2/job_host_summaries/",
|
||||
"activity_stream": "/api/v2/groups/2/activity_stream/",
|
||||
"inventory_sources": "/api/v2/groups/2/inventory_sources/",
|
||||
"ad_hoc_commands": "/api/v2/groups/2/ad_hoc_commands/",
|
||||
"inventory": "/api/v2/inventories/1/"
|
||||
},
|
||||
"summary_fields": {
|
||||
"inventory": {
|
||||
"id": 1,
|
||||
"name": " Inventory 1 Org 0",
|
||||
"description": "",
|
||||
"has_active_failures": false,
|
||||
"total_hosts": 33,
|
||||
"hosts_with_active_failures": 0,
|
||||
"total_groups": 4,
|
||||
"has_inventory_sources": false,
|
||||
"total_inventory_sources": 0,
|
||||
"inventory_sources_with_failures": 0,
|
||||
"organization_id": 1,
|
||||
"kind": ""
|
||||
},
|
||||
"created_by": {
|
||||
"id": 10,
|
||||
"username": "user-4",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"modified_by": {
|
||||
"id": 14,
|
||||
"username": "user-8",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"user_capabilities": {
|
||||
"edit": true,
|
||||
"delete": true,
|
||||
"copy": true
|
||||
}
|
||||
},
|
||||
"created": "2020-09-23T14:30:55.263148Z",
|
||||
"modified": "2020-09-23T14:30:55.263175Z",
|
||||
"name": " Group 2 Inventory 0",
|
||||
"description": "",
|
||||
"inventory": 1,
|
||||
"variables": ""
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "group",
|
||||
"url": "/api/v2/groups/3/",
|
||||
"related": {
|
||||
"created_by": "/api/v2/users/11/",
|
||||
"modified_by": "/api/v2/users/15/",
|
||||
"variable_data": "/api/v2/groups/3/variable_data/",
|
||||
"hosts": "/api/v2/groups/3/hosts/",
|
||||
"potential_children": "/api/v2/groups/3/potential_children/",
|
||||
"children": "/api/v2/groups/3/children/",
|
||||
"all_hosts": "/api/v2/groups/3/all_hosts/",
|
||||
"job_events": "/api/v2/groups/3/job_events/",
|
||||
"job_host_summaries": "/api/v2/groups/3/job_host_summaries/",
|
||||
"activity_stream": "/api/v2/groups/3/activity_stream/",
|
||||
"inventory_sources": "/api/v2/groups/3/inventory_sources/",
|
||||
"ad_hoc_commands": "/api/v2/groups/3/ad_hoc_commands/",
|
||||
"inventory": "/api/v2/inventories/1/"
|
||||
},
|
||||
"summary_fields": {
|
||||
"inventory": {
|
||||
"id": 1,
|
||||
"name": " Inventory 1 Org 0",
|
||||
"description": "",
|
||||
"has_active_failures": false,
|
||||
"total_hosts": 33,
|
||||
"hosts_with_active_failures": 0,
|
||||
"total_groups": 4,
|
||||
"has_inventory_sources": false,
|
||||
"total_inventory_sources": 0,
|
||||
"inventory_sources_with_failures": 0,
|
||||
"organization_id": 1,
|
||||
"kind": ""
|
||||
},
|
||||
"created_by": {
|
||||
"id": 11,
|
||||
"username": "user-5",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"modified_by": {
|
||||
"id": 15,
|
||||
"username": "user-9",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"user_capabilities": {
|
||||
"edit": true,
|
||||
"delete": true,
|
||||
"copy": true
|
||||
}
|
||||
},
|
||||
"created": "2020-09-23T14:30:55.281583Z",
|
||||
"modified": "2020-09-23T14:30:55.281615Z",
|
||||
"name": " Group 3 Inventory 0",
|
||||
"description": "",
|
||||
"inventory": 1,
|
||||
"variables": ""
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "group",
|
||||
"url": "/api/v2/groups/4/",
|
||||
"related": {
|
||||
"created_by": "/api/v2/users/12/",
|
||||
"modified_by": "/api/v2/users/16/",
|
||||
"variable_data": "/api/v2/groups/4/variable_data/",
|
||||
"hosts": "/api/v2/groups/4/hosts/",
|
||||
"potential_children": "/api/v2/groups/4/potential_children/",
|
||||
"children": "/api/v2/groups/4/children/",
|
||||
"all_hosts": "/api/v2/groups/4/all_hosts/",
|
||||
"job_events": "/api/v2/groups/4/job_events/",
|
||||
"job_host_summaries": "/api/v2/groups/4/job_host_summaries/",
|
||||
"activity_stream": "/api/v2/groups/4/activity_stream/",
|
||||
"inventory_sources": "/api/v2/groups/4/inventory_sources/",
|
||||
"ad_hoc_commands": "/api/v2/groups/4/ad_hoc_commands/",
|
||||
"inventory": "/api/v2/inventories/1/"
|
||||
},
|
||||
"summary_fields": {
|
||||
"inventory": {
|
||||
"id": 1,
|
||||
"name": " Inventory 1 Org 0",
|
||||
"description": "",
|
||||
"has_active_failures": false,
|
||||
"total_hosts": 33,
|
||||
"hosts_with_active_failures": 0,
|
||||
"total_groups": 4,
|
||||
"has_inventory_sources": false,
|
||||
"total_inventory_sources": 0,
|
||||
"inventory_sources_with_failures": 0,
|
||||
"organization_id": 1,
|
||||
"kind": ""
|
||||
},
|
||||
"created_by": {
|
||||
"id": 12,
|
||||
"username": "user-6",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"modified_by": {
|
||||
"id": 16,
|
||||
"username": "user-10",
|
||||
"first_name": "",
|
||||
"last_name": ""
|
||||
},
|
||||
"user_capabilities": {
|
||||
"edit": false,
|
||||
"delete": true,
|
||||
"copy": true
|
||||
}
|
||||
},
|
||||
"created": "2020-09-23T14:30:55.293574Z",
|
||||
"modified": "2020-09-23T14:30:55.293603Z",
|
||||
"name": " Group 4 Inventory 0",
|
||||
"description": "",
|
||||
"inventory": 1,
|
||||
"variables": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user