foreman_remote_execution 16.0.3 → 16.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_templates_controller.rb +2 -1
  3. data/app/controllers/cockpit_controller.rb +2 -2
  4. data/app/controllers/job_invocations_controller.rb +28 -1
  5. data/app/controllers/template_invocations_controller.rb +1 -1
  6. data/app/helpers/hosts_extensions_helper.rb +1 -1
  7. data/app/lib/actions/remote_execution/run_host_job.rb +5 -28
  8. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +7 -1
  9. data/app/models/job_template.rb +5 -0
  10. data/app/models/{ssh_execution_provider.rb → script_execution_provider.rb} +2 -4
  11. data/config/initializers/inflections.rb +0 -1
  12. data/config/routes.rb +1 -0
  13. data/lib/foreman_remote_execution/engine.rb +10 -257
  14. data/lib/foreman_remote_execution/plugin.rb +246 -0
  15. data/lib/foreman_remote_execution/version.rb +1 -1
  16. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  17. data/test/functional/api/v2/job_templates_controller_test.rb +29 -0
  18. data/test/unit/actions/run_host_job_test.rb +1 -1
  19. data/test/unit/job_invocation_report_template_test.rb +6 -6
  20. data/test/unit/remote_execution_provider_test.rb +19 -19
  21. data/webpack/JobInvocationDetail/CheckboxesActions.js +196 -0
  22. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  23. data/webpack/JobInvocationDetail/JobInvocationConstants.js +7 -7
  24. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +32 -0
  25. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +221 -91
  26. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  27. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +8 -22
  28. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +20 -27
  29. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  30. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  31. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  32. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  33. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  34. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +202 -0
  35. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  36. data/webpack/JobInvocationDetail/index.js +64 -31
  37. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  38. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  39. metadata +9 -7
  40. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  41. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -3,11 +3,13 @@ import {
3
3
  Flex,
4
4
  PageSection,
5
5
  PageSectionVariants,
6
+ Skeleton,
6
7
  } from '@patternfly/react-core';
7
8
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
8
9
  import { translate as __, documentLocale } from 'foremanReact/common/I18n';
9
10
  import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
10
11
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
12
+ import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
11
13
  import PropTypes from 'prop-types';
12
14
  import React, { useEffect, useState } from 'react';
13
15
  import { useDispatch, useSelector } from 'react-redux';
@@ -20,6 +22,7 @@ import {
20
22
  DATE_OPTIONS,
21
23
  JOB_INVOCATION_KEY,
22
24
  STATUS,
25
+ STATUS_UPPERCASE,
23
26
  } from './JobInvocationConstants';
24
27
  import JobInvocationHostTable from './JobInvocationHostTable';
25
28
  import JobInvocationOverview from './JobInvocationOverview';
