From e6ae171f4b60106b7618244fcf6be5be5e3b50e2 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 13 Aug 2020 15:38:18 -0400 Subject: [PATCH] Adds Ad Hoc Commands Wizard --- awx/ui_next/src/api/models/Inventories.js | 11 + .../AdHocCommands/AdHocCommands.jsx | 145 ++++++++ .../AdHocCommands/AdHocCommands.test.jsx | 339 ++++++++++++++++++ .../AdHocCommands/AdHocCommandsWizard.jsx | 95 +++++ .../AdHocCommandsWizard.test.jsx | 176 +++++++++ .../AdHocCommands/AdHocCredentialStep.jsx | 114 ++++++ .../AdHocCredentialStep.test.jsx | 51 +++ .../components/AdHocCommands/DetailsStep.jsx | 300 ++++++++++++++++ .../AdHocCommands/DetailsStep.test.jsx | 150 ++++++++ .../src/components/AdHocCommands/index.js | 1 + .../InventoryGroups/InventoryGroupsList.jsx | 30 ++ 11 files changed, 1412 insertions(+) create mode 100644 awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.test.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.test.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/DetailsStep.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/DetailsStep.test.jsx create mode 100644 awx/ui_next/src/components/AdHocCommands/index.js diff --git a/awx/ui_next/src/api/models/Inventories.js b/awx/ui_next/src/api/models/Inventories.js index 077a534d27..c9d774e002 100644 --- a/awx/ui_next/src/api/models/Inventories.js +++ b/awx/ui_next/src/api/models/Inventories.js @@ -99,6 +99,17 @@ class Inventories extends InstanceGroupsMixin(Base) { `${this.baseUrl}${inventoryId}/update_inventory_sources/` ); } + + readAdHocOptions(inventoryId) { + return this.http.options(`${this.baseUrl}${inventoryId}/ad_hoc_commands/`); + } + + launchAdHocCommands(inventoryId, values) { + return this.http.post( + `${this.baseUrl}${inventoryId}/ad_hoc_commands/`, + values + ); + } } export default Inventories; diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx new file mode 100644 index 0000000000..0fa1990a6d --- /dev/null +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx @@ -0,0 +1,145 @@ +import React, { useState, Fragment, useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; + +import useRequest, { useDismissableError } from '../../util/useRequest'; +import AlertModal from '../AlertModal'; +import { CredentialTypesAPI } from '../../api'; +import ErrorDetail from '../ErrorDetail'; +import AdHocCommandsForm from './AdHocCommandsWizard'; +import ContentLoading from '../ContentLoading'; +import ContentError from '../ContentError'; + +function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) { + const [isWizardOpen, setIsWizardOpen] = useState(false); + const history = useHistory(); + + const { + error: fetchError, + request: fetchModuleOptions, + result: { moduleOptions, verbosityOptions, credentialTypeId }, + } = useRequest( + useCallback(async () => { + const [choices, credId] = await Promise.all([ + apiModule.readAdHocOptions(itemId), + CredentialTypesAPI.read({ namespace: 'ssh' }), + ]); + const itemObject = (item, index) => { + return { + key: index, + value: item, + label: `${item}`, + isDisabled: false, + }; + }; + + const options = choices.data.actions.GET.module_name.choices.map( + (choice, index) => itemObject(choice[0], index) + ); + + const verbosityItems = choices.data.actions.GET.verbosity.choices.map( + (choice, index) => itemObject(choice[0], index) + ); + + return { + moduleOptions: [itemObject('', -1), ...options], + verbosityOptions: [itemObject('', -1), ...verbosityItems], + credentialTypeId: credId.data.results[0].id, + }; + }, [itemId, apiModule]), + { moduleOptions: [] } + ); + + useEffect(() => { + fetchModuleOptions(); + }, [fetchModuleOptions]); + + const { + isloading: isLaunchLoading, + error: launchError, + request: launchAdHocCommands, + } = useRequest( + useCallback( + async values => { + const { data } = await apiModule.launchAdHocCommands(itemId, values); + history.push(`/jobs/${data.module_name}/${data.id}/output`); + }, + [apiModule, itemId, history] + ) + ); + + const { error, dismissError } = useDismissableError( + launchError || fetchError + ); + + const handleSubmit = async (values, limitTypedValue) => { + const { credential, limit, ...remainingValues } = values; + const newCredential = credential[0].id; + if (limitTypedValue) { + values.limit = limit.concat(limitTypedValue); + } + const stringifyLimit = values.limit.join(', ').trim(); + + const manipulatedValues = { + limit: stringifyLimit[0], + credential: newCredential, + ...remainingValues, + }; + await launchAdHocCommands(manipulatedValues); + setIsWizardOpen(false); + }; + + if (isLaunchLoading) { + return ; + } + + if (error && isWizardOpen) { + return ( + { + dismissError(); + setIsWizardOpen(false); + }} + > + + + ); + } + return ( + + {children({ + openAdHocCommands: () => setIsWizardOpen(true), + })} + + {isWizardOpen && ( + setIsWizardOpen(false)} + onLaunch={handleSubmit} + error={error} + onDismissError={() => dismissError()} + /> + )} + {launchError && ( + + {i18n._(t`Failed to launch job.`)} + + + )} + + ); +} + +export default withI18n()(AdHocCommands); diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx new file mode 100644 index 0000000000..dadd88fc86 --- /dev/null +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx @@ -0,0 +1,339 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { + mountWithContexts, + waitForElement, +} from '../../../testUtils/enzymeHelpers'; +import { CredentialTypesAPI, InventoriesAPI, CredentialsAPI } from '../../api'; +import AdHocCommands from './AdHocCommands'; + +jest.mock('../../api/models/CredentialTypes'); +jest.mock('../../api/models/Inventories'); +jest.mock('../../api/models/Credentials'); + +const credentials = [ + { id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' }, + { id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' }, + { id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' }, + { id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' }, + { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, +]; +const adHocItems = [ + { + name: ' Inventory 1 Org 0', + }, + { name: 'Inventory 2 Org 0' }, +]; + +const children = ({ openAdHocCommands }) => ( + , + [ + + + {({ openAdHocCommands }) => ( + + )} + + , + ], ]} /> )}