foreman_remote_execution 16.0.4 → 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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +28 -1
  3. data/app/controllers/template_invocations_controller.rb +1 -1
  4. data/config/routes.rb +1 -0
  5. data/lib/foreman_remote_execution/engine.rb +9 -256
  6. data/lib/foreman_remote_execution/plugin.rb +246 -0
  7. data/lib/foreman_remote_execution/version.rb +1 -1
  8. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  9. data/test/unit/job_invocation_report_template_test.rb +6 -6
  10. data/webpack/JobInvocationDetail/CheckboxesActions.js +196 -0
  11. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -6
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  16. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +20 -27
  17. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  18. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  19. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  20. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  21. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  22. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +202 -0
  23. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  24. data/webpack/JobInvocationDetail/index.js +61 -30
  25. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  26. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  27. metadata +8 -6
  28. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  29. 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,21 +41,20 @@ 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
60
  const handleFilterChange = newFilter => {
@@ -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,26 +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
- onFilterChange={handleFilterChange}
123
- />
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>
124
139
  <Divider
125
140
  orientation={{
126
141
  default: 'vertical',
@@ -130,35 +145,51 @@ const JobInvocationDetailPage = ({
130
145
  className="job-overview"
131
146
  alignItems={{ default: 'alignItemsCenter' }}
132
147
  >
133
- <JobInvocationOverview
134
- data={items}
135
- isAlreadyStarted={isAlreadyStarted}
136
- formattedStartDate={formattedStartDate}
137
- onFilterChange={handleFilterChange}
138
- />
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>
139
161
  </Flex>
140
162
  </Flex>
141
163
  <PageSection
142
164
  variant={PageSectionVariants.light}
143
165
  className="job-additional-info"
144
166
  >
145
- {items.id !== undefined && <JobAdditionInfo data={items} />}
167
+ <SkeletonLoader
168
+ status={pageStatus}
169
+ skeletonProps={{ height: 150, width: '100%' }}
170
+ >
171
+ <JobAdditionInfo data={items} />
172
+ </SkeletonLoader>
146
173
  </PageSection>
147
174
  </PageLayout>
148
175
  <PageSection
149
176
  variant={PageSectionVariants.light}
150
177
  className="job-details-table-section table-section"
151
178
  >
152
- {items.id !== undefined && (
179
+ <SkeletonLoader
180
+ status={pageStatus}
181
+ skeletonProps={{ height: 400, width: '100%' }}
182
+ >
153
183
  <JobInvocationHostTable
154
184
  id={id}
155
185
  targeting={targeting}
186
+ failedCount={failed}
156
187
  finished={finished}
157
188
  autoRefresh={autoRefresh}
158
189
  initialFilter={selectedFilter}
159
190
  onFilterUpdate={handleFilterChange}
160
191
  />
161
- )}
192
+ </SkeletonLoader>
162
193
  </PageSection>
163
194
  </>
164
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.4
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-06-26 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
@@ -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
- });