foreman-tasks 2.0.1 → 2.0.2

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 (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