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:
softwarefactory-project-zuul[bot] 2021-04-29 20:43:55 +00:00 committed by GitHub
commit 43d33281a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 489 additions and 46 deletions

View File

@ -12,10 +12,9 @@ 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({ adHocItems, i18n, hasListItems }) {
function AdHocCommands({ adHocItems, i18n, hasListItems, onLaunchLoading }) {
const history = useHistory();
const { id } = useParams();
@ -36,22 +35,29 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
}, [isKebabified, isWizardOpen, onKebabModalChange]);
const {
result: { moduleOptions, credentialTypeId, isAdHocDisabled },
result: {
moduleOptions,
credentialTypeId,
isAdHocDisabled,
organizationId,
},
request: fetchData,
error: fetchError,
} = useRequest(
useCallback(async () => {
const [options, cred] = await Promise.all([
const [options, { data }, cred] = await Promise.all([
InventoriesAPI.readAdHocOptions(id),
InventoriesAPI.readDetail(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,
organizationId: data.organization,
};
}, [id]),
{ moduleOptions: [], isAdHocDisabled: true }
{ moduleOptions: [], isAdHocDisabled: true, organizationId: null }
);
useEffect(() => {
fetchData();
@ -76,19 +82,20 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
);
const handleSubmit = async values => {
const { credential, ...remainingValues } = values;
const { credential, execution_environment, ...remainingValues } = values;
const newCredential = credential[0].id;
const manipulatedValues = {
credential: newCredential,
execution_environment: execution_environment[0]?.id,
...remainingValues,
};
await launchAdHocCommands(manipulatedValues);
};
if (isLaunchLoading) {
return <ContentLoading />;
}
useEffect(() => onLaunchLoading(isLaunchLoading), [
isLaunchLoading,
onLaunchLoading,
]);
if (error && isWizardOpen) {
return (
@ -141,6 +148,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
{isWizardOpen && (
<AdHocCommandsWizard
adHocItems={adHocItems}
organizationId={organizationId}
moduleOptions={moduleOptions}
verbosityOptions={verbosityOptions}
credentialTypeId={credentialTypeId}

View File

@ -4,12 +4,19 @@ import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { CredentialTypesAPI, InventoriesAPI, CredentialsAPI } from '../../api';
import {
CredentialTypesAPI,
InventoriesAPI,
CredentialsAPI,
ExecutionEnvironmentsAPI,
} from '../../api';
import AdHocCommands from './AdHocCommands';
jest.mock('../../api/models/CredentialTypes');
jest.mock('../../api/models/Inventories');
jest.mock('../../api/models/Credentials');
jest.mock('../../api/models/ExecutionEnvironments');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({
@ -51,6 +58,15 @@ describe('<AdHocCommands />', () => {
CredentialTypesAPI.read.mockResolvedValue({
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;
afterEach(() => {
@ -61,7 +77,11 @@ describe('<AdHocCommands />', () => {
test('mounts successfully', async () => {
await act(async () => {
wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems />
<AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
);
});
expect(wrapper.find('AdHocCommands').length).toBe(1);
@ -83,12 +103,26 @@ describe('<AdHocCommands />', () => {
},
},
});
InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } });
CredentialTypesAPI.read.mockResolvedValue({
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 () => {
wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems />
<AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
);
});
await act(async () =>
@ -102,15 +136,35 @@ describe('<AdHocCommands />', () => {
test('should submit properly', async () => {
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
InventoriesAPI.readDetail.mockResolvedValue({
data: { organization: 1 },
});
CredentialsAPI.read.mockResolvedValue({
data: {
results: credentials,
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 () => {
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')()
);
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
// 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 () => {
wrapper
.find('input[aria-labelledby="check-action-item-4"]')
@ -176,6 +249,7 @@ describe('<AdHocCommands />', () => {
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
module_name: 'command',
verbosity: 1,
execution_environment: 2,
});
});
@ -202,13 +276,24 @@ describe('<AdHocCommands />', () => {
['foo', 'foo'],
],
},
verbosity: { choices: [[1], [2]] },
verbosity: {
choices: [[1], [2]],
},
},
},
},
});
InventoriesAPI.readDetail.mockResolvedValue({
data: { organization: 1 },
});
CredentialTypesAPI.read.mockResolvedValue({
data: { results: [{ id: 1 }] },
data: {
results: [
{
id: 1,
},
],
},
});
CredentialsAPI.read.mockResolvedValue({
data: {
@ -216,9 +301,33 @@ describe('<AdHocCommands />', () => {
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 () => {
wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems />
<AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
);
});
@ -240,7 +349,10 @@ describe('<AdHocCommands />', () => {
'command'
);
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);
});
@ -259,10 +371,36 @@ describe('<AdHocCommands />', () => {
// 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 () => {
wrapper
.find('input[aria-labelledby="check-action-item-4"]')
.simulate('change', { target: { checked: true } });
.simulate('change', {
target: {
checked: true,
},
});
});
wrapper.update();
@ -291,7 +429,11 @@ describe('<AdHocCommands />', () => {
});
await act(async () => {
wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems />
<AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@ -312,7 +454,11 @@ describe('<AdHocCommands />', () => {
});
await act(async () => {
wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems={false} />
<AdHocCommands
adHocItems={adHocItems}
hasListItems={false}
onLaunchLoading={() => jest.fn()}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@ -335,7 +481,11 @@ describe('<AdHocCommands />', () => {
);
await act(async () => {
wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems />
<AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
);
});
await act(async () => wrapper.find('button').prop('onClick')());

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
import { Tooltip } from '@patternfly/react-core';
@ -10,6 +10,7 @@ import styled from 'styled-components';
import Wizard from '../Wizard';
import AdHocCredentialStep from './AdHocCredentialStep';
import AdHocDetailsStep from './AdHocDetailsStep';
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
const AlertText = styled.div`
color: var(--pf-global--danger-color--200);
@ -23,11 +24,11 @@ const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
function AdHocCommandsWizard({
onLaunch,
i18n,
moduleOptions,
verbosityOptions,
onCloseWizard,
credentialTypeId,
organizationId,
}) {
const [currentStepId, setCurrentStepId] = useState(1);
const [enableLaunch, setEnableLaunch] = useState(false);
@ -57,17 +58,17 @@ function AdHocCommandsWizard({
key: 1,
name: hasDetailsStepError ? (
<AlertText>
{i18n._(t`Details`)}
{t`Details`}
<Tooltip
position="right"
content={i18n._(t`This step contains errors`)}
content={t`This step contains errors`}
trigger="click mouseenter focus"
>
<ExclamationCircleIcon />
</Tooltip>
</AlertText>
) : (
i18n._(t`Details`)
t`Details`
),
component: (
<AdHocDetailsStep
@ -76,12 +77,25 @@ function AdHocCommandsWizard({
/>
),
enableNext: enabledNextOnDetailsStep(),
nextButtonText: i18n._(t`Next`),
nextButtonText: t`Next`,
},
{
id: 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: (
<AdHocCredentialStep
credentialTypeId={credentialTypeId}
@ -89,7 +103,7 @@ function AdHocCommandsWizard({
/>
),
enableNext: enableLaunch && Object.values(errors).length === 0,
nextButtonText: i18n._(t`Launch`),
nextButtonText: t`Launch`,
canJumpTo: currentStepId >= 2,
},
];
@ -106,10 +120,10 @@ function AdHocCommandsWizard({
onLaunch(values);
}}
steps={steps}
title={i18n._(t`Run command`)}
title={t`Run command`}
nextButtonText={currentStep.nextButtonText || undefined}
backButtonText={i18n._(t`Back`)}
cancelButtonText={i18n._(t`Cancel`)}
backButtonText={t`Back`}
cancelButtonText={t`Cancel`}
/>
);
}
@ -128,6 +142,7 @@ const FormikApp = withFormik({
module_name: '',
extra_vars: '---',
job_type: 'run',
execution_environment: '',
};
},
})(AdHocCommandsWizard);
@ -139,4 +154,4 @@ FormikApp.propTypes = {
onCloseWizard: PropTypes.func.isRequired,
credentialTypeId: PropTypes.number.isRequired,
};
export default withI18n()(FormikApp);
export default FormikApp;

View File

@ -4,12 +4,14 @@ import {
mountWithContexts,
waitForElement,
} from '../../../testUtils/enzymeHelpers';
import { CredentialsAPI } from '../../api';
import { CredentialsAPI, ExecutionEnvironmentsAPI } from '../../api';
import AdHocCommandsWizard from './AdHocCommandsWizard';
jest.mock('../../api/models/CredentialTypes');
jest.mock('../../api/models/Inventories');
jest.mock('../../api/models/Credentials');
jest.mock('../../api/models/ExecutionEnvironments');
const verbosityOptions = [
{ value: '0', key: '0', label: '0 (Normal)' },
{ value: '1', key: '1', label: '1 (Verbose)' },
@ -39,6 +41,7 @@ describe('<AdHocCommandsWizard/>', () => {
verbosityOptions={verbosityOptions}
onCloseWizard={() => {}}
credentialTypeId={1}
organizationId={1}
/>
);
});
@ -97,6 +100,18 @@ describe('<AdHocCommandsWizard/>', () => {
wrapper.update();
});
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({
data: {
results: [
@ -127,10 +142,40 @@ describe('<AdHocCommandsWizard/>', () => {
);
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);
expect(wrapper.find('CheckboxListItem').length).toBe(2);
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
await act(async () => {
wrapper
.find('input[aria-labelledby="check-action-item-1"]')
@ -150,8 +195,21 @@ describe('<AdHocCommandsWizard/>', () => {
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 () => {
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
@ -201,6 +259,12 @@ describe('<AdHocCommandsWizard/>', () => {
wrapper.find('Button[type="submit"]').prop('onClick')()
);
wrapper.update();
await act(async () =>
wrapper.find('Button[type="submit"]').prop('onClick')()
);
wrapper.update();
expect(wrapper.find('ContentError').length).toBe(1);
});

View File

@ -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);
});
});

View File

@ -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;

View File

@ -28,6 +28,7 @@ const QS_CONFIG = getQSConfig('host', {
});
function InventoryGroupHostList({ i18n }) {
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const { id: inventoryId, groupId } = useParams();
const location = useLocation();
@ -172,7 +173,9 @@ function InventoryGroupHostList({ i18n }) {
<>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading || isDisassociateLoading}
hasContentLoading={
isLoading || isDisassociateLoading || isAdHocLaunchLoading
}
items={hosts}
itemCount={hostCount}
pluralizedItemName={i18n._(t`Hosts`)}
@ -215,6 +218,7 @@ function InventoryGroupHostList({ i18n }) {
<AdHocCommands
adHocItems={selected}
hasListItems={hostCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
<DisassociateButton
key="disassociate"

View File

@ -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 { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@ -30,6 +30,7 @@ function cannotDelete(item) {
function InventoryGroupsList({ i18n }) {
const location = useLocation();
const { id: inventoryId } = useParams();
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const {
result: {
@ -107,7 +108,7 @@ function InventoryGroupsList({ i18n }) {
<>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={groups}
itemCount={groupCount}
qsConfig={QS_CONFIG}
@ -174,6 +175,7 @@ function InventoryGroupsList({ i18n }) {
<AdHocCommands
adHocItems={selected}
hasListItems={groupCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
<Tooltip content={renderTooltip()} position="top" key="delete">
<InventoryGroupsDeleteModal

View File

@ -28,6 +28,7 @@ const QS_CONFIG = getQSConfig('group', {
function InventoryHostGroupsList({ i18n }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const { hostId, id: invId } = useParams();
const { search } = useLocation();
@ -147,7 +148,9 @@ function InventoryHostGroupsList({ i18n }) {
<>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading || isDisassociateLoading}
hasContentLoading={
isLoading || isDisassociateLoading || isAdHocLaunchLoading
}
items={groups}
itemCount={itemCount}
qsConfig={QS_CONFIG}
@ -205,6 +208,7 @@ function InventoryHostGroupsList({ i18n }) {
<AdHocCommands
adHocItems={selected}
hasListItems={itemCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
<DisassociateButton
key="disassociate"

View File

@ -23,6 +23,7 @@ const QS_CONFIG = getQSConfig('host', {
function InventoryHostList({ i18n }) {
const [selected, setSelected] = useState([]);
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const { id } = useParams();
const { search } = useLocation();
@ -106,7 +107,7 @@ function InventoryHostList({ i18n }) {
<>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading || isDeleteLoading}
hasContentLoading={isLoading || isDeleteLoading || isAdHocLaunchLoading}
items={hosts}
itemCount={hostCount}
pluralizedItemName={i18n._(t`Hosts`)}
@ -152,6 +153,7 @@ function InventoryHostList({ i18n }) {
<AdHocCommands
adHocItems={selected}
hasListItems={hostCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
<ToolbarDeleteButton
key="delete"

View File

@ -26,6 +26,7 @@ const QS_CONFIG = getQSConfig('group', {
});
function InventoryRelatedGroupList({ i18n }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const [associateError, setAssociateError] = useState(null);
const [disassociateError, setDisassociateError] = useState(null);
const { id: inventoryId, groupId } = useParams();
@ -154,7 +155,7 @@ function InventoryRelatedGroupList({ i18n }) {
<>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={groups}
itemCount={itemCount}
pluralizedItemName={i18n._(t`Related Groups`)}
@ -197,6 +198,7 @@ function InventoryRelatedGroupList({ i18n }) {
<AdHocCommands
adHocItems={selected}
hasListItems={itemCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
<DisassociateButton
key="disassociate"

View File

@ -1,4 +1,4 @@
import React, { useEffect, useCallback } from 'react';
import React, { useEffect, useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@ -20,7 +20,7 @@ const QS_CONFIG = getQSConfig('host', {
function SmartInventoryHostList({ i18n, inventory }) {
const location = useLocation();
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
const {
result: { hosts, count },
error: contentError,
@ -56,7 +56,7 @@ function SmartInventoryHostList({ i18n, inventory }) {
<>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={hosts}
itemCount={count}
pluralizedItemName={i18n._(t`Hosts`)}
@ -98,6 +98,7 @@ function SmartInventoryHostList({ i18n, inventory }) {
<AdHocCommands
adHocItems={selected}
hasListItems={count > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
]
: []