foreman_remote_execution 16.5.3 → 16.6.4

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +0 -15
  3. data/lib/foreman_remote_execution/version.rb +1 -1
  4. data/webpack/JobInvocationDetail/JobInvocationActions.js +7 -0
  5. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +5 -2
  6. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +6 -3
  7. data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +1 -0
  8. data/webpack/JobInvocationDetail/__tests__/fixtures.js +9 -0
  9. data/webpack/JobWizard/JobWizard.js +7 -0
  10. data/webpack/JobWizard/JobWizardConstants.js +5 -0
  11. data/webpack/JobWizard/JobWizardSelectors.js +8 -0
  12. data/webpack/JobWizard/PermissionDenied.js +5 -2
  13. data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +53 -9
  14. data/webpack/JobWizard/__tests__/fixtures.js +24 -0
  15. data/webpack/JobWizard/autofill.js +57 -4
  16. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +7 -2
  17. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +31 -7
  18. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +95 -1
  19. data/webpack/JobWizard/steps/HostsAndInputs/index.js +5 -0
  20. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +2 -0
  21. data/webpack/JobWizard/submit.js +15 -2
  22. data/webpack/react_app/components/RegistrationExtension/RexPull.js +8 -5
  23. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +5 -1
  24. metadata +3 -33
  25. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -90
  26. data/webpack/__mocks__/foremanReact/components/SearchBar.js +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d4a947b587075d0d60662d365b36c3fab8006b958a363c39354b1f67f771c69
4
- data.tar.gz: 16bc29adc59c2a3bf1605902aece3e9d0f2bbd67749f4bc1a429ae52507165c8
3
+ metadata.gz: c9b021c23fae39af103f3ac3bdb0fd3e208b74fb8ad234f66f7770a5e4dfc78f
4
+ data.tar.gz: 4fd6498051640caae5cf6e4580d01106883a0f33d486b672a0d77ed49205e52c
5
5
  SHA512:
6
- metadata.gz: 3766b56254d55dd3f520ca16cca021d558362db4a34ccb15fb6833124a14f5900deefc78fc4125b4128c28f8eafd6d41f9c97d5526702a66ff32eb28665cde6f
7
- data.tar.gz: 1cb89677b91f21c07296af85bd5b18c993572965e701d8b8e55d0880ff5e5fc81b7d7a56a6a053a916309825489c3ec90d929092382f5dd1630fb44fd599a6fc
6
+ metadata.gz: bf4e127076278dca8d3ec7d4b86a625a89bb95c4a4c5cd728e9207e8ff3151f983965061498bbfd813c6d9d08f49ec59f9ba2bc06be113f8e721e8f04dfc0285
7
+ data.tar.gz: 0124e5ae5e0f484269adf05e9e9e3aa10e1262c4c6f635707dee035f217a5206565bc231fa65dee356609ce6e9db332d7c1faf96b8d1b7b3760483171997e5fd
data/Rakefile CHANGED
@@ -3,21 +3,6 @@ begin
3
3
  rescue LoadError
4
4
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
5
  end
6
- begin
7
- require 'rdoc/task'
8
- rescue LoadError
9
- require 'rdoc/rdoc'
10
- require 'rake/rdoctask'
11
- RDoc::Task = Rake::RDocTask
12
- end
13
-
14
- RDoc::Task.new(:rdoc) do |rdoc|
15
- rdoc.rdoc_dir = 'rdoc'
16
- rdoc.title = 'ForemanRemoteExecution'
17
- rdoc.options << '--line-numbers'
18
- rdoc.rdoc_files.include('README.rdoc')
19
- rdoc.rdoc_files.include('lib/**/*.rb')
20
- end
21
6
 
22
7
  APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
23
8
 
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '16.5.3'.freeze
2
+ VERSION = '16.6.4'.freeze
3
3
  end
@@ -24,6 +24,13 @@ export const getJobInvocation = url => dispatch => {
24
24
  handleError: () => {
25
25
  dispatch(stopInterval(JOB_INVOCATION_KEY));
26
26
  },
27
+ errorToast: ({ response }) =>
28
+ // eslint-disable-next-line camelcase
29
+ response?.data?.error?.full_messages?.[0] ||
30
+ // eslint-disable-next-line camelcase
31
+ response?.data?.error?.full_messages ||
32
+ response?.data?.error?.message ||
33
+ 'Error',
27
34
  }),
28
35
  1000
29
36
  );
@@ -1,12 +1,14 @@
1
1
  /* eslint-disable max-lines */
2
2
  /* eslint-disable camelcase */
3
3
  import {
4
+ Icon,
4
5
  EmptyState,
5
6
  EmptyStateBody,
6
7
  EmptyStateHeader,
7
8
  EmptyStateVariant,
8
9
  ToolbarItem,
9
10
  } from '@patternfly/react-core';
