mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 07:26:03 -03:30
Merge pull request #10004 from AlexSCorey/9864-AddEEtoAdHocWizard
Adds an execution environment step to the ad hoc commands SUMMARY This addresses some of #9864 by adding a step to select an execution environment to the ad hoc commands wizard ISSUE TYPE Bugfix Pull Request COMPONENT NAME UI AWX VERSION ADDITIONAL INFORMATION Reviewed-by: Kersom <None> Reviewed-by: Tiago Góes <tiago.goes2009@gmail.com>
This commit is contained in:
@@ -12,10 +12,9 @@ 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 { KebabifiedContext } from '../../contexts/Kebabified';
|
||||||
import ContentLoading from '../ContentLoading';
|
|
||||||
import ContentError from '../ContentError';
|
import ContentError from '../ContentError';
|
||||||
|
|
||||||
function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
function AdHocCommands({ adHocItems, i18n, hasListItems, onLaunchLoading }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
@@ -36,22 +35,29 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
|||||||
}, [isKebabified, isWizardOpen, onKebabModalChange]);
|
}, [isKebabified, isWizardOpen, onKebabModalChange]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { moduleOptions, credentialTypeId, isAdHocDisabled },
|
result: {
|
||||||
|
moduleOptions,
|
||||||
|
credentialTypeId,
|
||||||
|
isAdHocDisabled,
|
||||||
|
organizationId,
|
||||||
|
},
|
||||||
request: fetchData,
|
request: fetchData,
|
||||||
error: fetchError,
|
error: fetchError,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [options, cred] = await Promise.all([
|
const [options, { data }, cred] = await Promise.all([
|
||||||
InventoriesAPI.readAdHocOptions(id),
|
InventoriesAPI.readAdHocOptions(id),
|
||||||
|
InventoriesAPI.readDetail(id),
|
||||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
moduleOptions: options.data.actions.GET.module_name.choices,
|
moduleOptions: options.data.actions.GET.module_name.choices,
|
||||||
credentialTypeId: cred.data.results[0].id,
|
credentialTypeId: cred.data.results[0].id,
|
||||||
isAdHocDisabled: !options.data.actions.POST,
|
isAdHocDisabled: !options.data.actions.POST,
|
||||||
|
organizationId: data.organization,
|
||||||
};
|
};
|
||||||
}, [id]),
|
}, [id]),
|
||||||
{ moduleOptions: [], isAdHocDisabled: true }
|
{ moduleOptions: [], isAdHocDisabled: true, organizationId: null }
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -76,19 +82,20 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
const { credential, ...remainingValues } = values;
|
const { credential, execution_environment, ...remainingValues } = values;
|
||||||
const newCredential = credential[0].id;
|
const newCredential = credential[0].id;
|
||||||
|
|
||||||
const manipulatedValues = {
|
const manipulatedValues = {
|
||||||
credential: newCredential,
|
credential: newCredential,
|
||||||
|
execution_environment: execution_environment[0]?.id,
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
};
|
};
|
||||||
await launchAdHocCommands(manipulatedValues);
|
await launchAdHocCommands(manipulatedValues);
|
||||||
};
|
};
|
||||||
|
useEffect(() => onLaunchLoading(isLaunchLoading), [
|
||||||
if (isLaunchLoading) {
|
isLaunchLoading,
|
||||||
return <ContentLoading />;
|
onLaunchLoading,
|
||||||
}
|
]);
|
||||||
|
|
||||||
if (error && isWizardOpen) {
|
if (error && isWizardOpen) {
|
||||||
return (
|
return (
|
||||||
@@ -141,6 +148,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
|||||||
{isWizardOpen && (
|
{isWizardOpen && (
|
||||||
<AdHocCommandsWizard
|
<AdHocCommandsWizard
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
|
organizationId={organizationId}
|
||||||
moduleOptions={moduleOptions}
|
moduleOptions={moduleOptions}
|
||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
credentialTypeId={credentialTypeId}
|
credentialTypeId={credentialTypeId}
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import { CredentialTypesAPI, InventoriesAPI, CredentialsAPI } from '../../api';
|
import {
|
||||||
|
CredentialTypesAPI,
|
||||||
|
InventoriesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
|
ExecutionEnvironmentsAPI,
|
||||||
|
} from '../../api';
|
||||||
import AdHocCommands from './AdHocCommands';
|
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('../../api/models/ExecutionEnvironments');
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useParams: () => ({
|
useParams: () => ({
|
||||||
@@ -51,6 +58,15 @@ describe('<AdHocCommands />', () => {
|
|||||||
CredentialTypesAPI.read.mockResolvedValue({
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
||||||
});
|
});
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||||
|
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
let wrapper;
|
let wrapper;
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -61,7 +77,11 @@ describe('<AdHocCommands />', () => {
|
|||||||
test('mounts successfully', async () => {
|
test('mounts successfully', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||||
@@ -83,12 +103,26 @@ describe('<AdHocCommands />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } });
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
data: { results: [{ id: 1 }] },
|
data: { results: [{ id: 1 }] },
|
||||||
});
|
});
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||||
|
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await act(async () =>
|
await act(async () =>
|
||||||
@@ -102,15 +136,35 @@ describe('<AdHocCommands />', () => {
|
|||||||
|
|
||||||
test('should submit properly', async () => {
|
test('should submit properly', async () => {
|
||||||
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||||
|
InventoriesAPI.readDetail.mockResolvedValue({
|
||||||
|
data: { organization: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
results: credentials,
|
results: credentials,
|
||||||
count: 5,
|
count: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||||
|
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {} } },
|
||||||
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,8 +201,27 @@ describe('<AdHocCommands />', () => {
|
|||||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
);
|
);
|
||||||
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
|
||||||
// second step of wizard
|
// second step of wizard
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('input[aria-labelledby="check-action-item-2"]')
|
||||||
|
.simulate('change', { target: { checked: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('CheckboxListItem[label="EE2"]').prop('isSelected')
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
// third step of wizard
|
||||||
|
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('input[aria-labelledby="check-action-item-4"]')
|
.find('input[aria-labelledby="check-action-item-4"]')
|
||||||
@@ -176,6 +249,7 @@ describe('<AdHocCommands />', () => {
|
|||||||
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
||||||
module_name: 'command',
|
module_name: 'command',
|
||||||
verbosity: 1,
|
verbosity: 1,
|
||||||
|
execution_environment: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,13 +276,24 @@ describe('<AdHocCommands />', () => {
|
|||||||
['foo', 'foo'],
|
['foo', 'foo'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
verbosity: { choices: [[1], [2]] },
|
verbosity: {
|
||||||
|
choices: [[1], [2]],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
InventoriesAPI.readDetail.mockResolvedValue({
|
||||||
|
data: { organization: 1 },
|
||||||
|
});
|
||||||
CredentialTypesAPI.read.mockResolvedValue({
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
data: { results: [{ id: 1 }] },
|
data: {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -216,9 +301,33 @@ describe('<AdHocCommands />', () => {
|
|||||||
count: 5,
|
count: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'EE1 1',
|
||||||
|
url: 'wwww.google.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'EE2',
|
||||||
|
url: 'wwww.google.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {} } },
|
||||||
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,7 +349,10 @@ describe('<AdHocCommands />', () => {
|
|||||||
'command'
|
'command'
|
||||||
);
|
);
|
||||||
wrapper.find('input#module_args').simulate('change', {
|
wrapper.find('input#module_args').simulate('change', {
|
||||||
target: { value: 'foo', name: 'module_args' },
|
target: {
|
||||||
|
value: 'foo',
|
||||||
|
name: 'module_args',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||||
});
|
});
|
||||||
@@ -259,10 +371,36 @@ describe('<AdHocCommands />', () => {
|
|||||||
|
|
||||||
// second step of wizard
|
// second step of wizard
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('input[aria-labelledby="check-action-item-2"]')
|
||||||
|
.simulate('change', {
|
||||||
|
target: {
|
||||||
|
checked: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('CheckboxListItem[label="EE2"]').prop('isSelected')
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
// third step of wizard
|
||||||
|
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('input[aria-labelledby="check-action-item-4"]')
|
.find('input[aria-labelledby="check-action-item-4"]')
|
||||||
.simulate('change', { target: { checked: true } });
|
.simulate('change', {
|
||||||
|
target: {
|
||||||
|
checked: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -291,7 +429,11 @@ describe('<AdHocCommands />', () => {
|
|||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
@@ -312,7 +454,11 @@ describe('<AdHocCommands />', () => {
|
|||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems={false} />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems={false}
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
@@ -335,7 +481,11 @@ describe('<AdHocCommands />', () => {
|
|||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
<AdHocCommands
|
||||||
|
adHocItems={adHocItems}
|
||||||
|
hasListItems
|
||||||
|
onLaunchLoading={() => jest.fn()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await act(async () => wrapper.find('button').prop('onClick')());
|
await act(async () => wrapper.find('button').prop('onClick')());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
||||||
import { Tooltip } from '@patternfly/react-core';
|
import { Tooltip } from '@patternfly/react-core';
|
||||||
@@ -10,6 +10,7 @@ import styled from 'styled-components';
|
|||||||
import Wizard from '../Wizard';
|
import Wizard from '../Wizard';
|
||||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||||
import AdHocDetailsStep from './AdHocDetailsStep';
|
import AdHocDetailsStep from './AdHocDetailsStep';
|
||||||
|
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
|
||||||
|
|
||||||
const AlertText = styled.div`
|
const AlertText = styled.div`
|
||||||
color: var(--pf-global--danger-color--200);
|
color: var(--pf-global--danger-color--200);
|
||||||
@@ -23,11 +24,11 @@ const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
|||||||
|
|
||||||
function AdHocCommandsWizard({
|
function AdHocCommandsWizard({
|
||||||
onLaunch,
|
onLaunch,
|
||||||
i18n,
|
|
||||||
moduleOptions,
|
moduleOptions,
|
||||||
verbosityOptions,
|
verbosityOptions,
|
||||||
onCloseWizard,
|
onCloseWizard,
|
||||||
credentialTypeId,
|
credentialTypeId,
|
||||||
|
organizationId,
|
||||||
}) {
|
}) {
|
||||||
const [currentStepId, setCurrentStepId] = useState(1);
|
const [currentStepId, setCurrentStepId] = useState(1);
|
||||||
const [enableLaunch, setEnableLaunch] = useState(false);
|
const [enableLaunch, setEnableLaunch] = useState(false);
|
||||||
@@ -57,17 +58,17 @@ function AdHocCommandsWizard({
|
|||||||
key: 1,
|
key: 1,
|
||||||
name: hasDetailsStepError ? (
|
name: hasDetailsStepError ? (
|
||||||
<AlertText>
|
<AlertText>
|
||||||
{i18n._(t`Details`)}
|
{t`Details`}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="right"
|
position="right"
|
||||||
content={i18n._(t`This step contains errors`)}
|
content={t`This step contains errors`}
|
||||||
trigger="click mouseenter focus"
|
trigger="click mouseenter focus"
|
||||||
>
|
>
|
||||||
<ExclamationCircleIcon />
|
<ExclamationCircleIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</AlertText>
|
</AlertText>
|
||||||
) : (
|
) : (
|
||||||
i18n._(t`Details`)
|
t`Details`
|
||||||
),
|
),
|
||||||
component: (
|
component: (
|
||||||
<AdHocDetailsStep
|
<AdHocDetailsStep
|
||||||
@@ -76,12 +77,25 @@ function AdHocCommandsWizard({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: enabledNextOnDetailsStep(),
|
enableNext: enabledNextOnDetailsStep(),
|
||||||
nextButtonText: i18n._(t`Next`),
|
nextButtonText: t`Next`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
key: 2,
|
key: 2,
|
||||||
name: i18n._(t`Machine credential`),
|
name: t`Execution Environment`,
|
||||||
|
component: (
|
||||||
|
<AdHocExecutionEnvironmentStep organizationId={organizationId} />
|
||||||
|
),
|
||||||
|
// Removed this line when https://github.com/patternfly/patternfly-react/issues/5729 is fixed
|
||||||
|
stepNavItemProps: { style: { 'white-space': 'nowrap' } },
|
||||||
|
enableNext: true,
|
||||||
|
nextButtonText: t`Next`,
|
||||||
|
canJumpTo: currentStepId >= 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
key: 3,
|
||||||
|
name: t`Machine credential`,
|
||||||
component: (
|
component: (
|
||||||
<AdHocCredentialStep
|
<AdHocCredentialStep
|
||||||
credentialTypeId={credentialTypeId}
|
credentialTypeId={credentialTypeId}
|
||||||
@@ -89,7 +103,7 @@ function AdHocCommandsWizard({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: enableLaunch && Object.values(errors).length === 0,
|
enableNext: enableLaunch && Object.values(errors).length === 0,
|
||||||
nextButtonText: i18n._(t`Launch`),
|
nextButtonText: t`Launch`,
|
||||||
canJumpTo: currentStepId >= 2,
|
canJumpTo: currentStepId >= 2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -106,10 +120,10 @@ function AdHocCommandsWizard({
|
|||||||
onLaunch(values);
|
onLaunch(values);
|
||||||
}}
|
}}
|
||||||
steps={steps}
|
steps={steps}
|
||||||
title={i18n._(t`Run command`)}
|
title={t`Run command`}
|
||||||
nextButtonText={currentStep.nextButtonText || undefined}
|
nextButtonText={currentStep.nextButtonText || undefined}
|
||||||
backButtonText={i18n._(t`Back`)}
|
backButtonText={t`Back`}
|
||||||
cancelButtonText={i18n._(t`Cancel`)}
|
cancelButtonText={t`Cancel`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -128,6 +142,7 @@ const FormikApp = withFormik({
|
|||||||
module_name: '',
|
module_name: '',
|
||||||
extra_vars: '---',
|
extra_vars: '---',
|
||||||
job_type: 'run',
|
job_type: 'run',
|
||||||
|
execution_environment: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})(AdHocCommandsWizard);
|
})(AdHocCommandsWizard);
|
||||||
@@ -139,4 +154,4 @@ FormikApp.propTypes = {
|
|||||||
onCloseWizard: PropTypes.func.isRequired,
|
onCloseWizard: PropTypes.func.isRequired,
|
||||||
credentialTypeId: PropTypes.number.isRequired,
|
credentialTypeId: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
export default withI18n()(FormikApp);
|
export default FormikApp;
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import { CredentialsAPI } from '../../api';
|
import { CredentialsAPI, ExecutionEnvironmentsAPI } from '../../api';
|
||||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||||
|
|
||||||
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('../../api/models/ExecutionEnvironments');
|
||||||
|
|
||||||
const verbosityOptions = [
|
const verbosityOptions = [
|
||||||
{ value: '0', key: '0', label: '0 (Normal)' },
|
{ value: '0', key: '0', label: '0 (Normal)' },
|
||||||
{ value: '1', key: '1', label: '1 (Verbose)' },
|
{ value: '1', key: '1', label: '1 (Verbose)' },
|
||||||
@@ -39,6 +41,7 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
onCloseWizard={() => {}}
|
onCloseWizard={() => {}}
|
||||||
credentialTypeId={1}
|
credentialTypeId={1}
|
||||||
|
organizationId={1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -97,6 +100,18 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
});
|
});
|
||||||
test('launch button should become active', async () => {
|
test('launch button should become active', async () => {
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'EE 1', url: '' },
|
||||||
|
{ id: 2, name: 'EE 2', url: '' },
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {} } },
|
||||||
|
});
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
results: [
|
results: [
|
||||||
@@ -127,10 +142,40 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
|
// step 2
|
||||||
|
|
||||||
|
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||||
|
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||||
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper
|
||||||
|
.find('input[aria-labelledby="check-action-item-1"]')
|
||||||
|
.simulate('change', { target: { checked: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('CheckboxListItem[label="EE 1"]').prop('isSelected')
|
||||||
|
).toBe(true);
|
||||||
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
// step 3
|
||||||
|
|
||||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('input[aria-labelledby="check-action-item-1"]')
|
.find('input[aria-labelledby="check-action-item-1"]')
|
||||||
@@ -150,8 +195,21 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(onLaunch).toHaveBeenCalled();
|
expect(onLaunch).toHaveBeenCalledWith({
|
||||||
|
become_enabled: '',
|
||||||
|
credential: [{ id: 1, name: 'Cred 1', url: '' }],
|
||||||
|
diff_mode: false,
|
||||||
|
execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
|
||||||
|
extra_vars: '---',
|
||||||
|
forks: 0,
|
||||||
|
job_type: 'run',
|
||||||
|
limit: 'Inventory 1, Inventory 2, inventory 3',
|
||||||
|
module_args: 'foo',
|
||||||
|
module_name: 'command',
|
||||||
|
verbosity: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show error in navigation bar', async () => {
|
test('should show error in navigation bar', async () => {
|
||||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||||
|
|
||||||
@@ -201,6 +259,12 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { ExecutionEnvironmentsAPI } from '../../api';
|
||||||
|
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
|
||||||
|
|
||||||
|
jest.mock('../../api/models/ExecutionEnvironments');
|
||||||
|
|
||||||
|
describe('<AdHocExecutionEnvironmentStep />', () => {
|
||||||
|
let wrapper;
|
||||||
|
beforeEach(async () => {
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||||
|
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { actions: { GET: {} } },
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik>
|
||||||
|
<AdHocExecutionEnvironmentStep organizationId={1} />
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should mount properly', async () => {
|
||||||
|
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api', async () => {
|
||||||
|
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||||
|
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalled();
|
||||||
|
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import React, { useEffect, useCallback } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useField } from 'formik';
|
||||||
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
|
import { ExecutionEnvironmentsAPI } from '../../api';
|
||||||
|
import Popover from '../Popover';
|
||||||
|
|
||||||
|
import { parseQueryString, getQSConfig, mergeParams } from '../../util/qs';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
import ContentError from '../ContentError';
|
||||||
|
import ContentLoading from '../ContentLoading';
|
||||||
|
import OptionsList from '../OptionsList';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('execution_environments', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
function AdHocExecutionEnvironmentStep({ organizationId }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const [executionEnvironmentField, , executionEnvironmentHelpers] = useField(
|
||||||
|
'execution_environment'
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
request: fetchExecutionEnvironments,
|
||||||
|
result: {
|
||||||
|
executionEnvironments,
|
||||||
|
executionEnvironmentsCount,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
},
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
const globallyAvailableParams = { or__organization__isnull: 'True' };
|
||||||
|
const organizationIdParams = organizationId
|
||||||
|
? { or__organization__id: organizationId }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
data: { results, count },
|
||||||
|
},
|
||||||
|
actionsResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
ExecutionEnvironmentsAPI.read(
|
||||||
|
mergeParams(params, {
|
||||||
|
...globallyAvailableParams,
|
||||||
|
...organizationIdParams,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
ExecutionEnvironmentsAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
executionEnvironments: results,
|
||||||
|
executionEnvironmentsCount: count,
|
||||||
|
relatedSearchableKeys: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
searchableKeys: Object.keys(
|
||||||
|
actionsResponse.data.actions?.GET || {}
|
||||||
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
|
};
|
||||||
|
}, [history.location.search, organizationId]),
|
||||||
|
{
|
||||||
|
executionEnvironments: [],
|
||||||
|
executionEnvironmentsCount: 0,
|
||||||
|
relatedSearchableKeys: [],
|
||||||
|
searchableKeys: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchExecutionEnvironments();
|
||||||
|
}, [fetchExecutionEnvironments]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ContentError error={error} />;
|
||||||
|
}
|
||||||
|
if (isLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<FormGroup
|
||||||
|
fieldId="execution_enviroment"
|
||||||
|
label={t`Execution Environments`}
|
||||||
|
aria-label={t`Execution Environments`}
|
||||||
|
labelIcon={
|
||||||
|
<Popover
|
||||||
|
content={t`Select the Execution Environment you want this command to run inside.`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<OptionsList
|
||||||
|
isLoading={isLoading}
|
||||||
|
value={executionEnvironmentField.value || []}
|
||||||
|
options={executionEnvironments}
|
||||||
|
optionCount={executionEnvironmentsCount}
|
||||||
|
header={t`Execution Environments`}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
searchColumns={[
|
||||||
|
{
|
||||||
|
name: t`Name`,
|
||||||
|
key: 'name__icontains',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`Created By (Username)`,
|
||||||
|
key: 'created_by__username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t`Modified By (Username)`,
|
||||||
|
key: 'modified_by__username',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sortColumns={[
|
||||||
|
{
|
||||||
|
name: t`Name`,
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
name="execution_environment"
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
|
selectItem={value => {
|
||||||
|
executionEnvironmentHelpers.setValue([value]);
|
||||||
|
}}
|
||||||
|
deselectItem={() => {
|
||||||
|
executionEnvironmentHelpers.setValue([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default AdHocExecutionEnvironmentStep;
|
||||||
@@ -28,6 +28,7 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryGroupHostList({ i18n }) {
|
function InventoryGroupHostList({ i18n }) {
|
||||||
|
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const { id: inventoryId, groupId } = useParams();
|
const { id: inventoryId, groupId } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -172,7 +173,9 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading || isDisassociateLoading}
|
hasContentLoading={
|
||||||
|
isLoading || isDisassociateLoading || isAdHocLaunchLoading
|
||||||
|
}
|
||||||
items={hosts}
|
items={hosts}
|
||||||
itemCount={hostCount}
|
itemCount={hostCount}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
@@ -215,6 +218,7 @@ function InventoryGroupHostList({ i18n }) {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
adHocItems={selected}
|
adHocItems={selected}
|
||||||
hasListItems={hostCount > 0}
|
hasListItems={hostCount > 0}
|
||||||
|
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||||
/>,
|
/>,
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } 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';
|
||||||
@@ -30,6 +30,7 @@ function cannotDelete(item) {
|
|||||||
function InventoryGroupsList({ i18n }) {
|
function InventoryGroupsList({ i18n }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { id: inventoryId } = useParams();
|
const { id: inventoryId } = useParams();
|
||||||
|
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
@@ -107,7 +108,7 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading || isAdHocLaunchLoading}
|
||||||
items={groups}
|
items={groups}
|
||||||
itemCount={groupCount}
|
itemCount={groupCount}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
@@ -174,6 +175,7 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
adHocItems={selected}
|
adHocItems={selected}
|
||||||
hasListItems={groupCount > 0}
|
hasListItems={groupCount > 0}
|
||||||
|
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||||
/>,
|
/>,
|
||||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||||
<InventoryGroupsDeleteModal
|
<InventoryGroupsDeleteModal
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const QS_CONFIG = getQSConfig('group', {
|
|||||||
|
|
||||||
function InventoryHostGroupsList({ i18n }) {
|
function InventoryHostGroupsList({ i18n }) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||||
const { hostId, id: invId } = useParams();
|
const { hostId, id: invId } = useParams();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
@@ -147,7 +148,9 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading || isDisassociateLoading}
|
hasContentLoading={
|
||||||
|
isLoading || isDisassociateLoading || isAdHocLaunchLoading
|
||||||
|
}
|
||||||
items={groups}
|
items={groups}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
@@ -205,6 +208,7 @@ function InventoryHostGroupsList({ i18n }) {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
adHocItems={selected}
|
adHocItems={selected}
|
||||||
hasListItems={itemCount > 0}
|
hasListItems={itemCount > 0}
|
||||||
|
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||||
/>,
|
/>,
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
|
|
||||||
function InventoryHostList({ i18n }) {
|
function InventoryHostList({ i18n }) {
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ function InventoryHostList({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading || isDeleteLoading}
|
hasContentLoading={isLoading || isDeleteLoading || isAdHocLaunchLoading}
|
||||||
items={hosts}
|
items={hosts}
|
||||||
itemCount={hostCount}
|
itemCount={hostCount}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
@@ -152,6 +153,7 @@ function InventoryHostList({ i18n }) {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
adHocItems={selected}
|
adHocItems={selected}
|
||||||
hasListItems={hostCount > 0}
|
hasListItems={hostCount > 0}
|
||||||
|
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||||
/>,
|
/>,
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton
|
||||||
key="delete"
|
key="delete"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const QS_CONFIG = getQSConfig('group', {
|
|||||||
});
|
});
|
||||||
function InventoryRelatedGroupList({ i18n }) {
|
function InventoryRelatedGroupList({ i18n }) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||||
const [associateError, setAssociateError] = useState(null);
|
const [associateError, setAssociateError] = useState(null);
|
||||||
const [disassociateError, setDisassociateError] = useState(null);
|
const [disassociateError, setDisassociateError] = useState(null);
|
||||||
const { id: inventoryId, groupId } = useParams();
|
const { id: inventoryId, groupId } = useParams();
|
||||||
@@ -154,7 +155,7 @@ function InventoryRelatedGroupList({ i18n }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading || isAdHocLaunchLoading}
|
||||||
items={groups}
|
items={groups}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
pluralizedItemName={i18n._(t`Related Groups`)}
|
pluralizedItemName={i18n._(t`Related Groups`)}
|
||||||
@@ -197,6 +198,7 @@ function InventoryRelatedGroupList({ i18n }) {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
adHocItems={selected}
|
adHocItems={selected}
|
||||||
hasListItems={itemCount > 0}
|
hasListItems={itemCount > 0}
|
||||||
|
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||||
/>,
|
/>,
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -20,7 +20,7 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
|
|
||||||
function SmartInventoryHostList({ i18n, inventory }) {
|
function SmartInventoryHostList({ i18n, inventory }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||||
const {
|
const {
|
||||||
result: { hosts, count },
|
result: { hosts, count },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
@@ -56,7 +56,7 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
|||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={contentError}
|
contentError={contentError}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading || isAdHocLaunchLoading}
|
||||||
items={hosts}
|
items={hosts}
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
pluralizedItemName={i18n._(t`Hosts`)}
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
@@ -98,6 +98,7 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
adHocItems={selected}
|
adHocItems={selected}
|
||||||
hasListItems={count > 0}
|
hasListItems={count > 0}
|
||||||
|
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
|
|||||||
Reference in New Issue
Block a user