From edff8ab3faab2b08b06c3bdd582055298695c2e7 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Thu, 2 Nov 2023 14:26:00 -0400 Subject: [PATCH 1/4] Fixes #36887 - Add Schedule a Job to new host overview --- .../components/FeaturesDropdown/actions.js | 6 ++- .../components/FeaturesDropdown/constant.js | 3 -- .../components/FeaturesDropdown/constants.js | 5 +++ .../components/FeaturesDropdown/index.js | 41 +++++++++++++------ .../components/FeaturesDropdown/index.scss | 11 +++++ webpack/react_app/extend/Fills.js | 6 +++ 6 files changed, 55 insertions(+), 17 deletions(-) delete mode 100644 webpack/react_app/components/FeaturesDropdown/constant.js create mode 100644 webpack/react_app/components/FeaturesDropdown/constants.js create mode 100644 webpack/react_app/components/FeaturesDropdown/index.scss 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..8ca244777 100644 --- a/webpack/react_app/components/FeaturesDropdown/index.js +++ b/webpack/react_app/components/FeaturesDropdown/index.js @@ -14,37 +14,48 @@ import { translate as __ } from 'foremanReact/common/I18n'; import { foremanUrl } 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, selectedCount }) => { const [isOpen, setIsOpen] = useState(false); - const { response, status } = useAPI( - 'get', - foremanUrl(REX_FEATURES_API(hostId)) - ); + const rexFeaturesUrl = hostId + ? 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; - if (!canRunJob) { + if (!!hostId && !canRunJob) { return null; } - // eslint-disable-next-line camelcase - const features = response?.remote_execution_features; + + const features = hostId + ? 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')} @@ -54,15 +65,17 @@ const FeaturesDropdown = ({ hostId }) => { return ( setIsOpen(false)} toggle={ setIsOpen(prev => !prev)} - isDisabled={status === STATUS.PENDING} + isDisabled={status === STATUS.PENDING || selectedCount === 0} splitButtonVariant="action" /> } @@ -74,9 +87,13 @@ const FeaturesDropdown = ({ hostId }) => { FeaturesDropdown.propTypes = { hostId: PropTypes.number, + hostSearch: PropTypes.string, + selectedCount: PropTypes.number, }; FeaturesDropdown.defaultProps = { hostId: undefined, + hostSearch: undefined, + selectedCount: 0, }; 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 = () => { From 305e8987527151c9b5a7281696a2a0b304a24f8a Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 3 Nov 2023 12:07:05 -0400 Subject: [PATCH 2/4] Refs #36887 - ensure dropdown isn't disabled on host details page --- .../react_app/components/FeaturesDropdown/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/webpack/react_app/components/FeaturesDropdown/index.js b/webpack/react_app/components/FeaturesDropdown/index.js index 8ca244777..82f0cc490 100644 --- a/webpack/react_app/components/FeaturesDropdown/index.js +++ b/webpack/react_app/components/FeaturesDropdown/index.js @@ -25,18 +25,19 @@ import './index.scss'; const FeaturesDropdown = ({ hostId, hostSearch, selectedCount }) => { const [isOpen, setIsOpen] = useState(false); - const rexFeaturesUrl = 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; - if (!!hostId && !canRunJob) { + if (isSingleHost && !canRunJob) { return null; } - const features = hostId + const features = isSingleHost ? response?.remote_execution_features // eslint-disable-line camelcase : response?.results; const dropdownItems = features @@ -62,6 +63,8 @@ const FeaturesDropdown = ({ hostId, hostSearch, selectedCount }) => { , ]; + const disableDropdown = !isSingleHost && selectedCount === 0; + return ( { splitButtonItems={scheduleJob} toggleVariant="secondary" onToggle={() => setIsOpen(prev => !prev)} - isDisabled={status === STATUS.PENDING || selectedCount === 0} + isDisabled={status === STATUS.PENDING || disableDropdown} splitButtonVariant="action" /> } From fdd71feb3b12cf17dc86f35d3afc6ebfcbf2daaa Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 6 Nov 2023 17:43:16 -0500 Subject: [PATCH 3/4] Refs #36887 - hide dropdown based on permissions --- app/views/api/v2/host/main.rabl | 2 ++ app/views/api/v2/host/permissions.rabl | 7 +++++ .../components/FeaturesDropdown/index.js | 27 ++++++++++++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 app/views/api/v2/host/permissions.rabl diff --git a/app/views/api/v2/host/main.rabl b/app/views/api/v2/host/main.rabl index a6db41c11..c364f0df3 100644 --- a/app/views/api/v2/host/main.rabl +++ b/app/views/api/v2/host/main.rabl @@ -1 +1,3 @@ attributes :cockpit_url + +extends "api/v2/host/permissions" diff --git a/app/views/api/v2/host/permissions.rabl b/app/views/api/v2/host/permissions.rabl new file mode 100644 index 000000000..6dd18a1f9 --- /dev/null +++ b/app/views/api/v2/host/permissions.rabl @@ -0,0 +1,7 @@ +if params.has_key?(:include_permissions) + node do |resource| + if resource&.class&.try(:include?, Authorizable) + node(:can_create_job_invocations) { authorized_for(:auth_object => resource, :authorizer => authorizer, :permission => "create_job_invocations") } + end + end +end diff --git a/webpack/react_app/components/FeaturesDropdown/index.js b/webpack/react_app/components/FeaturesDropdown/index.js index 82f0cc490..ee1a16404 100644 --- a/webpack/react_app/components/FeaturesDropdown/index.js +++ b/webpack/react_app/components/FeaturesDropdown/index.js @@ -23,7 +23,12 @@ import { import { runFeature } from './actions'; import './index.scss'; -const FeaturesDropdown = ({ hostId, hostSearch, selectedCount }) => { +const FeaturesDropdown = ({ + hostId, + hostSearch, + hostResponse, + selectedCount, +}) => { const [isOpen, setIsOpen] = useState(false); const isSingleHost = !!hostId; // identifies whether we're on the host details or host overview page const rexFeaturesUrl = isSingleHost @@ -31,9 +36,13 @@ const FeaturesDropdown = ({ hostId, hostSearch, selectedCount }) => { : 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; - if (isSingleHost && !canRunJob) { + const canRunJob = isSingleHost + ? // eslint-disable-next-line camelcase + response?.permissions?.can_run_job + : hostResponse?.response?.results?.some( + result => result.can_create_job_invocations + ); + if (!canRunJob) { return null; } @@ -92,11 +101,21 @@ 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; From 681168edf5b4d6453dbe3ad02d3e695e4842b49e Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 20 Nov 2023 13:35:53 -0500 Subject: [PATCH 4/4] Refs #36887 - use general permission, not per-host --- .../concerns/api/v2/hosts_controller_extensions.rb | 12 ++++++++++++ app/views/api/v2/host/main.rabl | 2 -- app/views/api/v2/host/permissions.rabl | 7 ------- lib/foreman_remote_execution/engine.rb | 1 + .../react_app/components/FeaturesDropdown/index.js | 12 ++++++------ 5 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 app/models/concerns/api/v2/hosts_controller_extensions.rb delete mode 100644 app/views/api/v2/host/permissions.rabl 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/app/views/api/v2/host/main.rabl b/app/views/api/v2/host/main.rabl index c364f0df3..a6db41c11 100644 --- a/app/views/api/v2/host/main.rabl +++ b/app/views/api/v2/host/main.rabl @@ -1,3 +1 @@ attributes :cockpit_url - -extends "api/v2/host/permissions" diff --git a/app/views/api/v2/host/permissions.rabl b/app/views/api/v2/host/permissions.rabl deleted file mode 100644 index 6dd18a1f9..000000000 --- a/app/views/api/v2/host/permissions.rabl +++ /dev/null @@ -1,7 +0,0 @@ -if params.has_key?(:include_permissions) - node do |resource| - if resource&.class&.try(:include?, Authorizable) - node(:can_create_job_invocations) { authorized_for(:auth_object => resource, :authorizer => authorizer, :permission => "create_job_invocations") } - 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/index.js b/webpack/react_app/components/FeaturesDropdown/index.js index ee1a16404..729a6b6ea 100644 --- a/webpack/react_app/components/FeaturesDropdown/index.js +++ b/webpack/react_app/components/FeaturesDropdown/index.js @@ -11,7 +11,7 @@ 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 { @@ -36,12 +36,12 @@ const FeaturesDropdown = ({ : ALL_REX_FEATURES_URL; const { response, status } = useAPI('get', foremanUrl(rexFeaturesUrl)); const dispatch = useDispatch(); + const permissions = propsToCamelCase( + (isSingleHost ? response?.permissions : hostResponse?.response) || {} + ); const canRunJob = isSingleHost - ? // eslint-disable-next-line camelcase - response?.permissions?.can_run_job - : hostResponse?.response?.results?.some( - result => result.can_create_job_invocations - ); + ? permissions.canRunJob + : permissions.canCreateJobInvocations; if (!canRunJob) { return null; }