11
+ import { AddCircleOIcon } from '@patternfly/react-icons';
10
12
  import { ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table';
11
13
  import { useDispatch } from 'react-redux';
12
14
  import { APIActions } from 'foremanReact/redux/API';
@@ -22,7 +24,6 @@ import {
22
24
  import { getPageStats } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
23
25
  import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
24
26
  import { getControllerSearchProps } from 'foremanReact/constants';
25
- import { Icon } from 'patternfly-react';
26
27
  import PropTypes from 'prop-types';
27
28
  import React, {
28
29
  useEffect,
@@ -339,7 +340,9 @@ const JobInvocationHostTable = ({
339
340
  <Td colSpan={100}>
340
341
  <EmptyState variant={EmptyStateVariant.xl}>
341
342
  <span className="empty-state-icon">
342
- <Icon name="add-circle-o" type="pf" size="2x" />
343
+ <Icon size="xl" iconSize="xl">
344
+ <AddCircleOIcon name="add-circle-o" />
345
+ </Icon>
343
346
  </span>
344
347
  <EmptyStateHeader
345
348
  titleText={<>{__('No Results')}</>}
@@ -11,7 +11,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
11
11
  export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
12
12
  let lineCounter = 0;
13
13
  // eslint-disable-next-line no-control-regex
14
- const COLOR_PATTERN = /\x1b\[(\d+)m/g;
14
+ const COLOR_PATTERN = /\x1b\[[\d;]*m/g;
15
15
  const CONSOLE_COLOR = {
16
16
  '31': 'red',
17
17
  '32': 'lightgreen',
@@ -27,12 +27,15 @@ export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
27
27
  '95': 'violet',
28
28
  '96': 'turquoise',
29
29
  '0': 'default',
30
+ '39': 'default',
30
31
  };
31
32
 
32
33
  const colorizeLine = line => {
33
34
  line = line.replace(COLOR_PATTERN, seq => {
34
- const color = seq.match(/(\d+)m/)[1];
35
- return `{{{format color:${color}}}}`;
35
+ const codes = seq.match(/(\d+)/g) || [];
36
+ const lastColorCode =
37
+ [...codes].reverse().find(code_ => code_ in CONSOLE_COLOR) || '0';
38
+ return `{{{format color:${lastColorCode}}}}`;
36
39
  });
37
40
 
38
41
  let currentColor = 'default';
@@ -31,6 +31,7 @@ describe('OutputCodeBlock', () => {
31
31
  expect(screen.getByText('This is green text')).toHaveStyle(
32
32
  'color: lightgreen'
33
33
  );
34
+ expect(screen.getByText('Compound red text')).toHaveStyle('color: red');
34
35
  });
35
36
 
36
37
  test('displays no output message when filtered', () => {
@@ -196,6 +196,15 @@ export const jobInvocationOutput = [
196
196
  timestamp: 1733931148.2044532,
197
197
  },
198
198
 
199
+ {
200
+ id: 1960,
201
+ template_invocation_id: templateInvocationID,
202
+ timestamp: 1733931149.2044532,
203
+ meta: null,
204
+ external_id: '0',
205
+ output_type: 'stdout',
206
+ output: '\u001b[0;31mCompound red text\u001b[0m\n',
207
+ },
199
208
  {
200
209
  id: 1907,
201
210
  template_invocation_id: templateInvocationID,
@@ -62,10 +62,12 @@ export const JobWizard = ({ rerunData }) => {
62
62
  hostGroups: [],
63
63
  });
64
64
  const [hostsSearchQuery, setHostsSearchQuery] = useState('');
65
+ const [selectedBookmark, setSelectedBookmark] = useState(null);
65
66
  const [fills, setFills] = useState(
66
67
  rerunData
67
68
  ? {
68
69
  search: rerunData?.targeting?.search_query,
70
+ bookmark_id: rerunData?.targeting?.bookmark_id,
69
71
  ...rerunData.inputs,
70
72
  ...routerSearch,
71
73
  }
@@ -251,6 +253,7 @@ export const JobWizard = ({ rerunData }) => {
251
253
  setFills,
252
254
  setSelectedTargets,
253
255
  setHostsSearchQuery,
256
+ setSelectedBookmark,
254
257
  setJobTemplateID,
255
258
  setTemplateValues,
256
259
  setAdvancedValues,
@@ -298,6 +301,8 @@ export const JobWizard = ({ rerunData }) => {
298
301
  setSelected={setSelectedTargets}
299
302
  hostsSearchQuery={hostsSearchQuery}
300
303
  setHostsSearchQuery={setHostsSearchQuery}
304
+ selectedBookmark={selectedBookmark}
305
+ setSelectedBookmark={setSelectedBookmark}
301
306
  />
302
307
  ),
303
308
  canJumpTo: isTemplate,
@@ -474,6 +479,7 @@ export const JobWizard = ({ rerunData }) => {
474
479
  dispatch,
475
480
  selectedTargets,
476
481
  hostsSearchQuery,
482
+ selectedBookmark,
477
483
  location,
478
484
  organization,
479
485
  feature,
@@ -507,6 +513,7 @@ JobWizard.propTypes = {
507
513
  job_category: PropTypes.string,
508
514
  targeting: PropTypes.shape({
509
515
  search_query: PropTypes.string,
516
+ bookmark_id: PropTypes.number,
510
517
  targeting_type: PropTypes.string,
511
518
  randomized_ordering: PropTypes.bool,
512
519
  }),
@@ -1,5 +1,6 @@
1
1
  import { translate as __ } from 'foremanReact/common/I18n';
2
2
  import { foremanUrl } from 'foremanReact/common/helpers';
3
+ import { getControllerSearchProps } from 'foremanReact/constants';
3
4
 
4
5
  export const JOB_TEMPLATES = 'JOB_TEMPLATES';
5
6
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
@@ -61,6 +62,10 @@ export const hostMethods = {
61
62
 
62
63
  export const hostQuerySearchID = 'mainHostQuery';
63
64
  export const hostsController = 'hosts';
65
+ export const hostsSearchProps = getControllerSearchProps(
66
+ hostsController,
67
+ hostQuerySearchID
68
+ );
64
69
 
65
70
  export const dataName = {
66
71
  [HOSTS]: 'hosts',
@@ -9,6 +9,8 @@ import {
9
9
  } from 'foremanReact/redux/API/APISelectors';
10
10
  import { STATUS } from 'foremanReact/constants';
11
11
  import { selectRouterLocation } from 'foremanReact/routes/RouterSelector';
12
+ import { BOOKMARKS } from 'foremanReact/components/PF4/Bookmarks/BookmarksConstants';
13
+ import { selectBookmarksResults } from 'foremanReact/components/PF4/Bookmarks/BookmarksSelectors';
12
14
 
13
15
  import {
14
16
  JOB_TEMPLATES,
@@ -134,3 +136,9 @@ export const selectRouterSearch = state => {
134
136
  const { search } = selectRouterLocation(state) || {};
135
137
  return URI.parseQuery(search);
136
138
  };
139
+
140
+ const HOSTS_CONTROLLER = 'hosts';
141
+ const BOOKMARKS_HOSTS_KEY = `${BOOKMARKS}_${HOSTS_CONTROLLER.toUpperCase()}`;
142
+
143
+ export const selectHostBookmarks = state =>
144
+ selectBookmarksResults(state, BOOKMARKS_HOSTS_KEY, HOSTS_CONTROLLER);
@@ -1,8 +1,9 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { translate as __ } from 'foremanReact/common/I18n';
4
- import { Icon } from 'patternfly-react';
4
+ import { LockIcon } from '@patternfly/react-icons';
5
5
  import {
6
+ Icon,
6
7
  Button,
7
8
  EmptyState,
8
9
  EmptyStateVariant,
@@ -36,7 +37,9 @@ const PermissionDenied = ({ missingPermissions, setProceedAnyway }) => {
36
37
  return (
37
38
  <EmptyState variant={EmptyStateVariant.xl}>
38
39
  <span className="empty-state-icon">
39
- <Icon name="lock" type="fa" size="2x" />
40
+ <Icon size="xl" iconSize="xl">
41
+ <LockIcon name="lock" />
42
+ </Icon>
40
43
  </span>
41
44
  <EmptyStateHeader
42
45
  titleText={<>{__('Permission Denied')}</>}
@@ -3,21 +3,19 @@ import { Provider } from 'react-redux';
3
3
  import { render, fireEvent, screen, act } from '@testing-library/react';
4
4
  import { MockedProvider } from '@apollo/client/testing';
5
5
 
6
- import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
7
6
  import * as api from 'foremanReact/redux/API';
8
7
  import JobWizardPageRerun from '../JobWizardPageRerun';
9
8
  import * as selectors from '../JobWizardSelectors';
10
- import { testSetup, mockApi, gqlMock, jobInvocation } from './fixtures';
9
+ import {
10
+ testSetup,
11
+ mockApi,
12
+ gqlMock,
13
+ jobInvocation,
14
+ bookmarksList,
15
+ } from './fixtures';
11
16
 
12
17
  const store = testSetup(selectors, api);
13
18
  mockApi(api);
14
- jest.spyOn(APIHooks, 'useAPI');
15
- APIHooks.useAPI.mockImplementation((action, url) => {
16
- if (url === '/ui_job_wizard/job_invocation?id=57') {
17
- return { response: jobInvocation, status: 'RESOLVED' };
18
- }
19
- return {};
20
- });
21
19
 
22
20
  describe('Job wizard fill', () => {
23
21
  it('fill defaults into fields', async () => {
@@ -76,4 +74,50 @@ describe('Job wizard fill', () => {
76
74
  }).value
77
75
  ).toBe('6');
78
76
  });
77
+
78
+ it('fills bookmark on rerun when job used a bookmark', async () => {
79
+ const bookmark = bookmarksList[0];
80
+ const jobWithBookmark = {
81
+ ...jobInvocation,
82
+ job: {
83
+ ...jobInvocation.job,
84
+ targeting: {
85
+ ...jobInvocation.job.targeting,
86
+ bookmark_id: bookmark.id,
87
+ search_query: null,
88
+ },
89
+ },
90
+ };
91
+
92
+ selectors.selectRerunJobInvocationResponse.mockImplementation(
93
+ () => jobWithBookmark
94
+ );
95
+
96
+ render(
97
+ <MockedProvider mocks={gqlMock} addTypename={false}>
98
+ <Provider store={store}>
99
+ <JobWizardPageRerun
100
+ match={{
101
+ params: { id: '99' },
102
+ }}
103
+ />
104
+ </Provider>
105
+ </MockedProvider>
106
+ );
107
+
108
+ await act(async () => {
109
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
110
+ });
111
+
112
+ const hostMethodSelect = screen.getByRole('button', {
113
+ name: 'host method',
114
+ });
115
+ expect(hostMethodSelect.textContent).toContain('Search query');
116
+
117
+ expect(screen.queryAllByText(bookmark.query)).toHaveLength(1);
118
+
119
+ selectors.selectRerunJobInvocationResponse.mockImplementation(
120
+ () => jobInvocation
121
+ );
122
+ });
79
123
  });
@@ -116,6 +116,22 @@ export const jobTemplateResponse = {
116
116
  ],
117
117
  };
118
118
 
119
+ export const bookmarksList = [
120
+ { id: 19, name: 'my hosts', query: 'name ~ myhost', controller: 'hosts' },
121
+ {
122
+ id: 23,
123
+ name: 'active hosts',
124
+ query: 'last_report > "1 hour ago"',
125
+ controller: 'hosts',
126
+ },
127
+ {
128
+ id: 31,
129
+ name: 'dashboard default',
130
+ query: 'os = centos',
131
+ controller: 'dashboard',
132
+ },
133
+ ];
134
+
119
135
  export const jobCategories = ['Services', 'Ansible Commands', 'Puppet'];
120
136
 
121
137
  export const testSetup = (selectors, api) => {
@@ -176,6 +192,14 @@ export const testSetup = (selectors, api) => {
176
192
  subtotal: 3,
177
193
  },
178
194
  },
195
+ bookmarksPF4: {
196
+ hosts: {
197
+ results: bookmarksList,
198
+ },
199
+ },
200
+ API: {
201
+ BOOKMARKS_HOSTS: { status: 'RESOLVED', results: [] },
202
+ },
179
203
  });
180
204
  return store;
181
205
  };
@@ -1,7 +1,14 @@
1
- import { useEffect } from 'react';
2
- import { useDispatch } from 'react-redux';
1
+ import { useEffect, useState } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
3
  import { get } from 'foremanReact/redux/API';
4
- import { HOST_IDS, REX_FEATURE } from './JobWizardConstants';
4
+ import { getBookmarks } from 'foremanReact/components/PF4/Bookmarks/BookmarksActions';
5
+ import {
6
+ HOST_IDS,
7
+ REX_FEATURE,
8
+ hostsController,
9
+ hostsSearchProps,
10
+ } from './JobWizardConstants';
11
+ import { selectHostBookmarks } from './JobWizardSelectors';
5
12
  import './JobWizard.scss';
6
13
 
7
14
  export const useAutoFill = ({
@@ -9,11 +16,28 @@ export const useAutoFill = ({
9
16
  setFills,
10
17
  setSelectedTargets,
11
18
  setHostsSearchQuery,
19
+ setSelectedBookmark,
12
20
  setJobTemplateID,
13
21
  setTemplateValues,
14
22
  setAdvancedValues,
15
23
  }) => {
16
24
  const dispatch = useDispatch();
25
+ const bookmarks = useSelector(selectHostBookmarks);
26
+ const [pendingBookmarkId, setPendingBookmarkId] = useState(null);
27
+
28
+ useEffect(() => {
29
+ if (pendingBookmarkId === null || bookmarks.length === 0) return;
30
+ const bookmark = bookmarks.find(bm => bm.id === pendingBookmarkId);
31
+ if (bookmark) {
32
+ setSelectedBookmark({
33
+ id: bookmark.id,
34
+ name: bookmark.name,
35
+ query: bookmark.query,
36
+ });
37
+ setHostsSearchQuery(bookmark.query);
38
+ }
39
+ setPendingBookmarkId(null);
40
+ }, [bookmarks, pendingBookmarkId, setSelectedBookmark, setHostsSearchQuery]);
17
41
 
18
42
  useEffect(() => {
19
43
  if (Object.keys(fills).length) {
@@ -22,10 +46,12 @@ export const useAutoFill = ({
22
46
  search,
23
47
  feature,
24
48
  template_id: templateID,
49
+ bookmark_id: bookmarkId,
25
50
  ...rest
26
51
  } = { ...fills };
27
52
  setFills({});
28
53
  if (hostIds) {
54
+ setSelectedBookmark(null);
29
55
  const hostSearch = Array.isArray(hostIds)
30
56
  ? `id = ${hostIds.join(' or id = ')}`
31
57
  : `id = ${hostIds}`;
@@ -52,9 +78,34 @@ export const useAutoFill = ({
52
78
  })
53
79
  );
54
80
  }
55
- if ((search || search === '') && !hostIds?.length) {
81
+ if (bookmarkId) {
82
+ setSelectedTargets({
83
+ hosts: [],
84
+ hostCollections: [],
85
+ hostGroups: [],
86
+ });
87
+ const numericId = Number(bookmarkId);
88
+ if (bookmarks.length > 0) {
89
+ const bookmark = bookmarks.find(bm => bm.id === numericId);
90
+ if (bookmark) {
91
+ setSelectedBookmark({
92
+ id: bookmark.id,
93
+ name: bookmark.name,
94
+ query: bookmark.query,
95
+ });
96
+ setHostsSearchQuery(bookmark.query);
97
+ }
98
+ } else {
99
+ setPendingBookmarkId(numericId);
100
+ dispatch(
101
+ getBookmarks(hostsSearchProps.bookmarks.url, hostsController)
102
+ );
103
+ }
104
+ } else if ((search || search === '') && !hostIds?.length) {
56
105
  // replace an empty string search with a dummy search query to match all hosts
57
106
  // but only if search query was entered (based on presence of :search parameter)
107
+
108
+ setSelectedBookmark(null);
58
109
  const hostSearch = search === '' ? "name != ''" : search;
59
110
  setHostsSearchQuery(hostSearch);
60
111
  }
@@ -100,9 +151,11 @@ export const useAutoFill = ({
100
151
  setFills,
101
152
  setSelectedTargets,
102
153
  setHostsSearchQuery,
154
+ setSelectedBookmark,
103
155
  setJobTemplateID,
104
156
  setTemplateValues,
105
157
  setAdvancedValues,
106
158
  dispatch,
159
+ bookmarks,
107
160
  ]);
108
161
  };
@@ -125,7 +125,7 @@ describe('AdvancedFields', () => {
125
125
  const resourceSelectField = screen.getByLabelText(
126
126
  'adv resource select toggle'
127
127
  );
128
- const searchField = screen.getByPlaceholderText('Filter...');
128
+ const searchField = screen.getByPlaceholderText('Search');
129
129
  const dateField = screen.getByLabelText('adv date datepicker');
130
130
  const timeField = screen.getByLabelText('adv date timepicker');
131
131
 
@@ -403,6 +403,11 @@ describe('AdvancedFields', () => {
403
403
 
404
404
  jest.advanceTimersByTime(10000);
405
405
  });
406
- expect(newStore.getActions()).toMatchSnapshot('resource search');
406
+ const actions = newStore.getActions();
407
+ const resourceSearchAction = actions.filter(
408
+ action => action.key === 'ForemanTasksTask'
409
+ );
410
+ expect(resourceSearchAction).toHaveLength(2);
411
+ expect(String(resourceSearchAction[1].url)).toContain('name=some+search');
407
412
  });
408
413
  });
@@ -1,24 +1,47 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
3
4
  import SearchBar from 'foremanReact/components/SearchBar';
4
- import { getControllerSearchProps } from 'foremanReact/constants';
5
- import { hostsController, hostQuerySearchID } from '../../JobWizardConstants';
5
+ import { hostQuerySearchID, hostsSearchProps } from '../../JobWizardConstants';
6
+ import { selectHostBookmarks } from '../../JobWizardSelectors';
7
+
8
+ export const HostSearch = ({ value, setValue, onBookmarkMatch }) => {
9
+ const bookmarks = useSelector(selectHostBookmarks);
10
+
11
+ const handleSearchChange = search => {
12
+ setValue(search);
13
+ onBookmarkMatch(null);
14
+ };
15
+
16
+ const handleBookmarkSearch = query => {
17
+ const matched = bookmarks.find(
18
+ bookmark => bookmark.query && bookmark.query.trim() === query.trim()
19
+ );
20
+ if (matched) {
21
+ onBookmarkMatch({
22
+ id: matched.id,
23
+ name: matched.name,
24
+ query: matched.query,
25
+ });
26
+ } else {
27
+ onBookmarkMatch(null);
28
+ }
29
+ };
6
30
 
7
- export const HostSearch = ({ value, setValue }) => {
8
- const props = getControllerSearchProps(hostsController, hostQuerySearchID);
9
31
  return (
10
32
  <div className="foreman-search-field">
11
33
  <SearchBar
12
34
  data={{
13
- ...props,
35
+ ...hostsSearchProps,
14
36
  autocomplete: {
15
37
  id: hostQuerySearchID,
16
38
  url: '/hosts/auto_complete_search',
17
39
  searchQuery: value,
18
40
  },
19
41
  }}
20
- onSearch={null}
21
- onSearchChange={search => setValue(search)}
42
+ onSearch={handleBookmarkSearch}
43
+ onSearchChange={handleSearchChange}
44
+ bookmarksPosition="right"
22
45
  />
23
46
  </div>
24
47
  );
@@ -27,4 +50,5 @@ export const HostSearch = ({ value, setValue }) => {
27
50
  HostSearch.propTypes = {
28
51
  value: PropTypes.string.isRequired,
29
52
  setValue: PropTypes.func.isRequired,
53
+ onBookmarkMatch: PropTypes.func.isRequired,
30
54
  };
@@ -6,7 +6,12 @@ import * as api from 'foremanReact/redux/API';
6
6
  import * as routerSelectors from 'foremanReact/routes/RouterSelector';
7
7
  import { JobWizard } from '../../../JobWizard';
8
8
  import * as selectors from '../../../JobWizardSelectors';
9
- import { testSetup, mockApi, gqlMock } from '../../../__tests__/fixtures';
9
+ import {
10
+ testSetup,
11
+ mockApi,
12
+ gqlMock,
13
+ bookmarksList,
14
+ } from '../../../__tests__/fixtures';
10
15
 
11
16
  const store = testSetup(selectors, api);
12
17
  mockApi(api);
@@ -183,6 +188,95 @@ describe('Hosts', () => {
183
188
  expect(screen.queryAllByText('os=gnome')).toHaveLength(1);
184
189
  });
185
190
 
191
+ it('submits bookmark_id when search matches a bookmark', async () => {
192
+ const bookmark = bookmarksList[0];
193
+ routerSelectors.selectRouterLocation.mockImplementation(() => ({
194
+ search: '',
195
+ }));
196
+ const bookmarkStore = testSetup(selectors, api);
197
+ mockApi(api);
198
+
199
+ render(
200
+ <MockedProvider mocks={gqlMock} addTypename={false}>
201
+ <Provider store={bookmarkStore}>
202
+ <JobWizard />
203
+ </Provider>
204
+ </MockedProvider>
205
+ );
206
+
207
+ await act(async () => {
208
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
209
+ });
210
+ await act(async () => {
211
+ fireEvent.click(screen.getByRole('button', { name: 'host method' }));
212
+ });
213
+ await act(async () => {
214
+ fireEvent.click(screen.getByText('Search query'));
215
+ });
216
+
217
+ await act(async () => {
218
+ fireEvent.click(
219
+ screen.getByRole('button', { name: 'bookmarks dropdown toggle' })
220
+ );
221
+ });
222
+ await act(async () => {
223
+ fireEvent.click(screen.getByText(bookmark.name));
224
+ });
225
+
226
+ await act(async () => {
227
+ fireEvent.click(screen.getByText('Review details'));
228
+ });
229
+ await act(async () => {
230
+ fireEvent.click(screen.getByText('Submit'));
231
+ });
232
+
233
+ const submitAction = bookmarkStore
234
+ .getActions()
235
+ .find(action => action?.key === 'JOB_INVOCATION');
236
+ expect(submitAction).toBeDefined();
237
+ const { job_invocation: invocation } = submitAction.params;
238
+ expect(invocation.bookmark_id).toBe(bookmark.id);
239
+ expect(invocation.search_query).toBeNull();
240
+ });
241
+
242
+ it('does not submit bookmark_id when search does not match a bookmark', async () => {
243
+ const customQuery = 'some custom query';
244
+ routerSelectors.selectRouterLocation.mockImplementation(() => ({
245
+ search: `search=${encodeURIComponent(customQuery)}`,
246
+ }));
247
+ const customStore = testSetup(selectors, api);
248
+ mockApi(api);
249
+
250
+ render(
251
+ <MockedProvider mocks={gqlMock} addTypename={false}>
252
+ <Provider store={customStore}>
253
+ <JobWizard />
254
+ </Provider>
255
+ </MockedProvider>
256
+ );
257
+
258
+ await act(async () => {
259
+ fireEvent.click(screen.getByText('Target hosts and inputs'));
260
+ });
261
+
262
+ expect(screen.queryAllByText(customQuery)).toHaveLength(1);
263
+
264
+ await act(async () => {
265
+ fireEvent.click(screen.getByText('Review details'));
266
+ });
267
+ await act(async () => {
268
+ fireEvent.click(screen.getByText('Submit'));
269
+ });
270
+
271
+ const submitAction = customStore
272
+ .getActions()
273
+ .find(action => action?.key === 'JOB_INVOCATION');
274
+ expect(submitAction).toBeDefined();
275
+ const { job_invocation: invocation } = submitAction.params;
276
+ expect(invocation.bookmark_id).toBeNull();
277
+ expect(invocation.search_query).toBe(customQuery);
278
+ });
279
+
186
280
  it('input fill from url', async () => {
187
281
  const inputText = 'test text';
188
282
  const advancedInputText = 'test adv text';
@@ -51,6 +51,7 @@ const HostsAndInputs = ({
51
51
  setSelected,
52
52
  hostsSearchQuery,
53
53
  setHostsSearchQuery,
54
+ setSelectedBookmark,
54
55
  }) => {
55
56
  const defaultHostMethod = hostsSearchQuery.length
56
57
  ? hostMethods.searchQuery
@@ -132,6 +133,7 @@ const HostsAndInputs = ({
132
133
 
133
134
  const clearSearch = () => {
134
135
  setHostsSearchQuery('');
136
+ setSelectedBookmark(null);
135
137
  };
136
138
  const [errorText, setErrorText] = useState(
137
139
  __('Please select at least one host')
@@ -156,6 +158,7 @@ const HostsAndInputs = ({
156
158
  className="target-method-select"
157
159
  toggleIcon={<FilterIcon />}
158
160
  fieldId="host_methods"
161
+ toggleAriaLabel={__('host method')}
159
162
  options={Object.values(hostMethods).filter(method => {
160
163
  if (method === hostMethods.hostCollections && !withKatello) {
161
164
  return false;
@@ -186,6 +189,7 @@ const HostsAndInputs = ({
186
189
  <HostSearch
187
190
  setValue={setHostsSearchQuery}
188
191
  value={hostsSearchQuery}
192
+ onBookmarkMatch={setSelectedBookmark}
189
193
  />
190
194
  )}
191
195
  {hostMethod === hostMethods.hosts && (
@@ -286,6 +290,7 @@ HostsAndInputs.propTypes = {
286
290
  setSelected: PropTypes.func.isRequired,
287
291
  hostsSearchQuery: PropTypes.string.isRequired,
288
292
  setHostsSearchQuery: PropTypes.func.isRequired,
293
+ setSelectedBookmark: PropTypes.func.isRequired,
289
294
  };
290
295
 
291
296
  export default HostsAndInputs;
@@ -55,6 +55,8 @@ const store = mockStore({
55
55
  subtotal: 3,
56
56
  },
57
57
  },
58
+ bookmarksPF4: {},
59
+ API: {},
58
60
  });
59
61
  jest.useFakeTimers();
60
62
 
@@ -2,6 +2,14 @@ import { post } from 'foremanReact/redux/API';
2
2
  import { repeatTypes, JOB_INVOCATION } from './JobWizardConstants';
3
3
  import { buildHostQuery } from './steps/HostsAndInputs/buildHostQuery';
4
4
 
5
+ const hasExplicitTargets = selectedTargets =>
6
+ selectedTargets.hosts.length > 0 ||
7
+ selectedTargets.hostCollections.length > 0 ||
8
+ selectedTargets.hostGroups.length > 0;
9
+
10
+ const shouldSendBookmark = (selectedBookmark, selectedTargets) =>
11
+ selectedBookmark && !hasExplicitTargets(selectedTargets);
12
+
5
13
  export const submit = ({
6
14
  jobTemplateID,
7
15
  templateValues,
@@ -9,6 +17,7 @@ export const submit = ({
9
17
  scheduleValue,
10
18
  selectedTargets,
11
19
  hostsSearchQuery,
20
+ selectedBookmark,
12
21
  location,
13
22
  organization,
14
23
  feature,
@@ -110,8 +119,12 @@ export const submit = ({
110
119
  concurrency_control: {
111
120
  concurrency_level: concurrencyLevel,
112
121
  },
113
- bookmark_id: null,
114
- search_query: buildHostQuery(selectedTargets, hostsSearchQuery),
122
+ bookmark_id: shouldSendBookmark(selectedBookmark, selectedTargets)
123
+ ? selectedBookmark.id
124
+ : null,
125
+ search_query: shouldSendBookmark(selectedBookmark, selectedTargets)
126
+ ? null
127
+ : buildHostQuery(selectedTargets, hostsSearchQuery),
115
128
  description_format: description,
116
129
  execution_timeout_interval: timeoutToKill,
117
130
  feature,
@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
4
4
 
5
5
  import { translate as __ } from 'foremanReact/common/I18n';
6
6
  import LabelIcon from 'foremanReact/components/common/LabelIcon';
7
- import { Alert } from 'patternfly-react';
8
-
9
7
  import {
8
+ Alert,
10
9
  FormGroup,
11
10
  FormSelectOption,
12
11
  FormSelect,
@@ -26,11 +25,15 @@ const options = (value = '') => {
26
25
  };
27
26
 
28
27
  const pullWarning = (
29
- <Alert type="info" isInline style={{ marginTop: '10px' }}>
30
- {__(
28
+ <Alert
29
+ ouiaId="overrideAlert"
30
+ variant="info"
31
+ isInline
32
+ title={__(
31
33
  'Please make sure that the Smart Proxy is configured correctly for the Pull provider.'
32
34
  )}
33
- </Alert>
35
+ style={{ marginTop: '10px' }}
36
+ />
34
37
  );
35
38
 
36
39
  function showPullWarning(valueFromParam, value) {
@@ -14,6 +14,7 @@ exports[`TargetingHostsPage renders 1`] = `
14
14
  md={6}
15
15
  >
16
16
  <SearchBar
17
+ bookmarksPosition="left"
17
18
  data={
18
19
  Object {
19
20
  "autocomplete": Object {
@@ -31,9 +32,12 @@ exports[`TargetingHostsPage renders 1`] = `
31
32
  "controller": "hosts",
32
33
  }
33
34
  }
35
+ initialQuery=""
36
+ name={null}
34
37
  onBookmarkClick={[Function]}
35
- onChange={[Function]}
36
38
  onSearch={[Function]}
39
+ onSearchChange={[Function]}
40
+ restrictedSearchQuery={[Function]}
37
41
  />
38
42
  </Col>
39
43
  </Row>
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.5.3
4
+ version: 16.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-31 00:00:00.000000000 Z
10
+ date: 2026-06-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deface
@@ -37,34 +37,6 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: 8.3.0
40
- - !ruby/object:Gem::Dependency
41
- name: factory_bot_rails
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: 4.8.0
47
- type: :development
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: 4.8.0
54
- - !ruby/object:Gem::Dependency
55
- name: rdoc
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
40
  description: A plugin bringing remote execution to the Foreman, completing the config
69
41
  management functionality with remote management functionality.
70
42
  email:
@@ -448,7 +420,6 @@ files:
448
420
  - webpack/JobWizard/steps/AdvancedFields/DescriptionField.js
449
421
  - webpack/JobWizard/steps/AdvancedFields/Fields.js
450
422
  - webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js
451
- - webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap
452
423
  - webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js
453
424
  - webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js
454
425
  - webpack/JobWizard/steps/CategoryAndTemplate/index.js
@@ -503,7 +474,6 @@ files:
503
474
  - webpack/__mocks__/foremanReact/components/Head/index.js
504
475
  - webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js
505
476
  - webpack/__mocks__/foremanReact/components/Pagination.js
506
- - webpack/__mocks__/foremanReact/components/SearchBar.js
507
477
  - webpack/__mocks__/foremanReact/components/ToastsList/index.js
508
478
  - webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js
509
479
  - webpack/__mocks__/foremanReact/constants.js
@@ -589,7 +559,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
589
559
  - !ruby/object:Gem::Version
590
560
  version: '0'
591
561
  requirements: []
592
- rubygems_version: 4.0.6
562
+ rubygems_version: 4.0.10
593
563
  specification_version: 4
594
564
  summary: A plugin bringing remote execution to the Foreman, completing the config
595
565
  management functionality with remote management functionality.
@@ -1,90 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`AdvancedFields search resources action: resource search 1`] = `
4
- Array [
5
- Object {
6
- "key": "JOB_CATEGORIES",
7
- "type": "get",
8
- "url": "/ui_job_wizard/categories",
9
- },
10
- Object {
11
- "key": "HOST_IDS",
12
- "params": Object {
13
- "search": "id = 105 or id = 37",
14
- },
15
- "type": "get",
16
- "url": "/api/hosts",
17
- },
18
- Object {
19
- "key": "JOB_TEMPLATES",
20
- "type": "get",
21
- "url": URI {
22
- "_deferred_build": true,
23
- "_parts": Object {
24
- "duplicateQueryParameters": false,
25
- "escapeQuerySpace": true,
26
- "fragment": null,
27
- "hostname": null,
28
- "password": null,
29
- "path": "foreman/api/v2/job_templates",
30
- "port": null,
31
- "preventInvalidHostname": false,
32
- "protocol": null,
33
- "query": "search=job_category%3D%22Ansible+Commands%22&per_page=all",
34
- "urn": null,
35
- "username": null,
36
- },
37
- "_string": "",
38
- },
39
- },
40
- Object {
41
- "key": "JOB_TEMPLATE",
42
- "type": "get",
43
- "url": "/ui_job_wizard/template/178",
44
- },
45
- Object {
46
- "key": "ForemanTasksTask",
47
- "type": "get",
48
- "url": URI {
49
- "_deferred_build": true,
50
- "_parts": Object {
51
- "duplicateQueryParameters": false,
52
- "escapeQuerySpace": true,
53
- "fragment": null,
54
- "hostname": null,
55
- "password": null,
56
- "path": "/ui_job_wizard/resources",
57
- "port": null,
58
- "preventInvalidHostname": false,
59
- "protocol": null,
60
- "query": "resource=ForemanTasks%3A%3ATask",
61
- "urn": null,
62
- "username": null,
63
- },
64
- "_string": "",
65
- },
66
- },
67
- Object {
68
- "key": "ForemanTasksTask",
69
- "type": "get",
70
- "url": URI {
71
- "_deferred_build": true,
72
- "_parts": Object {
73
- "duplicateQueryParameters": false,
74
- "escapeQuerySpace": true,
75
- "fragment": null,
76
- "hostname": null,
77
- "password": null,
78
- "path": "/ui_job_wizard/resources",
79
- "port": null,
80
- "preventInvalidHostname": false,
81
- "protocol": null,
82
- "query": "resource=ForemanTasks%3A%3ATask&name=some+search",
83
- "urn": null,
84
- "username": null,
85
- },
86
- "_string": "",
87
- },
88
- },
89
- ]
90
- `;
@@ -1,19 +0,0 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
3
-
4
- const SearchBar = ({ onChange }) => (
5
- <input
6
- className="foreman-search"
7
- onChange={onChange}
8
- placeholder="Filter..."
9
- />
10
- );
11
- export default SearchBar;
12
-
13
- SearchBar.propTypes = {
14
- onChange: PropTypes.func,
15
- };
16
-
17
- SearchBar.defaultProps = {
18
- onChange: () => null,
19
- };