diff --git a/app/models/concerns/api/v2/hosts_controller_extensions.rb b/app/models/concerns/api/v2/hosts_controller_extensions.rb new file mode 100644 index 000000000..8f4f98416 --- /dev/null +++ b/app/models/concerns/api/v2/hosts_controller_extensions.rb @@ -0,0 +1,12 @@ +module Api + module V2 + module HostsControllerExtensions + extend ActiveSupport::Concern + def index_node_permissions + super.merge({ + :can_create_job_invocations => authorized_for(:controller => 'job_invocations', :action => 'create'), + }) + end + end + end +end diff --git a/lib/foreman_remote_execution/engine.rb b/lib/foreman_remote_execution/engine.rb index b7db148d6..b748fbe34 100644 --- a/lib/foreman_remote_execution/engine.rb +++ b/lib/foreman_remote_execution/engine.rb @@ -337,6 +337,7 @@ class Engine < ::Rails::Engine ForemanRemoteExecution.register_rex_feature + ::Api::V2::HostsController.include Api::V2::HostsControllerExtensions ::Api::V2::SubnetsController.include ::ForemanRemoteExecution::Concerns::Api::V2::SubnetsControllerExtensions ::Api::V2::RegistrationController.prepend ::ForemanRemoteExecution::Concerns::Api::V2::RegistrationControllerExtensions ::Api::V2::RegistrationController.include ::ForemanRemoteExecution::Concerns::Api::V2::RegistrationControllerExtensions::ApipieExtensions diff --git a/webpack/react_app/components/FeaturesDropdown/actions.js b/webpack/react_app/components/FeaturesDropdown/actions.js index 002b5da02..ed5c1aa7e 100644 --- a/webpack/react_app/components/FeaturesDropdown/actions.js +++ b/webpack/react_app/components/FeaturesDropdown/actions.js @@ -2,9 +2,11 @@ import { foremanUrl } from 'foremanReact/common/helpers'; import { sprintf, translate as __ } from 'foremanReact/common/I18n'; import { post } from 'foremanReact/redux/API'; -export const runFeature = (hostId, feature, label) => dispatch => { +export const runFeature = (hostId, feature, label, hostSearch) => dispatch => { const url = foremanUrl( - `/job_invocations?feature=${feature}&host_ids%5B%5D=${hostId}` + hostId + ? `/job_invocations?feature=${feature}&host_ids%5B%5D=${hostId}` + : `/job_invocations?feature=${feature}&search=${hostSearch}` ); const redirectUrl = 'job_invocations/new'; diff --git a/webpack/react_app/components/FeaturesDropdown/constant.js b/webpack/react_app/components/FeaturesDropdown/constant.js deleted file mode 100644 index b390cbc60..000000000 --- a/webpack/react_app/components/FeaturesDropdown/constant.js +++ /dev/null @@ -1,3 +0,0 @@ -export const REX_FEATURES_API = host => - `/api/v2/hosts/${host}/available_remote_execution_features`; -export const NEW_JOB_PAGE = '/job_invocations/new?host_ids%5B%5D'; diff --git a/webpack/react_app/components/FeaturesDropdown/constants.js b/webpack/react_app/components/FeaturesDropdown/constants.js new file mode 100644 index 000000000..4e05c57a6 --- /dev/null +++ b/webpack/react_app/components/FeaturesDropdown/constants.js @@ -0,0 +1,5 @@ +export const REX_FEATURES_HOST_URL = host => + `/api/v2/hosts/${host}/available_remote_execution_features`; +export const ALL_REX_FEATURES_URL = '/api/v2/remote_execution_features'; +export const NEW_JOB_PAGE = '/job_invocations/new?host_ids%5B%5D'; +export const ALL_HOSTS_NEW_JOB_PAGE = '/job_invocations/new?search'; diff --git a/webpack/react_app/components/FeaturesDropdown/index.js b/webpack/react_app/components/FeaturesDropdown/index.js index d9fdefbcd..729a6b6ea 100644 --- a/webpack/react_app/components/FeaturesDropdown/index.js +++ b/webpack/react_app/components/FeaturesDropdown/index.js @@ -11,58 +11,83 @@ import { push } from 'connected-react-router'; import { useAPI } from 'foremanReact/common/hooks/API/APIHooks'; import { translate as __ } from 'foremanReact/common/I18n'; -import { foremanUrl } from 'foremanReact/common/helpers'; +import { foremanUrl, propsToCamelCase } from 'foremanReact/common/helpers'; import { STATUS } from 'foremanReact/constants'; -import { REX_FEATURES_API, NEW_JOB_PAGE } from './constant'; +import { + REX_FEATURES_HOST_URL, + ALL_REX_FEATURES_URL, + NEW_JOB_PAGE, + ALL_HOSTS_NEW_JOB_PAGE, +} from './constants'; import { runFeature } from './actions'; +import './index.scss'; -const FeaturesDropdown = ({ hostId }) => { +const FeaturesDropdown = ({ + hostId, + hostSearch, + hostResponse, + selectedCount, +}) => { const [isOpen, setIsOpen] = useState(false); - const { response, status } = useAPI( - 'get', - foremanUrl(REX_FEATURES_API(hostId)) - ); + const isSingleHost = !!hostId; // identifies whether we're on the host details or host overview page + const rexFeaturesUrl = isSingleHost + ? REX_FEATURES_HOST_URL(hostId) + : ALL_REX_FEATURES_URL; + const { response, status } = useAPI('get', foremanUrl(rexFeaturesUrl)); const dispatch = useDispatch(); - // eslint-disable-next-line camelcase - const canRunJob = response?.permissions?.can_run_job; + const permissions = propsToCamelCase( + (isSingleHost ? response?.permissions : hostResponse?.response) || {} + ); + const canRunJob = isSingleHost + ? permissions.canRunJob + : permissions.canCreateJobInvocations; if (!canRunJob) { return null; } - // eslint-disable-next-line camelcase - const features = response?.remote_execution_features; + + const features = isSingleHost + ? response?.remote_execution_features // eslint-disable-line camelcase + : response?.results; const dropdownItems = features ?.filter(feature => feature.host_action_button) ?.map(({ name, label, id, description }) => ( dispatch(runFeature(hostId, label, name))} + onClick={() => dispatch(runFeature(hostId, label, name, hostSearch))} key={id} description={description} > {name} )); + const newJobPageUrl = hostId + ? `${NEW_JOB_PAGE}=${hostId}` + : `${ALL_HOSTS_NEW_JOB_PAGE}=${hostSearch}`; const scheduleJob = [ dispatch(push(`${NEW_JOB_PAGE}=${hostId}`))} + onClick={() => dispatch(push(newJobPageUrl))} key="schedule-job-action" > {__('Schedule a job')} , ]; + const disableDropdown = !isSingleHost && selectedCount === 0; + return ( setIsOpen(false)} toggle={ setIsOpen(prev => !prev)} - isDisabled={status === STATUS.PENDING} + isDisabled={status === STATUS.PENDING || disableDropdown} splitButtonVariant="action" /> } @@ -74,9 +99,23 @@ const FeaturesDropdown = ({ hostId }) => { FeaturesDropdown.propTypes = { hostId: PropTypes.number, + hostSearch: PropTypes.string, + selectedCount: PropTypes.number, + hostResponse: PropTypes.shape({ + response: PropTypes.shape({ + results: PropTypes.arrayOf( + PropTypes.shape({ + can_create_job_invocations: PropTypes.bool, + }) + ), + }), + }), }; FeaturesDropdown.defaultProps = { hostId: undefined, + hostSearch: undefined, + selectedCount: 0, + hostResponse: undefined, }; export default FeaturesDropdown; diff --git a/webpack/react_app/components/FeaturesDropdown/index.scss b/webpack/react_app/components/FeaturesDropdown/index.scss new file mode 100644 index 000000000..a17dea8e7 --- /dev/null +++ b/webpack/react_app/components/FeaturesDropdown/index.scss @@ -0,0 +1,11 @@ +#schedule-a-job-dropdown ul.pf-c-dropdown__menu { + padding-left: 0; + li { + display: unset; + a { + font-size: 16px; + color: var(--pf-c-dropdown__menu-item--Color); + font-weight: 300; + } + } +} \ No newline at end of file diff --git a/webpack/react_app/extend/Fills.js b/webpack/react_app/extend/Fills.js index 234f8e2db..305546726 100644 --- a/webpack/react_app/extend/Fills.js +++ b/webpack/react_app/extend/Fills.js @@ -38,6 +38,12 @@ const fills = [ component: props => , weight: 1000, }, + { + slot: '_all-hosts-schedule-a-job', + name: '_all-hosts-schedule-a-job', + component: props => , + weight: 1000, + }, ]; const registerFills = () => {