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 = () => {