foreman-tasks 2.0.1 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/app/controllers/foreman_tasks/api/tasks_controller.rb +4 -0
  4. data/app/lib/actions/proxy_action.rb +1 -1
  5. data/app/models/foreman_tasks/task/dynflow_task.rb +2 -1
  6. data/app/views/foreman_tasks/api/tasks/show.json.rabl +2 -0
  7. data/db/migrate/20200611090846_add_task_lock_index_on_resource_type_and_task_id.rb +3 -3
  8. data/lib/foreman_tasks/tasks/cleanup.rake +2 -2
  9. data/lib/foreman_tasks/tasks/dynflow.rake +6 -0
  10. data/lib/foreman_tasks/tasks/export_tasks.rake +1 -1
  11. data/lib/foreman_tasks/version.rb +1 -1
  12. data/package.json +1 -0
  13. data/script/npm_link_foreman_js.sh +26 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +24 -5
  15. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +1 -0
  16. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Task.test.js.snap +20 -3
  17. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +3 -14
  18. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsActions.js +6 -2
  19. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +4 -1
  20. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +1 -0
  21. data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
  22. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/ConfirmModalSelectors.js +1 -0
  23. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/ConfirmModalSelectors.test.js +1 -0
  24. data/webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/__snapshots__/ConfirmModalSelectors.test.js.snap +2 -0
  25. data/webpack/ForemanTasks/Components/TasksTable/TasksBulkActions.js +24 -7
  26. data/webpack/ForemanTasks/Components/TasksTable/TasksTableActions.js +3 -3
  27. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +6 -2
  28. data/webpack/ForemanTasks/Components/TasksTable/TasksTableSelectors.js +1 -0
  29. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksBulkActions.test.js +13 -0
  30. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js +1 -0
  31. data/webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTablePage.test.js +2 -1
  32. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap +1 -0
  33. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksBulkActions.test.js.snap +48 -0
  34. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap +1 -0
  35. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +39 -5
  36. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionCellFormatter.test.js.snap +1 -0
  37. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionCellFormatter.test.js.snap +2 -0
  38. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionCellFormatter.test.js +1 -1
  39. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionCellFormatter.test.js +1 -1
  40. data/webpack/ForemanTasks/Components/TasksTable/formatters/actionCellFormatter.js +10 -7
  41. data/webpack/ForemanTasks/Components/TasksTable/formatters/selectionCellFormatter.js +7 -0
  42. data/webpack/ForemanTasks/Components/common/ActionButtons/ActionButton.js +39 -31
  43. data/webpack/ForemanTasks/Components/common/ActionButtons/ActionButton.test.js +17 -8
  44. data/webpack/ForemanTasks/Components/common/ActionButtons/__snapshots__/ActionButton.test.js.snap +8 -0
  45. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1026fe8670d8a41977697a9ff2bd4f04758dca2f18a9bac806dea4856cb4478
4
- data.tar.gz: 8ec9ddbfb5a444a525cc27d178ade81c30e5c7c8b4fe0e577aba7d780ed3b37d
3
+ metadata.gz: 62a10715bd5b194c1cb750c2f100b60a2f3bca155e0df6d303b284cf157a87d9
4
+ data.tar.gz: fd24e269912731dd0e2cee07e99e694dbf98547cab3a3d3dd89963f87d2dc3b9
5
5
  SHA512:
6
- metadata.gz: 044b43805dcfd80a9607154d0a8be54b54b8b054923727d79f699dec5a4f355fe84d1533f5049ecc47d65efd3313447a070cc9bd01bea47bbd639f800847375e
7
- data.tar.gz: edd5a4f741c2e55eb7d695011e8dc7a017214e0a5b43027035352095ad8132d8ea93a75c5991a97c5ffa05645f970f6d878a5d33ba6f6c75ed7ece1b16af4237
6
+ metadata.gz: 011ae333df38537c17b1593a06f41828aa600e45d653c4231644b7d86e0a0ea92016de9d36abb067c432eb4e04466bec72e9ceb9f6ad87be3142b9896d7f18e4
7
+ data.tar.gz: 0c06ac795bc95787682262e42e8330b5b1e10d0923bc68581ba19623be01cac63ab6bab70d67a168a51c61b2f34f383f19c95276eee9fd564fb7a8c32edec16f
@@ -1,4 +1,6 @@
1
1
  language: node_js
2
+ cache:
3
+ npm: false
2
4
  node_js:
3
5
  - '10'
4
6
  - '12'
@@ -315,6 +315,10 @@ module ForemanTasks
315
315
  @resource_scope ||= ForemanTasks::Task.authorized("#{action_permission}_foreman_tasks")
316
316
  end
317
317
 
318
+ def controller_permission
319
+ 'foreman_tasks'
320
+ end
321
+
318
322
  def action_permission
319
323
  case params[:action]
320
324
  when 'bulk_search', 'summary', 'details', 'sub_tasks'
@@ -257,7 +257,7 @@ module Actions
257
257
  if failed_proxy_tasks.count < options[:retry_count]
258
258
  suspend do |suspended_action|
259
259
  @world.clock.ping suspended_action,
260
- Time.zone.now + options[:retry_interval],
260
+ Time.now.getlocal + options[:retry_interval],
261
261
  event
262
262
  end
263
263
  else
@@ -46,7 +46,8 @@ module ForemanTasks
46
46
  end
47
47
 
48
48
  def progress
49
- execution_plan.try(:progress) || 0
49
+ progress_raw = execution_plan.try(:progress) || 0
50
+ progress_raw.round(2)
50
51
  end
51
52
 
52
53
  def execution_plan(silence_exception = true)
@@ -1,5 +1,7 @@
1
1
  object @task if @task
2
2
 
3
+ extends 'api/v2/layouts/permissions'
4
+
3
5
  attributes :id, :label, :pending, :action
4
6
  attributes :username, :started_at, :ended_at, :state, :result, :progress
5
7
  attributes :input, :output, :humanized, :cli_example, :start_at
