Adds an execution environment step to the ad hoc commands

This commit is contained in:
Alex Corey
2021-04-20 16:26:39 -04:00
parent 1e7b7d1a30
commit e6bde23aea
16 changed files with 16785 additions and 33 deletions

View File

@@ -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();
@@ -76,19 +75,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 (

View File

@@ -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);
@@ -86,9 +106,22 @@ describe('<AdHocCommands />', () => {
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 () =>
@@ -108,9 +141,22 @@ 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,
},
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems /> <AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
); );
}); });
@@ -147,8 +193,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 +241,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 +268,21 @@ describe('<AdHocCommands />', () => {
['foo', 'foo'], ['foo', 'foo'],
], ],
}, },
verbosity: { choices: [[1], [2]] }, verbosity: {
choices: [[1], [2]],
},
}, },
}, },
}, },
}); });
CredentialTypesAPI.read.mockResolvedValue({ CredentialTypesAPI.read.mockResolvedValue({
data: { results: [{ id: 1 }] }, data: {
results: [
{
id: 1,
},
],
},
}); });
CredentialsAPI.read.mockResolvedValue({ CredentialsAPI.read.mockResolvedValue({
data: { data: {
@@ -216,9 +290,30 @@ 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,
},
});
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<AdHocCommands adHocItems={adHocItems} hasListItems /> <AdHocCommands
adHocItems={adHocItems}
hasListItems
onLaunchLoading={() => jest.fn()}
/>
); );
}); });
@@ -240,7 +335,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 +357,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 +415,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 +440,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 +467,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')());

View File

@@ -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);
@@ -81,6 +82,14 @@ function AdHocCommandsWizard({
{ {
id: 2, id: 2,
key: 2, key: 2,
name: t`Execution Environment`,
component: <AdHocExecutionEnvironmentStep />,
enableNext: true,
canJumpTo: currentStepId >= 2,
},
{
id: 3,
key: 3,
name: i18n._(t`Machine credential`), name: i18n._(t`Machine credential`),
component: ( component: (
<AdHocCredentialStep <AdHocCredentialStep
@@ -128,6 +137,7 @@ const FormikApp = withFormik({
module_name: '', module_name: '',
extra_vars: '---', extra_vars: '---',
job_type: 'run', job_type: 'run',
execution_environment: '',
}; };
}, },
})(AdHocCommandsWizard); })(AdHocCommandsWizard);

View File

@@ -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)' },
@@ -97,6 +99,15 @@ 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,
},
});
CredentialsAPI.read.mockResolvedValue({ CredentialsAPI.read.mockResolvedValue({
data: { data: {
results: [ results: [
@@ -127,10 +138,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 +191,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 +255,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);
}); });

View File

@@ -0,0 +1,47 @@
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,
},
});
await act(async () => {
wrapper = mountWithContexts(
<Formik>
<AdHocExecutionEnvironmentStep />
</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,108 @@
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 } 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_environemts', {
page: 1,
page_size: 5,
order_by: 'name',
});
function AdHocExecutionEnvironmentStep() {
const history = useHistory();
const [executionEnvironmentField, , executionEnvironmentHelpers] = useField(
'execution_environment'
);
const {
error,
isLoading,
request: fetchExecutionEnvironments,
result: { executionEnvironments, executionEnvironmentsCount },
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, history.location.search);
const {
data: { results, count },
} = await ExecutionEnvironmentsAPI.read(params);
return {
executionEnvironments: results,
executionEnvironmentsCount: count,
};
}, [history.location.search]),
{ executionEnvironments: [], executionEnvironmentsCount: 0 }
);
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
value={executionEnvironmentField.value || []}
options={executionEnvironments}
optionCount={executionEnvironmentsCount}
header={t`Execution Environments`}
qsConfig={QS_CONFIG}
searchColumns={[
{
name: t`Name`,
key: 'name',
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"
selectItem={value => {
executionEnvironmentHelpers.setValue([value]);
}}
deselectItem={() => {
executionEnvironmentHelpers.setValue([]);
}}
/>
</FormGroup>
</Form>
);
}
export default AdHocExecutionEnvironmentStep;

View File

@@ -92,11 +92,11 @@ describe('<InventoryDetail />', () => {
expectDetailToMatch(wrapper, 'Type', 'Inventory'); expectDetailToMatch(wrapper, 'Type', 'Inventory');
const org = wrapper.find('Detail[label="Organization"]'); const org = wrapper.find('Detail[label="Organization"]');
expect(org.prop('value')).toMatchInlineSnapshot(` expect(org.prop('value')).toMatchInlineSnapshot(`
<Link <ForwardRef
to="/organizations/1/details" to="/organizations/1/details"
> >
The Organization The Organization
</Link> </ForwardRef>
`); `);
const vars = wrapper.find('VariablesDetail'); const vars = wrapper.find('VariablesDetail');
expect(vars).toHaveLength(1); expect(vars).toHaveLength(1);

View File

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

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 { 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

View File

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

View File

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

View File

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

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 { 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}
/>, />,
] ]
: [] : []