@@ -38,25 +41,24 @@ const JobInvocationDetailPage = ({
38
41
  const items = useSelector(selectItems);
39
42
  const {
40
43
  description,
44
+ failed = 0,
41
45
  status_label: statusLabel,
42
46
  task,
43
47
  start_at: startAt,
44
- targeting,
48
+ targeting = {},
45
49
  } = items;
46
50
  const finished =
47
51
  statusLabel === STATUS.FAILED ||
48
52
  statusLabel === STATUS.SUCCEEDED ||
49
53
  statusLabel === STATUS.CANCELLED;
50
54
  const autoRefresh = task?.state === STATUS.PENDING || false;
51
- const { response, status } = useAPI(
52
- 'get',
53
- currentPermissionsUrl,
54
- CURRENT_PERMISSIONS
55
- );
55
+ useAPI('get', currentPermissionsUrl, {
56
+ key: CURRENT_PERMISSIONS,
57
+ });
56
58
  const [selectedFilter, setSelectedFilter] = useState('');
57
59
 
58
- const handleFilterChange = filter => {
59
- setSelectedFilter(filter);
60
+ const handleFilterChange = newFilter => {
61
+ setSelectedFilter(newFilter);
60
62
  };
61
63
 
62
64
  let isAlreadyStarted = false;
@@ -88,10 +90,22 @@ const JobInvocationDetailPage = ({
88
90
  // eslint-disable-next-line react-hooks/exhaustive-deps
89
91
  }, [dispatch, task?.id]);
90
92
 
93
+ const pageStatus =
94
+ items.id === undefined
95
+ ? STATUS_UPPERCASE.PENDING
96
+ : STATUS_UPPERCASE.RESOLVED;
97
+
91
98
  const breadcrumbOptions = {
92
99
  breadcrumbItems: [
93
100
  { caption: __('Jobs'), url: `/job_invocations` },
94
- { caption: description },
101
+ {
102
+ caption:
103
+ pageStatus === STATUS_UPPERCASE.PENDING ? (
104
+ <Skeleton width="350px" />
105
+ ) : (
106
+ description
107
+ ),
108
+ },
95
109
  ],
96
110
  isPf4: true,
97
111
  };
@@ -101,25 +115,27 @@ const JobInvocationDetailPage = ({
101
115
  <PageLayout
102
116
  header={description}
103
117
  breadcrumbOptions={breadcrumbOptions}
104
- toolbarButtons={
105
- <JobInvocationToolbarButtons
106
- jobId={id}
107
- data={items}
108
- currentPermissions={response.results}
109
- permissionsStatus={status}
110
- />
111
- }
118
+ toolbarButtons={<JobInvocationToolbarButtons jobId={id} data={items} />}
112
119
  searchable={false}
113
120
  >
114
121
  <Flex
115
122
  className="job-invocation-detail-flex"
116
123
  alignItems={{ default: 'alignItemsFlexStart' }}
117
124
  >
118
- <JobInvocationSystemStatusChart
119
- data={items}
120
- isAlreadyStarted={isAlreadyStarted}
121
- formattedStartDate={formattedStartDate}
122
- />
125
+ <SkeletonLoader
126
+ status={pageStatus}
127
+ skeletonProps={{
128
+ height: 105,
129
+ width: 375,
130
+ }}
131
+ >
132
+ <JobInvocationSystemStatusChart
133
+ data={items}
134
+ isAlreadyStarted={isAlreadyStarted}
135
+ formattedStartDate={formattedStartDate}
136
+ onFilterChange={handleFilterChange}
137
+ />
138
+ </SkeletonLoader>
123
139
  <Divider
124
140
  orientation={{
125
141
  default: 'vertical',
@@ -129,34 +145,51 @@ const JobInvocationDetailPage = ({
129
145
  className="job-overview"
130
146
  alignItems={{ default: 'alignItemsCenter' }}
131
147
  >
132
- <JobInvocationOverview
133
- data={items}
134
- isAlreadyStarted={isAlreadyStarted}
135
- formattedStartDate={formattedStartDate}
136
- onFilterChange={handleFilterChange}
137
- />
148
+ <SkeletonLoader
149
+ status={pageStatus}
150
+ skeletonProps={{
151
+ height: 105,
152
+ width: 270,
153
+ }}
154
+ >
155
+ <JobInvocationOverview
156
+ data={items}
157
+ isAlreadyStarted={isAlreadyStarted}
158
+ formattedStartDate={formattedStartDate}
159
+ />
160
+ </SkeletonLoader>
138
161
  </Flex>
139
162
  </Flex>
140
163
  <PageSection
141
164
  variant={PageSectionVariants.light}
142
165
  className="job-additional-info"
143
166
  >
144
- {items.id !== undefined && <JobAdditionInfo data={items} />}
167
+ <SkeletonLoader
168
+ status={pageStatus}
169
+ skeletonProps={{ height: 150, width: '100%' }}
170
+ >
171
+ <JobAdditionInfo data={items} />
172
+ </SkeletonLoader>
145
173
  </PageSection>
146
174
  </PageLayout>
147
175
  <PageSection
148
176
  variant={PageSectionVariants.light}
149
177
  className="job-details-table-section table-section"
150
178
  >
151
- {items.id !== undefined && (
179
+ <SkeletonLoader
180
+ status={pageStatus}
181
+ skeletonProps={{ height: 400, width: '100%' }}
182
+ >
152
183
  <JobInvocationHostTable
153
184
  id={id}
154
185
  targeting={targeting}
186
+ failedCount={failed}
155
187
  finished={finished}
156
188
  autoRefresh={autoRefresh}
157
189
  initialFilter={selectedFilter}
190
+ onFilterUpdate={handleFilterChange}
158
191
  />
159
- )}
192
+ </SkeletonLoader>
160
193
  </PageSection>
161
194
  </>
162
195
  );
@@ -1,12 +1,31 @@
1
1
  export const buildHostQuery = (selected, search) => {
2
+ const nameEscape = name => `"${name.replaceAll('"', '\\"')}"`;
2
3
  const { hosts, hostCollections, hostGroups } = selected;
3
- const hostsSearch = `id ^ (${hosts.map(({ id }) => id).join(',')})`;
4
- const hostCollectionsSearch = `host_collection_id ^ (${hostCollections
5
- .map(({ id }) => id)
6
- .join(',')})`;
7
- const hostGroupsSearch = `hostgroup_id ^ (${hostGroups
8
- .map(({ id }) => id)
9
- .join(',')})`;
4
+ const MAX_NAME_ITEMS = 50;
5
+ let hostsSearch;
6
+ if (hosts.length < MAX_NAME_ITEMS)
7
+ hostsSearch = `name ^ (${hosts.map(({ name }) => name).join(',')})`;
8
+ else hostsSearch = `id ^ (${hosts.map(({ id }) => id).join(',')})`;
9
+
10
+ let hostCollectionsSearch;
11
+ if (hostCollections.length < MAX_NAME_ITEMS)
12
+ hostCollectionsSearch = `host_collection ^ (${hostCollections
13
+ .map(({ name }) => nameEscape(name))
14
+ .join(',')})`;
15
+ else
16
+ hostCollectionsSearch = `host_collection_id ^ (${hostCollections
17
+ .map(({ id }) => id)
18
+ .join(',')})`;
19
+
20
+ let hostGroupsSearch;
21
+ if (hostCollections.length < MAX_NAME_ITEMS)
22
+ hostGroupsSearch = `hostgroup_fullname ^ (${hostGroups
23
+ .map(({ name }) => nameEscape(name))
24
+ .join(',')})`;
25
+ else
26
+ hostGroupsSearch = `hostgroup_id ^ (${hostGroups
27
+ .map(({ id }) => id)
28
+ .join(',')})`;
10
29
  const queryParts = [
11
30
  hosts.length ? hostsSearch : false,
12
31
  hostCollections.length ? hostCollectionsSearch : false,
@@ -1,4 +1,4 @@
1
- @import '~@theforeman/vendor/scss/variables';
1
+ @import 'foremanReact/common/variables';
2
2
 
3
3
  .tasks-labels-row {
4
4
  margin: 0;
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.0.3
4
+ version: 16.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-13 00:00:00.000000000 Z
10
+ date: 2025-07-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deface
@@ -187,7 +187,7 @@ files:
187
187
  - app/models/remote_execution_feature.rb
188
188
  - app/models/remote_execution_provider.rb
189
189
  - app/models/rex_mail_notification.rb
190
- - app/models/ssh_execution_provider.rb
190
+ - app/models/script_execution_provider.rb
191
191
  - app/models/target_remote_execution_proxy.rb
192
192
  - app/models/targeting.rb
193
193
  - app/models/targeting_host.rb
@@ -353,6 +353,7 @@ files:
353
353
  - extra/cockpit/settings.yml.example
354
354
  - lib/foreman_remote_execution.rb
355
355
  - lib/foreman_remote_execution/engine.rb
356
+ - lib/foreman_remote_execution/plugin.rb
356
357
  - lib/foreman_remote_execution/tasks/explain_proxy_selection.rake
357
358
  - lib/foreman_remote_execution/version.rb
358
359
  - lib/tasks/foreman_remote_execution_tasks.rake
@@ -419,17 +420,18 @@ files:
419
420
  - test/unit/renderer_scope_input_test.rb
420
421
  - test/unit/targeting_test.rb
421
422
  - test/unit/template_invocation_input_value_test.rb
423
+ - webpack/JobInvocationDetail/CheckboxesActions.js
424
+ - webpack/JobInvocationDetail/DropdownFilter.js
422
425
  - webpack/JobInvocationDetail/JobAdditionInfo.js
423
426
  - webpack/JobInvocationDetail/JobInvocationActions.js
424
427
  - webpack/JobInvocationDetail/JobInvocationConstants.js
425
428
  - webpack/JobInvocationDetail/JobInvocationDetail.scss
426
429
  - webpack/JobInvocationDetail/JobInvocationHostTable.js
427
- - webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js
428
430
  - webpack/JobInvocationDetail/JobInvocationOverview.js
429
431
  - webpack/JobInvocationDetail/JobInvocationSelectors.js
430
432
  - webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js
431
433
  - webpack/JobInvocationDetail/JobInvocationToolbarButtons.js
432
- - webpack/JobInvocationDetail/OpenAlInvocations.js
434
+ - webpack/JobInvocationDetail/OpenAllInvocationsModal.js
433
435
  - webpack/JobInvocationDetail/TemplateInvocation.js
434
436
  - webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js
435
437
  - webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js
@@ -438,8 +440,8 @@ files:
438
440
  - webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss
439
441
  - webpack/JobInvocationDetail/TemplateInvocationPage.js
440
442
  - webpack/JobInvocationDetail/__tests__/MainInformation.test.js
441
- - webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js
442
443
  - webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js
444
+ - webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js
443
445
  - webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
444
446
  - webpack/JobInvocationDetail/__tests__/fixtures.js
445
447
  - webpack/JobInvocationDetail/index.js
@@ -605,7 +607,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
605
607
  - !ruby/object:Gem::Version
606
608
  version: '0'
607
609
  requirements: []
608
- rubygems_version: 3.6.7
610
+ rubygems_version: 3.6.9
609
611
  specification_version: 4
610
612
  summary: A plugin bringing remote execution to the Foreman, completing the config
611
613
  management functionality with remote management functionality.
@@ -1,111 +0,0 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
3
- import {
4
- Alert,
5
- AlertActionCloseButton,
6
- Button,
7
- Modal,
8
- ModalVariant,
9
- } from '@patternfly/react-core';
10
- import { OutlinedWindowRestoreIcon } from '@patternfly/react-icons';
11
- import { translate as __ } from 'foremanReact/common/I18n';
12
- import { templateInvocationPageUrl } from './JobInvocationConstants';
13
-
14
- export const PopupAlert = ({ setShowAlert }) => (
15
- <Alert
16
- ouiaId="template-invocation-new-tab-popup-alert"
17
- variant="warning"
18
- actionClose={<AlertActionCloseButton onClose={() => setShowAlert(false)} />}
19
- title={__(
20
- 'Popups are blocked by your browser. Please allow popups for this site to open all invocations in new tabs.'
21
- )}
22
- />
23
- );
24
- export const OpenAlInvocations = ({ results, id, setShowAlert }) => {
25
- const [isModalOpen, setIsModalOpen] = React.useState(false);
26
- const handleModalToggle = () => {
27
- setIsModalOpen(!isModalOpen);
28
- };
29
-
30
- const openLink = url => {
31
- const newWin = window.open(url);
32
-
33
- if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
34
- setShowAlert(true);
35
- }
36
- };
37
- const OpenAllButton = () => (
38
- <Button
39
- variant="link"
40
- isInline
41
- aria-label="open all template invocations in a new tab"
42
- ouiaId="template-invocation-new-tab-button"
43
- onClick={() => {
44
- if (results.length <= 3) {
45
- results.forEach(result => {
46
- openLink(templateInvocationPageUrl(result.id, id), '_blank');
47
- });
48
- } else {
49
- handleModalToggle();
50
- }
51
- }}
52
- >
53
- <OutlinedWindowRestoreIcon />
54
- </Button>
55
- );
56
- const OpenAllModal = () => (
57
- <Modal
58
- ouiaId="template-invocation-new-tab-modal"
59
- title={__('Open all invocations in new tabs')}
60
- isOpen={isModalOpen}
61
- onClose={handleModalToggle}
62
- variant={ModalVariant.small}
63
- titleIconVariant="warning"
64
- actions={[
65
- <Button
66
- ouiaId="template-invocation-new-tab-modal-confirm"
67
- key="confirm"
68
- variant="primary"
69
- onClick={() => {
70
- results.forEach(result => {
71
- openLink(templateInvocationPageUrl(result.id, id), '_blank');
72
- });
73
- handleModalToggle();
74
- }}
75
- >
76
- {__('Open all in new tabs')}
77
- </Button>,
78
- <Button
79
- ouiaId="template-invocation-new-tab-modal-cancel"
80
- key="cancel"
81
- variant="link"
82
- onClick={handleModalToggle}
83
- >
84
- {__('Cancel')}
85
- </Button>,
86
- ]}
87
- >
88
- {__('Are you sure you want to open all invocations in new tabs?')}
89
- <br />
90
- {__('This will open a new tab for each invocation.')}
91
- <br />
92
- {__('The number of invocations is:')} <b>{results.length}</b>
93
- </Modal>
94
- );
95
- return (
96
- <>
97
- <OpenAllButton />
98
- <OpenAllModal />
99
- </>
100
- );
101
- };
102
-
103
- OpenAlInvocations.propTypes = {
104
- results: PropTypes.array.isRequired,
105
- id: PropTypes.string.isRequired,
106
- setShowAlert: PropTypes.func.isRequired,
107
- };
108
-
109
- PopupAlert.propTypes = {
110
- setShowAlert: PropTypes.func.isRequired,
111
- };
@@ -1,110 +0,0 @@
1
- import React from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
3
- import '@testing-library/jest-dom/extend-expect';
4
- import { OpenAlInvocations, PopupAlert } from '../OpenAlInvocations';
5
- import { templateInvocationPageUrl } from '../JobInvocationConstants';
6
-
7
- // Mock the templateInvocationPageUrl function
8
- jest.mock('../JobInvocationConstants', () => ({
9
- ...jest.requireActual('../JobInvocationConstants'),
10
- templateInvocationPageUrl: jest.fn((resultId, id) => `url/${resultId}/${id}`),
11
- }));
12
-
13
- describe('OpenAlInvocations', () => {
14
- const mockResults = [{ id: 1 }, { id: 2 }, { id: 3 }];
15
- const mockSetShowAlert = jest.fn();
16
- let windowSpy;
17
- const windowOpen = window.open;
18
-
19
- beforeAll(() => {
20
- window.open = () => {};
21
- });
22
- afterAll(() => {
23
- windowSpy.mockRestore();
24
- jest.clearAllMocks();
25
- window.open = windowOpen;
26
- });
27
-
28
- test('renders without crashing', () => {
29
- render(
30
- <OpenAlInvocations
31
- results={mockResults}
32
- id="test-id"
33
- setShowAlert={mockSetShowAlert}
34
- />
35
- );
36
- });
37
-
38
- test('opens links when results length is less than or equal to 3', () => {
39
- render(
40
- <OpenAlInvocations
41
- results={mockResults}
42
- id="test-id"
43
- setShowAlert={mockSetShowAlert}
44
- />
45
- );
46
-
47
- const button = screen.getByRole('button', { name: /open all/i });
48
- fireEvent.click(button);
49
-
50
- expect(templateInvocationPageUrl).toHaveBeenCalledTimes(mockResults.length);
51
- mockResults.forEach(result => {
52
- expect(templateInvocationPageUrl).toHaveBeenCalledWith(
53
- result.id,
54
- 'test-id'
55
- );
56
- });
57
- });
58
-
59
- test('shows modal when results length is greater than 3', () => {
60
- const largeResults = [...mockResults, { id: 4 }];
61
- render(
62
- <OpenAlInvocations
63
- results={largeResults}
64
- id="test-id"
65
- setShowAlert={mockSetShowAlert}
66
- />
67
- );
68
-
69
- const button = screen.getByRole('button', { name: /open all/i });
70
- fireEvent.click(button);
71
-
72
- expect(
73
- screen.getAllByText(/open all invocations in new tabs/i)
74
- ).toHaveLength(2);
75
- });
76
-
77
- test('shows alert when popups are blocked', () => {
78
- window.open = jest.fn().mockReturnValueOnce(null);
79
-
80
- render(
81
- <OpenAlInvocations
82
- results={mockResults}
83
- id="test-id"
84
- setShowAlert={mockSetShowAlert}
85
- />
86
- );
87
-
88
- const button = screen.getByRole('button', { name: /open all/i });
89
- fireEvent.click(button);
90
-
91
- expect(mockSetShowAlert).toHaveBeenCalledWith(true);
92
- });
93
- });
94
-
95
- describe('PopupAlert', () => {
96
- const mockSetShowAlert = jest.fn();
97
-
98
- test('renders without crashing', () => {
99
- render(<PopupAlert setShowAlert={mockSetShowAlert} />);
100
- });
101
-
102
- test('closes alert when close button is clicked', () => {
103
- render(<PopupAlert setShowAlert={mockSetShowAlert} />);
104
-
105
- const closeButton = screen.getByRole('button', { name: /close/i });
106
- fireEvent.click(closeButton);
107
-
108
- expect(mockSetShowAlert).toHaveBeenCalledWith(false);
109
- });
110
- });