@@ -2,8 +2,8 @@ class AddTaskLockIndexOnResourceTypeAndTaskId < ActiveRecord::Migration[6.0]
2
2
  def change
3
3
  add_index :foreman_tasks_locks, [:task_id, :resource_type, :resource_id], name: 'index_tasks_locks_on_task_id_resource_type_and_resource_id'
4
4
  # These indexes are not needed as they can be gained from partial index lookups
5
- remove_index :foreman_tasks_locks, :task_id
6
- remove_index :foreman_tasks_locks, :name
7
- remove_index :foreman_tasks_locks, :resource_type
5
+ [:task_id, :name, :resource_type].each do |index|
6
+ remove_index :foreman_tasks_locks, index if index_exists?(:foreman_tasks_locks, index)
7
+ end
8
8
  end
9
9
  end
@@ -14,7 +14,7 @@ namespace :foreman_tasks do
14
14
  If TASK_SEARCH is set then AFTER, STATES can be set and it's used for cleanup. If TASK_SEARCH is not set then
15
15
  the cleanup respects the configuration file and setting AFTER or STATES will throw exception.
16
16
  DESC
17
- task :run => 'environment' do
17
+ task :run => ['environment', 'dynflow:client'] do
18
18
  options = {}
19
19
 
20
20
  options[:filter] = ENV['TASK_SEARCH'] if ENV['TASK_SEARCH']
@@ -38,7 +38,7 @@ namespace :foreman_tasks do
38
38
  end
39
39
 
40
40
  desc 'Show the current configuration for auto-cleanup'
41
- task :config => 'environment' do
41
+ task :config => ['environment', 'dynflow:client'] do
42
42
  if ForemanTasks::Cleaner.cleanup_settings[:after]
43
43
  puts _('The tasks will be deleted after %{after}') % { :after => ForemanTasks::Cleaner.cleanup_settings[:after] }
44
44
  else
@@ -0,0 +1,6 @@
1
+ namespace :dynflow do
2
+ task :client do
3
+ ::ForemanTasks.dynflow.config.remote = true
4
+ ::ForemanTasks.dynflow.initialize!
5
+ end
6
+ end
@@ -19,7 +19,7 @@ namespace :foreman_tasks do
19
19
  all unsuccessful tasks in the past 60 days. The default TASK_FORMAT is html
20
20
  which requires a tar.gz file extension.
21
21
  DESC
22
- task :export_tasks => :environment do
22
+ task :export_tasks => [:environment, 'dynflow:client'] do
23
23
  deprecated_options = { :tasks => 'TASK_SEARCH',
24
24
  :days => 'TASK_DAYS',
25
25
  :export => 'TASK_FILE' }
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '2.0.1'.freeze
2
+ VERSION = '2.0.2'.freeze
3
3
  end
@@ -4,6 +4,7 @@
4
4
  "description": "Foreman Tasks =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
+ "foreman-js:link": "./script/npm_link_foreman_js.sh",
7
8
  "lint": "tfm-lint --plugin -d /webpack",
8
9
  "test": "tfm-test --plugin",
9
10
  "test:watch": "tfm-test --plugin --watchAll",
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+
3
+ # This script replace the npm installation of `foreman-js`
4
+ # with your local version. Usefull when developing `foreman-js`
5
+ # Read more about foreman-js: https://github.com/theforeman/foreman-js
6
+ #
7
+ # This script designed to run using `npm run foreman-js:link` in foreman root
8
+
9
+ set -e
10
+
11
+ if [[ -z "${FOREMAN_JS_LOCATION}" ]]; then # FOREMAN_JS_LOCATION is empty
12
+ FOREMAN_JS_LOCATION="../foreman-js"
13
+ echo "FOREMAN_JS_LOCATION is not defined, using \"${FOREMAN_JS_LOCATION}\" instead"
14
+ elif [ ! -d "${FOREMAN_JS_LOCATION}" ]; then
15
+ echo "Can't find folder ${FOREMAN_JS_LOCATION}"
16
+ exit 1
17
+ fi
18
+
19
+ FOREMAN_JS_LOCATION="../${FOREMAN_JS_LOCATION}"
20
+ FOREMAN_JS_PACKAGES_LOCATION="${FOREMAN_JS_LOCATION}/packages"
21
+ FOREMAN_JS_INSTALL_LOCATION="./node_modules/@theforeman"
22
+
23
+ set -x
24
+
25
+ rm -rf $FOREMAN_JS_INSTALL_LOCATION
26
+ ln -s $FOREMAN_JS_PACKAGES_LOCATION $FOREMAN_JS_INSTALL_LOCATION
@@ -34,6 +34,7 @@ const Task = props => {
34
34
  action,
35
35
  dynflowEnableConsole,
36
36
  taskProgressToggle,
37
+ canEdit,
37
38
  } = props;
38
39
  const forceUnlock = () => {
39
40
  if (!taskReload) {
@@ -47,6 +48,12 @@ const Task = props => {
47
48
  }
48
49
  unlockTaskRequest(id, action);
49
50
  };
51
+ const editActionsTitle = canEdit
52
+ ? undefined
53
+ : __('You do not have permission');
54
+ const dynflowTitle = dynflowEnableConsole
55
+ ? undefined
56
+ : `dynflow_enable_console ${__('Setting is off')}`;
50
57
  return (
51
58
  <React.Fragment>
52
59
  <UnlockModal onClick={unlock} />
@@ -74,12 +81,16 @@ const Task = props => {
74
81
  rel="noopener noreferrer"
75
82
  target="_blank"
76
83
  >
77
- {__('Dynflow console')}
84
+ <span title={dynflowTitle} data-original-title={dynflowTitle}>
85
+ {__('Dynflow console')}
86
+ </span>
78
87
  </Button>
79
88
  <Button
80
89
  className="resume-button"
81
90
  bsSize="small"
82
- disabled={!resumable}
91
+ title={editActionsTitle}
92
+ data-original-title={editActionsTitle}
93
+ disabled={!canEdit || !resumable}
83
94
  onClick={() => {
84
95
  if (!taskReload) {
85
96
  taskProgressToggle();
@@ -92,7 +103,9 @@ const Task = props => {
92
103
  <Button
93
104
  className="cancel-button"
94
105
  bsSize="small"
95
- disabled={!cancellable}
106
+ title={editActionsTitle}
107
+ data-original-title={editActionsTitle}
108
+ disabled={!canEdit || !cancellable}
96
109
  onClick={() => {
97
110
  if (!taskReload) {
98
111
  taskProgressToggle();
@@ -123,16 +136,20 @@ const Task = props => {
123
136
  <Button
124
137
  className="unlock-button"
125
138
  bsSize="small"
126
- disabled={state !== 'paused'}
139
+ disabled={!canEdit || state !== 'paused'}
127
140
  onClick={unlockModalActions.setModalOpen}
141
+ title={editActionsTitle}
142
+ data-original-title={editActionsTitle}
128
143
  >
129
144
  {__('Unlock')}
130
145
  </Button>
131
146
  <Button
132
147
  className="force-unlock-button"
133
148
  bsSize="small"
134
- disabled={state === 'stopped'}
149
+ disabled={!canEdit || state === 'stopped'}
135
150
  onClick={forceUnlockModalActions.setModalOpen}
151
+ title={editActionsTitle}
152
+ data-original-title={editActionsTitle}
136
153
  >
137
154
  {__('Force Unlock')}
138
155
  </Button>
@@ -160,6 +177,7 @@ Task.propTypes = {
160
177
  cancelTaskRequest: PropTypes.func,
161
178
  resumeTaskRequest: PropTypes.func,
162
179
  dynflowEnableConsole: PropTypes.bool,
180
+ canEdit: PropTypes.bool,
163
181
  };
164
182
 
165
183
  Task.defaultProps = {
@@ -177,6 +195,7 @@ Task.defaultProps = {
177
195
  cancelTaskRequest: () => null,
178
196
  resumeTaskRequest: () => null,
179
197
  dynflowEnableConsole: false,
198
+ canEdit: false,
180
199
  };
181
200
 
182
201
  export default Task;
@@ -20,6 +20,7 @@ const fixtures = {
20
20
  dynflowEnableConsole: true,
21
21
  parentTask: 'parent-id',
22
22
  taskReload: true,
23
+ canEdit: true,
23
24
  },
24
25
  };
25
26
 
@@ -51,7 +51,9 @@ exports[`Task rendering render with some Props 1`] = `
51
51
  rel="noopener noreferrer"
52
52
  target="_blank"
53
53
  >
54
- Dynflow console
54
+ <span>
55
+ Dynflow console
56
+ </span>
55
57
  </Button>
56
58
  <Button
57
59
  active={false}
@@ -129,6 +131,7 @@ exports[`Task rendering render with some Props 1`] = `
129
131
  </Row>
130
132
  <TaskInfo
131
133
  action=""
134
+ canEdit={true}
132
135
  cancelTaskRequest={[Function]}
133
136
  cancellable={false}
134
137
  dynflowEnableConsole={true}
@@ -210,7 +213,12 @@ exports[`Task rendering render without Props 1`] = `
210
213
  rel="noopener noreferrer"
211
214
  target="_blank"
212
215
  >
213
- Dynflow console
216
+ <span
217
+ data-original-title="dynflow_enable_console Setting is off"
218
+ title="dynflow_enable_console Setting is off"
219
+ >
220
+ Dynflow console
221
+ </span>
214
222
  </Button>
215
223
  <Button
216
224
  active={false}
@@ -219,8 +227,10 @@ exports[`Task rendering render without Props 1`] = `
219
227
  bsSize="small"
220
228
  bsStyle="default"
221
229
  className="resume-button"
230
+ data-original-title="You do not have permission"
222
231
  disabled={true}
223
232
  onClick={[Function]}
233
+ title="You do not have permission"
224
234
  >
225
235
  Resume
226
236
  </Button>
@@ -231,8 +241,10 @@ exports[`Task rendering render without Props 1`] = `
231
241
  bsSize="small"
232
242
  bsStyle="default"
233
243
  className="cancel-button"
244
+ data-original-title="You do not have permission"
234
245
  disabled={true}
235
246
  onClick={[Function]}
247
+ title="You do not have permission"
236
248
  >
237
249
  Cancel
238
250
  </Button>
@@ -243,8 +255,10 @@ exports[`Task rendering render without Props 1`] = `
243
255
  bsSize="small"
244
256
  bsStyle="default"
245
257
  className="unlock-button"
258
+ data-original-title="You do not have permission"
246
259
  disabled={true}
247
260
  onClick={[MockFunction]}
261
+ title="You do not have permission"
248
262
  >
249
263
  Unlock
250
264
  </Button>
@@ -255,8 +269,10 @@ exports[`Task rendering render without Props 1`] = `
255
269
  bsSize="small"
256
270
  bsStyle="default"
257
271
  className="force-unlock-button"
258
- disabled={false}
272
+ data-original-title="You do not have permission"
273
+ disabled={true}
259
274
  onClick={[MockFunction]}
275
+ title="You do not have permission"
260
276
  >
261
277
  Force Unlock
262
278
  </Button>
@@ -264,6 +280,7 @@ exports[`Task rendering render without Props 1`] = `
264
280
  </Row>
265
281
  <TaskInfo
266
282
  action=""
283
+ canEdit={false}
267
284
  cancelTaskRequest={[Function]}
268
285
  cancellable={false}
269
286
  dynflowEnableConsole={false}
@@ -10,20 +10,6 @@
10
10
  .container {
11
11
  margin: 0;
12
12
  }
13
- /*
14
- * This is a manifest file that'll be compiled into application.css, which will include all the files
15
- * listed below.
16
- *
17
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
18
- * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
19
- *
20
- * You're free to add application-wide styles to this file and they'll appear at the top of the
21
- * compiled file, but it's generally better to create a new file per style scope.
22
- *
23
- *= require_self
24
- *= require_tree .
25
- */
26
-
27
13
  .spin {
28
14
  -webkit-animation: spin 1s infinite linear;
29
15
  -moz-animation: spin 1s infinite linear;
@@ -60,4 +46,7 @@
60
46
  transform: rotate(360deg);
61
47
  }
62
48
  }
49
+ .dynflow-button > span {
50
+ pointer-events: auto;
51
+ }
63
52
  }
@@ -56,7 +56,9 @@ export const refetchTaskDetails = (id, loading) => dispatch => {
56
56
 
57
57
  const reloadTasksDetails = async (id, dispatch) => {
58
58
  try {
59
- const { data } = await API.get(`/foreman_tasks/api/tasks/${id}/details`);
59
+ const { data } = await API.get(
60
+ `/foreman_tasks/api/tasks/${id}/details?include_permissions`
61
+ );
60
62
  dispatch(requestSuccess(data));
61
63
  } catch (error) {
62
64
  dispatch(requestFailure(error));
@@ -83,7 +85,9 @@ const getTasksDetails = async (
83
85
  refetchTaskDetailsAction
84
86
  ) => {
85
87
  try {
86
- const { data } = await API.get(`/foreman_tasks/api/tasks/${id}/details`);
88
+ const { data } = await API.get(
89
+ `/foreman_tasks/api/tasks/${id}/details?include_permissions`
90
+ );
87
91
  dispatch(requestSuccess(data));
88
92
  if (data.state !== 'stopped') {
89
93
  dispatch(taskReloadStart(timeoutId, refetchTaskDetailsAction, id));
@@ -30,7 +30,7 @@ export const selectErrors = state => {
30
30
 
31
31
  export const selectProgress = state =>
32
32
  selectTaskDetails(state).progress
33
- ? parseFloat((selectTaskDetails(state).progress * 100).toFixed(2))
33
+ ? Math.trunc(selectTaskDetails(state).progress * 100)
34
34
  : 0;
35
35
 
36
36
  export const selectUsername = state =>
@@ -77,3 +77,6 @@ export const selectExternalId = state =>
77
77
 
78
78
  export const selectDynflowEnableConsole = state =>
79
79
  selectTaskDetails(state).dynflow_enable_console || false;
80
+
81
+ export const selectCanEdit = state =>
82
+ selectTaskDetails(state).can_edit || false;
@@ -15,6 +15,7 @@ exports[`TaskDetails rendering render with min Props 1`] = `
15
15
  >
16
16
  <Task
17
17
  action=""
18
+ canEdit={false}
18
19
  cancelStep={[MockFunction]}
19
20
  cancelTaskRequest={[Function]}
20
21
  cancellable={false}
@@ -32,6 +32,7 @@ import {
32
32
  selectParentTask,
33
33
  selectExternalId,
34
34
  selectDynflowEnableConsole,
35
+ selectCanEdit,
35
36
  } from './TaskDetailsSelectors';
36
37
 
37
38
  const mapStateToProps = state => ({
@@ -62,6 +63,7 @@ const mapStateToProps = state => ({
62
63
  parentTask: selectParentTask(state),
63
64
  externalId: selectExternalId(state),
64
65
  dynflowEnableConsole: selectDynflowEnableConsole(state),
66
+ canEdit: selectCanEdit(state),
65
67
  });
66
68
 
67
69
  const mapDispatchToProps = dispatch =>
@@ -27,6 +27,7 @@ export const selectSelectedTasks = state => {
27
27
  id: item.id,
28
28
  isCancellable: item.availableActions.cancellable,
29
29
  isResumable: item.availableActions.resumable,
30
+ canEdit: item.canEdit,
30
31
  }));
31
32
  };
32
33
 
@@ -22,6 +22,7 @@ const state = {
22
22
  id: 1,
23
23
  action: 'action1',
24
24
  available_actions: { cancellable: true },
25
+ can_edit: true,
25
26
  },
26
27
  { id: 2, action: 'action2', available_actions: { resumable: true } },
27
28
  ],
@@ -15,12 +15,14 @@ exports[`TasksDashboard - Selectors should select selectedRowsLen some 1`] = `3`
15
15
  exports[`TasksDashboard - Selectors should select selectedTasks 1`] = `
16
16
  Array [
17
17
  Object {
18
+ "canEdit": true,
18
19
  "id": 1,
19
20
  "isCancellable": true,
20
21
  "isResumable": undefined,
21
22
  "name": "action1",
22
23
  },
23
24
  Object {
25
+ "canEdit": undefined,
24
26
  "id": 2,
25
27
  "isCancellable": undefined,
26
28
  "isResumable": true,
@@ -63,7 +63,7 @@ export const bulkResumeById = ({
63
63
  url,
64
64
  parentTaskID,
65
65
  }) => async dispatch => {
66
- const resumeTasks = selected.filter(task => task.isResumable);
66
+ const resumeTasks = selected.filter(task => task.isResumable && task.canEdit);
67
67
  if (resumeTasks.length < selected.length)
68
68
  dispatch(
69
69
  addToast(
@@ -87,7 +87,7 @@ export const bulkResumeById = ({
87
87
  });
88
88
  });
89
89
  if (data.resumed) {
90
- reloadPage(url, parentTaskID, dispatch);
90
+ reloadPage(url, parentTaskID)(dispatch);
91
91
  }
92
92
  } catch (error) {
93
93
  handleErrorResume(error, dispatch);
@@ -135,7 +135,9 @@ export const bulkCancelById = ({
135
135
  url,
136
136
  parentTaskID,
137
137
  }) => async dispatch => {
138
- const cancelTasks = selected.filter(task => task.isCancellable);
138
+ const cancelTasks = selected.filter(
139
+ task => task.isCancellable && task.canEdit
140
+ );
139
141
  if (cancelTasks.length < selected.length)
140
142
  dispatch(
141
143
  addToast(
@@ -160,7 +162,7 @@ export const bulkCancelById = ({
160
162
  });
161
163
  });
162
164
  if (data.cancelled) {
163
- reloadPage(url, parentTaskID, dispatch);
165
+ reloadPage(url, parentTaskID)(dispatch);
164
166
  }
165
167
  } catch (error) {
166
168
  handleErrorCancel(error, dispatch);
@@ -185,6 +187,18 @@ export const bulkForceCancelById = ({
185
187
  parentTaskID,
186
188
  }) => async dispatch => {
187
189
  const stopTasks = selected.filter(task => task.state !== 'stopped');
190
+ const authorisedTasks = stopTasks.filter(task => task.canEdit);
191
+ if (authorisedTasks.length < stopTasks.length)
192
+ dispatch(
193
+ addToast(
194
+ warningToastData(
195
+ sprintf(
196
+ 'User has no permission for %s task(s)',
197
+ stopTasks.length - authorisedTasks.length
198
+ )
199
+ )
200
+ )
201
+ );
188
202
  if (stopTasks.length < selected.length)
189
203
  dispatch(
190
204
  addToast(
@@ -196,10 +210,13 @@ export const bulkForceCancelById = ({
196
210
  )
197
211
  )
198
212
  );
199
- if (stopTasks.length > 0) {
213
+ if (authorisedTasks.length > 0) {
200
214
  dispatch({ type: TASKS_FORCE_CANCEL_REQUEST });
201
215
  try {
202
- const { data } = await bulkByIdRequest(stopTasks, BULK_FORCE_CANCEL_PATH);
216
+ const { data } = await bulkByIdRequest(
217
+ authorisedTasks,
218
+ BULK_FORCE_CANCEL_PATH
219
+ );
203
220
  dispatch({ type: TASKS_FORCE_CANCEL_SUCCESS });
204
221
  if (data.stopped_length) {
205
222
  dispatch(
@@ -219,7 +236,7 @@ export const bulkForceCancelById = ({
219
236
  )
220
237
  );
221
238
  if (data.stopped_length > 0) {
222
- reloadPage(url, parentTaskID, dispatch);
239
+ reloadPage(url, parentTaskID)(dispatch);
223
240
  }
224
241
  } catch (error) {
225
242
  handleErrorForceCancel(error, dispatch);
@@ -22,7 +22,7 @@ import {
22
22
  export const getTableItems = url =>
23
23
  getTableItemsAction(TASKS_TABLE_ID, getURIQuery(url), getApiPathname(url));
24
24
 
25
- export const reloadPage = (url, parentTaskID, dispatch) => {
25
+ export const reloadPage = (url, parentTaskID) => dispatch => {
26
26
  dispatch(getTableItems(url));
27
27
  dispatch(fetchTasksSummary(getURIQuery(url).time, parentTaskID));
28
28
  };
@@ -34,7 +34,7 @@ export const cancelTask = ({
34
34
  parentTaskID,
35
35
  }) => async dispatch => {
36
36
  await dispatch(cancelTaskRequest(taskId, taskName));
37
- reloadPage(url, parentTaskID, dispatch);
37
+ reloadPage(url, parentTaskID)(dispatch);
38
38
  };
39
39
 
40
40
  export const resumeTask = ({
@@ -44,7 +44,7 @@ export const resumeTask = ({
44
44
  parentTaskID,
45
45
  }) => async dispatch => {
46
46
  await dispatch(resumeTaskRequest(taskId, taskName));
47
- reloadPage(url, parentTaskID, dispatch);
47
+ reloadPage(url, parentTaskID)(dispatch);
48
48
  };
49
49
 
50
50
  export const forceCancelTask = ({
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { getURIsearch } from 'foremanReact/common/urlHelpers';
4
- import { Spinner } from 'patternfly-react';
4
+ import { Spinner, Button, Icon } from 'patternfly-react';
5
5
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
6
6
  import { translate as __ } from 'foremanReact/common/I18n';
7
7
  import { getURIQuery } from 'foremanReact/common/helpers';
@@ -63,7 +63,10 @@ const TasksTablePage = ({
63
63
  toastNotifications="foreman-tasks-cancel"
64
64
  toolbarButtons={
65
65
  <React.Fragment>
66
- {props.status === STATUS.PENDING && <Spinner size="lg" loading />}
66
+ <Button onClick={() => props.reloadPage(url, props.parentTaskID)}>
67
+ <Icon type="fa" name="refresh" /> {__('Refresh Data')}
68
+ </Button>
69
+ {props.status === STATUS.PENDING && <Spinner size="md" loading />}
67
70
  <ExportButton
68
71
  url={getCSVurl(url, uriQuery)}
69
72
  title={__('Export All')}
@@ -118,6 +121,7 @@ TasksTablePage.propTypes = {
118
121
  openModalAction: PropTypes.func.isRequired,
119
122
  showSelectAll: PropTypes.bool,
120
123
  unselectAllRows: PropTypes.func.isRequired,
124
+ reloadPage: PropTypes.func.isRequired,
121
125
  };
122
126
 
123
127
  TasksTablePage.defaultProps = {
@@ -39,6 +39,7 @@ export const selectResults = createSelector(
39
39
  ...result.available_actions,
40
40
  stoppable: result.state !== 'stopped',
41
41
  },
42
+ canEdit: result.can_edit,
42
43
  }))
43
44
  );
44
45
 
@@ -18,6 +18,7 @@ jest.mock('foremanReact/redux/API');
18
18
  const task = {
19
19
  id: 'some-id',
20
20
  name: 'some-name',
21
+ canEdit: true,
21
22
  };
22
23
 
23
24
  const fixtures = {
@@ -140,6 +141,18 @@ const fixtures = {
140
141
  url: 'some-url',
141
142
  parentTaskID: 'parent',
142
143
  }),
144
+ 'handles bulkCancelById requests with canEdit false': () => {
145
+ const selected = [{ ...task, isCancellable: true, canEdit: false }];
146
+ return bulkCancelById({ selected, url: 'some-url' });
147
+ },
148
+ 'handles bulkResumeById requests with canEdit false': () => {
149
+ const selected = [{ ...task, isResumable: false, canEdit: false }];
150
+ return bulkResumeById({ selected, url: 'some-url' });
151
+ },
152
+ 'handles bulkForceCancelById requests with canEdit false': () => {
153
+ const selected = [{ ...task, isResumable: false, canEdit: false }];
154
+ return bulkForceCancelById({ selected, url: 'some-url' });
155
+ },
143
156
  };
144
157
 
145
158
  describe('TasksTable bulk actions', () => {
@@ -9,6 +9,7 @@ export const minProps = {
9
9
  unselectAllRows: jest.fn(),
10
10
  selectRow: jest.fn(),
11
11
  unselectRow: jest.fn(),
12
+ reloadPage: jest.fn(),
12
13
  selectedRows: [],
13
14
  pagination: {
14
15
  page: 1,
@@ -12,9 +12,10 @@ const history = {
12
12
  const fixtures = {
13
13
  'render with minimal props': { ...minProps, history },
14
14
 
15
- 'render with Breadcrubs': {
15
+ 'render with Breadcrubs and edit permissions': {
16
16
  ...minProps,
17
17
  history,
18
+ results: [{ action: 'a', canEdit: true }],
18
19
  getBreadcrumbs: () => ({
19
20
  breadcrumbItems: [
20
21
  { caption: 'Tasks', url: `/foreman_tasks/tasks` },
@@ -30,6 +30,7 @@ exports[`SubTasksPage rendering render with minimal props 1`] = `
30
30
  }
31
31
  }
32
32
  parentTaskID="some-id"
33
+ reloadPage={[MockFunction]}
33
34
  results={
34
35
  Array [
35
36
  "a",
@@ -48,6 +48,22 @@ Array [
48
48
  ]
49
49
  `;
50
50
 
51
+ exports[`TasksTable bulk actions handles bulkCancelById requests with canEdit false 1`] = `
52
+ Array [
53
+ Array [
54
+ Object {
55
+ "payload": Object {
56
+ "message": Object {
57
+ "message": "Not all the selected tasks can be cancelled",
58
+ "type": "warning",
59
+ },
60
+ },
61
+ "type": "TOASTS_ADD",
62
+ },
63
+ ],
64
+ ]
65
+ `;
66
+
51
67
  exports[`TasksTable bulk actions handles bulkCancelBySearch requests 1`] = `
52
68
  Array [
53
69
  Array [
@@ -155,6 +171,22 @@ Array [
155
171
  ]
156
172
  `;
157
173
 
174
+ exports[`TasksTable bulk actions handles bulkForceCancelById requests with canEdit false 1`] = `
175
+ Array [
176
+ Array [
177
+ Object {
178
+ "payload": Object {
179
+ "message": Object {
180
+ "message": "User has no permission for 1 task(s)",
181
+ "type": "warning",
182
+ },
183
+ },
184
+ "type": "TOASTS_ADD",
185
+ },
186
+ ],
187
+ ]
188
+ `;
189
+
158
190
  exports[`TasksTable bulk actions handles bulkForceCancelBySearch requests 1`] = `
159
191
  Array [
160
192
  Array [
@@ -219,6 +251,22 @@ Array [
219
251
  ]
220
252
  `;
221
253
 
254
+ exports[`TasksTable bulk actions handles bulkResumeById requests with canEdit false 1`] = `
255
+ Array [
256
+ Array [
257
+ Object {
258
+ "payload": Object {
259
+ "message": Object {
260
+ "message": "Not all the selected tasks can be resumed",
261
+ "type": "warning",
262
+ },
263
+ },
264
+ "type": "TOASTS_ADD",
265
+ },
266
+ ],
267
+ ]
268
+ `;
269
+
222
270
  exports[`TasksTable bulk actions handles bulkResumeBySearch requests 1`] = `
223
271
  Array [
224
272
  Array [
@@ -21,6 +21,7 @@ exports[`TasksIndexPage rendering render with minimal props 1`] = `
21
21
  "perPage": 10,
22
22
  }
23
23
  }
24
+ reloadPage={[MockFunction]}
24
25
  results={
25
26
  Array [
26
27
  "a",
@@ -1,6 +1,6 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`TasksTablePage rendering render with Breadcrubs 1`] = `
3
+ exports[`TasksTablePage rendering render with Breadcrubs and edit permissions 1`] = `
4
4
  <div
5
5
  className="tasks-table-wrapper"
6
6
  >
@@ -69,12 +69,27 @@ exports[`TasksTablePage rendering render with Breadcrubs 1`] = `
69
69
  toastNotifications="foreman-tasks-cancel"
70
70
  toolbarButtons={
71
71
  <React.Fragment>
72
+ <Button
73
+ active={false}
74
+ block={false}
75
+ bsClass="btn"
76
+ bsStyle="default"
77
+ disabled={false}
78
+ onClick={[Function]}
79
+ >
80
+ <Icon
81
+ name="refresh"
82
+ type="fa"
83
+ />
84
+
85
+ Refresh Data
86
+ </Button>
72
87
  <Spinner
73
88
  className=""
74
89
  inline={false}
75
90
  inverse={false}
76
91
  loading={true}
77
- size="lg"
92
+ size="md"
78
93
  />
79
94
  <ExportButton
80
95
  title="Export All"
@@ -112,10 +127,13 @@ exports[`TasksTablePage rendering render with Breadcrubs 1`] = `
112
127
  }
113
128
  }
114
129
  parentTaskID={null}
130
+ reloadPage={[MockFunction]}
115
131
  results={
116
132
  Array [
117
- "a",
118
- "b",
133
+ Object {
134
+ "action": "a",
135
+ "canEdit": true,
136
+ },
119
137
  ]
120
138
  }
121
139
  selectPage={[MockFunction]}
@@ -187,12 +205,27 @@ exports[`TasksTablePage rendering render with minimal props 1`] = `
187
205
  toastNotifications="foreman-tasks-cancel"
188
206
  toolbarButtons={
189
207
  <React.Fragment>
208
+ <Button
209
+ active={false}
210
+ block={false}
211
+ bsClass="btn"
212
+ bsStyle="default"
213
+ disabled={false}
214
+ onClick={[Function]}
215
+ >
216
+ <Icon
217
+ name="refresh"
218
+ type="fa"
219
+ />
220
+
221
+ Refresh Data
222
+ </Button>
190
223
  <Spinner
191
224
  className=""
192
225
  inline={false}
193
226
  inverse={false}
194
227
  loading={true}
195
- size="lg"
228
+ size="md"
196
229
  />
197
230
  <ExportButton
198
231
  title="Export All"
@@ -230,6 +263,7 @@ exports[`TasksTablePage rendering render with minimal props 1`] = `
230
263
  }
231
264
  }
232
265
  parentTaskID={null}
266
+ reloadPage={[MockFunction]}
233
267
  results={
234
268
  Array [
235
269
  "a",
@@ -7,6 +7,7 @@ exports[`actionCellFormatter render 1`] = `
7
7
  "cancellable": true,
8
8
  }
9
9
  }
10
+ canEdit={true}
10
11
  id="some-id"
11
12
  name="some-name"
12
13
  taskActions={Object {}}
@@ -3,8 +3,10 @@
3
3
  exports[`selectionCellFormatter render 1`] = `
4
4
  <TableSelectionCell
5
5
  checked={true}
6
+ disabled={true}
6
7
  id="selectsome-index"
7
8
  label="Select row"
8
9
  onChange={[Function]}
10
+ title="You do not have permission"
9
11
  />
10
12
  `;
@@ -4,7 +4,7 @@ describe('actionCellFormatter', () => {
4
4
  it('render', () => {
5
5
  const data = [
6
6
  { cancellable: true },
7
- { rowData: { action: 'some-name', id: 'some-id' } },
7
+ { rowData: { action: 'some-name', id: 'some-id', canEdit: true } },
8
8
  ];
9
9
  expect(actionCellFormatter({})(...data)).toMatchSnapshot();
10
10
  });
@@ -5,7 +5,7 @@ describe('selectionCellFormatter', () => {
5
5
  expect(
6
6
  selectionCellFormatter(
7
7
  { isSelected: () => true },
8
- { rowIndex: 'some-index' }
8
+ { rowIndex: 'some-index', rowData: {} }
9
9
  )
10
10
  ).toMatchSnapshot();
11
11
  });
@@ -4,13 +4,16 @@ import { ActionButton } from '../../common/ActionButtons/ActionButton';
4
4
 
5
5
  export const actionCellFormatter = taskActions => (
6
6
  value,
7
- { rowData: { action, id } }
7
+ { rowData: { action, id, canEdit } }
8
8
  ) =>
9
9
  cellFormatter(
10
- <ActionButton
11
- id={id}
12
- name={action}
13
- taskActions={taskActions}
14
- availableActions={value}
15
- />
10
+ canEdit && (
11
+ <ActionButton
12
+ canEdit={canEdit}
13
+ id={id}
14
+ name={action}
15
+ taskActions={taskActions}
16
+ availableActions={value}
17
+ />
18
+ )
16
19
  );
@@ -1,9 +1,16 @@
1
1
  import React from 'react';
2
+ import { translate as __ } from 'foremanReact/common/I18n';
2
3
  import TableSelectionCell from '../Components/TableSelectionCell';
3
4
 
4
5
  export default (selectionController, additionalData) => (
5
6
  <TableSelectionCell
6
7
  id={`select${additionalData.rowIndex}`}
8
+ disabled={!additionalData.rowData.canEdit}
9
+ title={
10
+ additionalData.rowData.canEdit
11
+ ? undefined
12
+ : __('You do not have permission')
13
+ }
7
14
  checked={selectionController.isSelected(additionalData)}
8
15
  onChange={() => selectionController.selectRow(additionalData)}
9
16
  />
@@ -4,45 +4,48 @@ import { translate as __ } from 'foremanReact/common/I18n';
4
4
  import { ActionButtons } from 'foremanReact/components/common/ActionButtons/ActionButtons';
5
5
 
6
6
  export const ActionButton = ({
7
+ canEdit,
7
8
  id,
8
9
  name,
9
10
  availableActions: { resumable, cancellable, stoppable },
10
11
  taskActions,
11
12
  }) => {
12
13
  const buttons = [];
13
- const isTitle = !(resumable || cancellable || stoppable);
14
+ const isTitle = canEdit && !(resumable || cancellable || stoppable);
14
15
  const title = isTitle ? __('Task cannot be canceled') : undefined;
15
- if (resumable) {
16
- buttons.push({
17
- title: __('Resume'),
18
- action: {
19
- disabled: !resumable,
20
- onClick: () => taskActions.resumeTask(id, name),
21
- id: `task-resume-button-${id}`,
22
- },
23
- });
24
- }
25
- if (cancellable || (!stoppable && !resumable)) {
26
- // Cancel is the default button that should be shown if no task action can be done
27
- buttons.push({
28
- title: __('Cancel'),
29
- action: {
30
- disabled: !cancellable,
31
- onClick: () => taskActions.cancelTask(id, name),
32
- id: `task-cancel-button-${id}`,
33
- },
34
- });
35
- }
16
+ if (canEdit) {
17
+ if (resumable) {
18
+ buttons.push({
19
+ title: __('Resume'),
20
+ action: {
21
+ disabled: !resumable,
22
+ onClick: () => taskActions.resumeTask(id, name),
23
+ id: `task-resume-button-${id}`,
24
+ },
25
+ });
26
+ }
27
+ if (cancellable || (!stoppable && !resumable)) {
28
+ // Cancel is the default button that should be shown if no task action can be done
29
+ buttons.push({
30
+ title: __('Cancel'),
31
+ action: {
32
+ disabled: !cancellable,
33
+ onClick: () => taskActions.cancelTask(id, name),
34
+ id: `task-cancel-button-${id}`,
35
+ },
36
+ });
37
+ }
36
38
 
37
- if (stoppable) {
38
- buttons.push({
39
- title: __('Force Cancel'),
40
- action: {
41
- disabled: !stoppable,
42
- onClick: () => taskActions.forceCancelTask(id, name),
43
- id: `task-force-cancel-button-${id}`,
44
- },
45
- });
39
+ if (stoppable) {
40
+ buttons.push({
41
+ title: __('Force Cancel'),
42
+ action: {
43
+ disabled: !stoppable,
44
+ onClick: () => taskActions.forceCancelTask(id, name),
45
+ id: `task-force-cancel-button-${id}`,
46
+ },
47
+ });
48
+ }
46
49
  }
47
50
  return (
48
51
  <span title={title}>
@@ -52,6 +55,7 @@ export const ActionButton = ({
52
55
  };
53
56
 
54
57
  ActionButton.propTypes = {
58
+ canEdit: PropTypes.bool,
55
59
  id: PropTypes.string.isRequired,
56
60
  name: PropTypes.string.isRequired,
57
61
  availableActions: PropTypes.shape({
@@ -65,3 +69,7 @@ ActionButton.propTypes = {
65
69
  forceCancelTask: PropTypes.func,
66
70
  }).isRequired,
67
71
  };
72
+
73
+ ActionButton.defaultProps = {
74
+ canEdit: false,
75
+ };
@@ -7,6 +7,7 @@ const resumeTask = jest.fn();
7
7
  const cancelTask = jest.fn();
8
8
  const forceCancelTask = jest.fn();
9
9
  const taskActions = { resumeTask, cancelTask, forceCancelTask };
10
+ const minProps = { canEdit: true, id: 'id', name: 'some-name' };
10
11
  const fixtures = {
11
12
  'render with cancellable true props': {
12
13
  availableActions: {
@@ -14,8 +15,7 @@ const fixtures = {
14
15
  resumable: false,
15
16
  },
16
17
  taskActions,
17
- id: 'id',
18
- name: 'some-name',
18
+ ...minProps,
19
19
  },
20
20
  'render with resumable true props': {
21
21
  availableActions: {
@@ -23,8 +23,7 @@ const fixtures = {
23
23
  resumable: true,
24
24
  },
25
25
  taskActions,
26
- id: 'id',
27
- name: 'some-name',
26
+ ...minProps,
28
27
  },
29
28
  'render with stoppable and cancellable true props': {
30
29
  availableActions: {
@@ -32,8 +31,7 @@ const fixtures = {
32
31
  stoppable: true,
33
32
  },
34
33
  taskActions,
35
- id: 'id',
36
- name: 'some-name',
34
+ ...minProps,
37
35
  },
38
36
  'render with cancellable false props': {
39
37
  availableActions: {
@@ -41,8 +39,16 @@ const fixtures = {
41
39
  resumable: false,
42
40
  },
43
41
  taskActions,
44
- id: 'id',
45
- name: 'some-name',
42
+ ...minProps,
43
+ },
44
+ 'render with canEdit false': {
45
+ availableActions: {
46
+ cancellable: false,
47
+ resumable: false,
48
+ },
49
+ taskActions,
50
+ ...minProps,
51
+ canEdit: false,
46
52
  },
47
53
  };
48
54
 
@@ -57,6 +63,7 @@ describe('ActionButton', () => {
57
63
  <ActionButton
58
64
  id={id}
59
65
  name={name}
66
+ canEdit
60
67
  availableActions={{ cancellable: true }}
61
68
  taskActions={taskActions}
62
69
  />
@@ -69,6 +76,7 @@ describe('ActionButton', () => {
69
76
  <ActionButton
70
77
  id={id}
71
78
  name={name}
79
+ canEdit
72
80
  availableActions={{ resumable: true }}
73
81
  taskActions={taskActions}
74
82
  />
@@ -81,6 +89,7 @@ describe('ActionButton', () => {
81
89
  <ActionButton
82
90
  id={id}
83
91
  name={name}
92
+ canEdit
84
93
  availableActions={{ stoppable: true }}
85
94
  taskActions={taskActions}
86
95
  />
@@ -1,5 +1,13 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
+ exports[`ActionButton snapshot test render with canEdit false 1`] = `
4
+ <span>
5
+ <ActionButtons
6
+ buttons={Array []}
7
+ />
8
+ </span>
9
+ `;
10
+
3
11
  exports[`ActionButton snapshot test render with cancellable false props 1`] = `
4
12
  <span
5
13
  title="Task cannot be canceled"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-23 00:00:00.000000000 Z
11
+ date: 2020-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dynflow
@@ -270,6 +270,7 @@ files:
270
270
  - lib/foreman_tasks/engine.rb
271
271
  - lib/foreman_tasks/task_error.rb
272
272
  - lib/foreman_tasks/tasks/cleanup.rake
273
+ - lib/foreman_tasks/tasks/dynflow.rake
273
274
  - lib/foreman_tasks/tasks/export_tasks.rake
274
275
  - lib/foreman_tasks/tasks/generate_task_actions.rake
275
276
  - lib/foreman_tasks/test_extensions.rb
@@ -289,6 +290,7 @@ files:
289
290
  - locale/zh_CN/LC_MESSAGES/foreman_tasks.mo
290
291
  - locale/zh_CN/foreman_tasks.po
291
292
  - package.json
293
+ - script/npm_link_foreman_js.sh
292
294
  - script/rails
293
295
  - script/travis_run_js_tests.sh
294
296
  - test/controllers/api/recurring_logics_controller_test.rb
@@ -593,7 +595,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
593
595
  - !ruby/object:Gem::Version
594
596
  version: '0'
595
597
  requirements: []
596
- rubygems_version: 3.0.4
598
+ rubygems_version: 3.0.3
597
599
  signing_key:
598
600
  specification_version: 4
599
601
  summary: Foreman plugin for showing tasks information for resources and users