foreman_remote_execution 18.0.1 → 18.1.0

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +28 -28
  3. data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +48 -48
  4. data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +29 -29
  5. data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +22 -22
  6. data/app/assets/javascripts/foreman_remote_execution/template_invocation.js +0 -170
  7. data/app/assets/stylesheets/foreman_remote_execution/job_invocations.scss +0 -6
  8. data/app/controllers/job_invocations_controller.rb +1 -98
  9. data/app/helpers/job_invocations_helper.rb +0 -49
  10. data/app/helpers/remote_execution_helper.rb +23 -123
  11. data/config/routes.rb +0 -6
  12. data/lib/foreman_remote_execution/plugin.rb +2 -2
  13. data/lib/foreman_remote_execution/version.rb +1 -1
  14. data/locale/foreman_remote_execution.pot +2 -2
  15. data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  16. data/locale/fr/foreman_remote_execution.po +28 -28
  17. data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  18. data/locale/ja/foreman_remote_execution.po +48 -48
  19. data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  20. data/locale/ko/foreman_remote_execution.po +29 -29
  21. data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
  22. data/locale/zh_CN/foreman_remote_execution.po +22 -22
  23. data/package.json +1 -14
  24. data/test/functional/job_invocations_controller_test.rb +0 -77
  25. data/webpack/JobInvocationDetail/JobInvocationConstants.js +5 -0
  26. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +0 -10
  27. data/webpack/JobInvocationDetail/TemplateInvocation.js +12 -69
  28. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +94 -72
  29. data/webpack/JobInvocationDetail/TemplateInvocationHelpers.js +33 -0
  30. data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +1 -1
  31. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +0 -6
  32. data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +1 -1
  33. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +1 -1
  34. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +5 -1
  35. data/webpack/JobInvocationDetail/__tests__/TemplateInvocationHelpers.test.js +94 -0
  36. data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +338 -139
  37. data/webpack/JobInvocationDetail/useTemplateInvocationOutputPolling.js +185 -0
  38. data/webpack/JobWizard/JobWizardPageRerun.js +0 -10
  39. data/webpack/JobWizard/__tests__/autofill.test.js +23 -0
  40. data/webpack/JobWizard/__tests__/integration.test.js +1 -1
  41. data/webpack/JobWizard/autofill.js +18 -4
  42. data/webpack/JobWizard/index.js +1 -18
  43. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +49 -1
  44. data/webpack/global_index.js +0 -2
  45. data/webpack/index.js +0 -15
  46. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +1 -1
  47. metadata +6 -53
  48. data/app/assets/stylesheets/foreman_remote_execution/modal_window.scss +0 -4
  49. data/app/helpers/job_invocations_chart_helper.rb +0 -80
  50. data/app/views/job_invocations/_card_results.html.erb +0 -12
  51. data/app/views/job_invocations/_card_schedule.html.erb +0 -33
  52. data/app/views/job_invocations/_card_target_hosts.html.erb +0 -57
  53. data/app/views/job_invocations/_card_user_input.html.erb +0 -16
  54. data/app/views/job_invocations/_description_fields.html.erb +0 -4
  55. data/app/views/job_invocations/_form.html.erb +0 -144
  56. data/app/views/job_invocations/_preview_hosts_modal.html.erb +0 -15
  57. data/app/views/job_invocations/_rerun_taxonomies.html.erb +0 -22
  58. data/app/views/job_invocations/_tab_hosts.html.erb +0 -11
  59. data/app/views/job_invocations/_tab_overview.html.erb +0 -41
  60. data/app/views/job_invocations/_tab_preview_templates.html.erb +0 -20
  61. data/app/views/job_invocations/_user_input.html.erb +0 -21
  62. data/app/views/job_invocations/new.html.erb +0 -9
  63. data/app/views/job_invocations/refresh.js.erb +0 -4
  64. data/app/views/job_invocations/show.html.erb +0 -59
  65. data/app/views/job_invocations/show.js.erb +0 -12
  66. data/app/views/job_invocations/show.json.erb +0 -5
  67. data/test/support/remote_execution_helper.rb +0 -5
  68. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +0 -62
  69. data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +0 -2
  70. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +0 -10
  71. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.js +0 -45
  72. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +0 -30
  73. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +0 -78
  74. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  75. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +0 -25
  76. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +0 -76
  77. data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +0 -6
  78. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +0 -6
  79. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +0 -9
  80. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +0 -31
  81. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +0 -12
  82. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +0 -96
  83. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +0 -75
  84. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +0 -16
  85. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +0 -56
  86. data/webpack/react_app/components/TargetingHosts/components/HostItem.js +0 -39
  87. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +0 -54
  88. data/webpack/react_app/components/TargetingHosts/index.js +0 -150
  89. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +0 -60
  90. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +0 -76
  91. data/webpack/react_app/components/jobInvocations/index.js +0 -94
  92. data/webpack/react_app/extend/reducers.js +0 -5
  93. data/webpack/react_app/redux/actions/jobInvocations/index.js +0 -87
  94. data/webpack/react_app/redux/consts.js +0 -5
  95. data/webpack/react_app/redux/reducers/index.js +0 -6
  96. data/webpack/react_app/redux/reducers/jobInvocations/index.fixtures.js +0 -46
  97. data/webpack/react_app/redux/reducers/jobInvocations/index.js +0 -34
  98. data/webpack/react_app/redux/reducers/jobInvocations/index.test.js +0 -43
@@ -135,7 +135,7 @@ msgid "Alphabetical"
135
135
  msgstr "按字母"
136
136
 
137
137
  msgid "Already on the review step"
138
- msgstr ""
138
+ msgstr "已在审阅步骤中"
139
139
 
140
140
  msgid "An error occurred while fetching the template invocation details."
141
141
  msgstr "在获取模板调用详情时出错。"
@@ -267,28 +267,28 @@ msgid "Copy to clipboard"
267
267
  msgstr "复制到剪贴板"
268
268
 
269
269
  msgid "Could not abort the job %s: %s"
270
- msgstr ""
270
+ msgstr "无法中止作业 %s: %s"
271
271
 
272
272
  msgid "Could not abort the task"
273
273
  msgstr "无法中止作业"
274
274
 
275
275
  msgid "Could not cancel recurring logic %s: %s"
276
- msgstr ""
276
+ msgstr "无法取消循环逻辑 %s:%s"
277
277
 
278
278
  msgid "Could not cancel the job %s: %s"
279
- msgstr ""
279
+ msgstr "无法取消作业 %s: %s"
280
280
 
281
281
  msgid "Could not cancel the task"
282
282
  msgstr "无法取消作业"
283
283
 
284
284
  msgid "Could not disable recurring logic %s: %s"
285
- msgstr ""
285
+ msgstr "无法禁用重复逻辑 %s:%s"
286
286
 
287
287
  msgid "Could not display data for job invocation."
288
288
  msgstr "无法显示用于作业调用的数据。"
289
289
 
290
290
  msgid "Could not enable recurring logic %s: %s"
291
- msgstr ""
291
+ msgstr "无法启用重复逻辑 %s:%s"
292
292
 
293
293
  msgid "Could not find any suitable interface for execution"
294
294
  msgstr "无法找到适合执行的接口"
@@ -321,7 +321,7 @@ msgid "Create a job template"
321
321
  msgstr "创建作业模板"
322
322
 
323
323
  msgid "Create a new job invocation"
324
- msgstr ""
324
+ msgstr "创建新作业调用"
325
325
 
326
326
  msgid "Create a recurring job"
327
327
  msgstr "创建一个重复运行的作业"
@@ -414,7 +414,7 @@ msgid "Designation of a special purpose"
414
414
  msgstr "设计特殊目的"
415
415
 
416
416
  msgid "Directory on the host where remote execution jobs in push mode place and run their scripts. You can override this setting per host with the `remote_execution_remote_working_dir` host parameter."
417
- msgstr ""
417
+ msgstr "主机上以推送模式运行的远程执行作业存放并运行其脚本的目录。。您可以使用 'remote_execution_remote_working_dir' 主机参数覆盖每个主机的设置。"
418
418
 
419
419
  msgid "Disable recurring"
420
420
  msgstr "禁用重复"
@@ -552,7 +552,7 @@ msgid "Failed rendering template: %s"
552
552
  msgstr "呈现模板失败:%s"
553
553
 
554
554
  msgid "Failed to load host invocation data"
555
- msgstr ""
555
+ msgstr "加载主机调用数据失败"
556
556
 
557
557
  msgid "Failed:"
558
558
  msgstr "失败:"
@@ -609,7 +609,7 @@ msgid "Get raw output for a host"
609
609
  msgstr "获取主机的原始输出"
610
610
 
611
611
  msgid "Go to job invocations"
612
- msgstr ""
612
+ msgstr "返回到作业调用"
613
613
 
614
614
  msgid "Has to be a positive number"
615
615
  msgstr "必须为一个正数"
@@ -678,10 +678,10 @@ msgid "Include all inputs from the foreign template"
678
678
  msgstr "在外部模板中包含所有输入格式"
679
679
 
680
680
  msgid "Include hosts and template invocations in the response. Defaults to true for backwards compatibility. Pass false to skip serializing all hosts."
681
- msgstr ""
681
+ msgstr "在响应中包含主机和模板调用。为了向后兼容,默认为 true。设置为 false 来跳过所有主机的序列化。"
682
682
 
683
683
  msgid "Include per-host task and permission data in the response"
684
- msgstr ""
684
+ msgstr "在响应中包含每个主机的任务和权限数据"
685
685
 
686
686
  msgid "Indicates that the action should be cancelled if it cannot be started before this time."
687
687
  msgstr "这代表,如果操作无法在这个时间前开始则操作应该被取消。"
@@ -852,7 +852,7 @@ msgid "Minute can only be a number between 0-59"
852
852
  msgstr "分钟只能是一个 0-59 之间的数字"
853
853
 
854
854
  msgid "Missing permissions to generate report templates"
855
- msgstr ""
855
+ msgstr "缺少生成报告模板的权限"
856
856
 
857
857
  msgid "Missing the required permissions: ${missingPermissions.join( ', ' )}"
858
858
  msgstr "缺少所需的权限:${missingPermissions.join( ', ' )}"
@@ -1098,7 +1098,7 @@ msgid "Recurring logic"
1098
1098
  msgstr "重复逻辑"
1099
1099
 
1100
1100
  msgid "Recurring logic %s cancelled successfully."
1101
- msgstr "成功取消重复逻辑 %s。"
1101
+ msgstr "成功取消循环逻辑 %s。"
1102
1102
 
1103
1103
  msgid "Recurring logic %s disabled successfully."
1104
1104
  msgstr "成功禁用重复逻辑 %s。"
@@ -1134,10 +1134,10 @@ msgid "Remote execution job"
1134
1134
  msgstr "远程执行作业"
1135
1135
 
1136
1136
  msgid "Remote working directory"
1137
- msgstr ""
1137
+ msgstr "远程工作目录"
1138
1138
 
1139
1139
  msgid "Remote working directory \"%{current_value}\" is not an absolute path"
1140
- msgstr ""
1140
+ msgstr "远程工作目录 \"%{current_value}\" 不是绝对路径"
1141
1141
 
1142
1142
  msgid "Repeat a maximum of N times"
1143
1143
  msgstr "最多重复 N 次"
@@ -1152,7 +1152,7 @@ msgid "Repeats"
1152
1152
  msgstr "重复"
1153
1153
 
1154
1154
  msgid "Report template not found or not configured properly"
1155
- msgstr ""
1155
+ msgstr "报告模板未找到或未正确配置"
1156
1156
 
1157
1157
  msgid "Rerun"
1158
1158
  msgstr "重新运行"
@@ -1359,7 +1359,7 @@ msgid "Show job invocation"
1359
1359
  msgstr "显示作业调用"
1360
1360
 
1361
1361
  msgid "Show job status for each host, only applicable when include_hosts is true"
1362
- msgstr ""
1362
+ msgstr "显示每个主机的作业状态,仅在 include_hosts 为 true 时才适用"
1363
1363
 
1364
1364
  msgid "Show job template details"
1365
1365
  msgstr "显示作业模板详情"
@@ -1675,7 +1675,7 @@ msgid "Unknown"
1675
1675
  msgstr "未知"
1676
1676
 
1677
1677
  msgid "Unknown error."
1678
- msgstr ""
1678
+ msgstr "未知错误"
1679
1679
 
1680
1680
  msgid "Unknown execution status"
1681
1681
  msgstr "未知的执行状态"
@@ -1825,7 +1825,7 @@ msgid "host already has an execution interface"
1825
1825
  msgstr "主机已有一个执行接口"
1826
1826
 
1827
1827
  msgid "host method"
1828
- msgstr ""
1828
+ msgstr "主机方法"
1829
1829
 
1830
1830
  msgid "hosts"
1831
1831
  msgstr "主机"
@@ -1855,10 +1855,10 @@ msgid "is month (range: 1-12)"
1855
1855
  msgstr "月份(范围:1-12)"
1856
1856
 
1857
1857
  msgid "job invocation"
1858
- msgstr ""
1858
+ msgstr "工作调用"
1859
1859
 
1860
1860
  msgid "must be an absolute path"
1861
- msgstr ""
1861
+ msgstr "必须是绝对路径"
1862
1862
 
1863
1863
  msgid "no"
1864
1864
  msgstr "否"
data/package.json CHANGED
@@ -3,12 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "license": "GPL-3.0",
5
5
  "scripts": {
6
- "lint": "tfm-lint --plugin -d /webpack",
7
- "lint:custom": "eslint ./webpack",
8
- "test": "tfm-test --plugin --config jest.config.js",
9
- "test:watch": "tfm-test --plugin --config jest.config.js --watchAll",
10
- "test:current": "tfm-test --plugin --config jest.config.js --watch",
11
- "publish-coverage": "tfm-publish-coverage"
6
+ "lint:custom": "eslint ./webpack"
12
7
  },
13
8
  "repository": {
14
9
  "type": "git",
@@ -19,11 +14,6 @@
19
14
  },
20
15
  "devDependencies": {
21
16
  "@babel/core": "^7.7.0",
22
- "@theforeman/builder": ">= 15.0.1",
23
- "@theforeman/eslint-plugin-foreman": ">= 15.0.1",
24
- "@theforeman/eslint-plugin-rules": ">= 15.0.1",
25
- "@theforeman/test": ">= 15.0.1",
26
- "@theforeman/vendor-dev": ">= 15.0.1",
27
17
  "babel-eslint": "^10.0.0",
28
18
  "eslint": "^6.8.0",
29
19
  "prettier": "^1.19.1",
@@ -32,8 +22,5 @@
32
22
  "graphql": "^15.5.0",
33
23
  "victory-core": "~36.8.6",
34
24
  "victory-pie": "~36.8.6"
35
- },
36
- "peerDependencies": {
37
- "@theforeman/vendor": ">= 15.0.1"
38
25
  }
39
26
  }
@@ -1,65 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'test_plugin_helper'
4
- require_relative '../support/remote_execution_helper'
5
4
 
6
5
  class JobInvocationsControllerTest < ActionController::TestCase
7
- test 'should parse inputs coming from the URL params' do
8
- template = FactoryBot.create(:job_template, :with_input)
9
- feature = FactoryBot.create(:remote_execution_feature,
10
- :job_template => template)
11
- params = {
12
- feature: feature.label,
13
- inputs: { template.template_inputs.first.name => 'foobar' },
14
- }
15
-
16
- get :new, params: params, session: set_session_user
17
- template_invocation_params = [
18
- {
19
- 'input_values' =>
20
- [
21
- {
22
- 'value' => 'foobar',
23
- 'template_input_id' => template.template_inputs.first.id,
24
- },
25
- ],
26
- 'template_id' => template.id,
27
- },
28
- ]
29
- assert_equal(template_invocation_params,
30
- assigns(:composer).params['template_invocations'])
31
- end
32
-
33
- test 'should allow no inputs' do
34
- template = FactoryBot.create(:job_template)
35
- feature = FactoryBot.create(:remote_execution_feature,
36
- :job_template => template)
37
- params = {
38
- feature: feature.label,
39
- }
40
- get :new, params: params, session: set_session_user
41
- template_invocation_params = [
42
- {
43
- 'template_id' => template.id,
44
- 'input_values' => {},
45
- },
46
- ]
47
- assert_equal(template_invocation_params,
48
- assigns(:composer).params['template_invocations'])
49
- end
50
-
51
- test 'new via GET and POST' do
52
- template = FactoryBot.create(:job_template, :with_input)
53
- feature = FactoryBot.create(:remote_execution_feature, job_template: template)
54
- params = { feature: feature.label, inputs: { template.template_inputs.first.name => 'foobar' } }
55
-
56
- get :new, params: params, session: set_session_user
57
- assert_response :success
58
-
59
- post :new, params: params, session: set_session_user
60
- assert_response :success
61
- end
62
-
63
6
  context '#report' do
64
7
  setup do
65
8
  @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
@@ -167,16 +110,6 @@ class JobInvocationsControllerTest < ActionController::TestCase
167
110
  assert 2, assigns(:job_invocations).size
168
111
  end
169
112
 
170
- test '#show' do
171
- get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
172
- assert_response :success
173
- end
174
-
175
- test '#rerun' do
176
- get :rerun, params: { id: @invocation2.id }, session: prepare_user(@admin)
177
- assert_response :success
178
- end
179
-
180
113
  test '#cancel' do
181
114
  ForemanTasks::Task.any_instance.expects(:cancel).returns(true)
182
115
  post :cancel, params: { id: @invocation2.id }, session: prepare_user(@admin)
@@ -192,16 +125,6 @@ class JobInvocationsControllerTest < ActionController::TestCase
192
125
  assert_equal @invocation2, assigns(:job_invocations)[0]
193
126
  end
194
127
 
195
- test '#show' do
196
- get :show, params: { id: @invocation.id }, session: prepare_user(@user)
197
- assert_response :not_found
198
- end
199
-
200
- test '#rerun' do
201
- get :rerun, params: { id: @invocation.id }, session: prepare_user(@user)
202
- assert_response :not_found
203
- end
204
-
205
128
  test 'cancel' do
206
129
  post :cancel, params: { id: @invocation.id }, session: prepare_user(@user)
207
130
  assert_response :not_found
@@ -16,6 +16,8 @@ export const DIRECT_OPEN_HOST_LIMIT = 3;
16
16
  export const AWAITING_STATUS_FILTER = '(job_invocation.result = N/A)';
17
17
 
18
18
  export const AUTO_REFRESH_INTERVAL_MS = 5000;
19
+ export const OUTPUT_REFRESH_INTERVAL_MS = 1000;
20
+ export const OUTPUT_MAX_REFRESH_INTERVAL_MS = 3000;
19
21
  export const DEFAULT_CHART_LEGEND_WIDTH = 270;
20
22
  export const CLIPBOARD_COPIED_EXIT_DELAY_MS = 1500;
21
23
  export const CLIPBOARD_DEFAULT_EXIT_DELAY_MS = 600;
@@ -23,6 +25,9 @@ export const CLIPBOARD_DEFAULT_EXIT_DELAY_MS = 600;
23
25
  export const showTemplateInvocationUrl = (hostID, jobID) =>
24
26
  `/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
25
27
 
28
+ export const templateInvocationOutputUrl = (hostID, jobID) =>
29
+ `/api/job_invocations/${jobID}/hosts/${hostID}`;
30
+
26
31
  export const templateInvocationPageUrl = (hostID, jobID) =>
27
32
  `/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
28
33
 
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
2
2
  import React, { useCallback, useMemo, useState } from 'react';
3
3
  import { useDispatch, useSelector } from 'react-redux';
4
4
  import { Button, Split, SplitItem } from '@patternfly/react-core';
5
- import { UndoIcon } from '@patternfly/react-icons';
6
5
  import {
7
6
  Dropdown,
8
7
  DropdownItem,
@@ -153,15 +152,6 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
153
152
  {__('Abort')}
154
153
  </DropdownItem>,
155
154
  ...recurrenceDropdownItems,
156
- <DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
157
- <DropdownItem
158
- ouiaId="legacy-ui-dropdown-item"
159
- icon={<UndoIcon />}
160
- href={`/legacy/job_invocations/${jobId}`}
161
- key="legacy-ui"
162
- >
163
- {__('Legacy UI')}
164
- </DropdownItem>,
165
155
  ],
166
156
  [
167
157
  canCancelJobInvocations,
@@ -1,17 +1,13 @@
1
- import React, { useEffect, useRef } from 'react';
1
+ import React from 'react';
2
2
  import { isEmpty } from 'lodash';
3
3
  import PropTypes from 'prop-types';
4
4
  import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
5
- import { useDispatch, useSelector } from 'react-redux';
6
- import { APIActions } from 'foremanReact/redux/API';
5
+ import { useSelector } from 'react-redux';
7
6
  import { translate as __ } from 'foremanReact/common/I18n';
8
7
  import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
9
8
  import { STATUS } from 'foremanReact/constants';
10
9
  import {
11
- showTemplateInvocationUrl,
12
10
  templateInvocationPageUrl,
13
- GET_TEMPLATE_INVOCATION,
14
- AUTO_REFRESH_INTERVAL_MS,
15
11
  CLIPBOARD_COPIED_EXIT_DELAY_MS,
16
12
  CLIPBOARD_DEFAULT_EXIT_DELAY_MS,
17
13
  } from './JobInvocationConstants';
@@ -19,6 +15,7 @@ import {
19
15
  selectTemplateInvocationStatus,
20
16
  selectTemplateInvocation,
21
17
  } from './JobInvocationSelectors';
18
+ import { useTemplateInvocationOutputPolling } from './useTemplateInvocationOutputPolling';
22
19
  import { OutputToggleGroup } from './TemplateInvocationComponents/OutputToggleGroup';
23
20
  import { PreviewTemplate } from './TemplateInvocationComponents/PreviewTemplate';
24
21
  import { OutputCodeBlock } from './TemplateInvocationComponents/OutputCodeBlock';
@@ -74,64 +71,16 @@ export const TemplateInvocation = ({
74
71
  showCommand,
75
72
  setShowCommand,
76
73
  }) => {
77
- const timeoutRef = useRef(null);
78
- const templateURL = showTemplateInvocationUrl(hostID, jobID);
79
74
  const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
80
75
 
81
76
  const status = useSelector(selectTemplateInvocationStatus(hostID));
82
77
  const response = useSelector(selectTemplateInvocation(hostID));
83
- const dispatch = useDispatch();
84
-
85
- const responseRef = useRef(response);
86
- useEffect(() => {
87
- responseRef.current = response;
88
- }, [response]);
89
-
90
- useEffect(() => {
91
- let cancelled = false;
92
-
93
- const schedulePoll = () => {
94
- if (cancelled) return;
95
- dispatch(
96
- APIActions.get({
97
- url: templateURL,
98
- key: `${GET_TEMPLATE_INVOCATION}_${hostID}`,
99
- handleSuccess: ({ data }) => {
100
- if (cancelled) return;
101
- const isFinished = data?.finished ?? true;
102
- // eslint-disable-next-line camelcase
103
- const autoRefresh = data?.auto_refresh || false;
104
- if (!isFinished && autoRefresh) {
105
- timeoutRef.current = setTimeout(
106
- schedulePoll,
107
- AUTO_REFRESH_INTERVAL_MS
108
- );
109
- } else {
110
- timeoutRef.current = null;
111
- }
112
- },
113
- handleError: () => {
114
- if (cancelled) return;
115
- timeoutRef.current = null;
116
- },
117
- })
118
- );
119
- };
120
-
121
- clearTimeout(timeoutRef.current);
122
- timeoutRef.current = null;
123
-
124
- if (isExpanded) {
125
- if (responseRef.current?.finished) return undefined;
126
- schedulePoll();
127
- }
128
-
129
- return () => {
130
- cancelled = true;
131
- clearTimeout(timeoutRef.current);
132
- timeoutRef.current = null;
133
- };
134
- }, [isExpanded, dispatch, templateURL, hostID]);
78
+ const liveOutput = useTemplateInvocationOutputPolling({
79
+ hostID,
80
+ jobID,
81
+ isExpanded,
82
+ response,
83
+ });
135
84
 
136
85
  if (!response || (status === STATUS.PENDING && isEmpty(response))) {
137
86
  return <Skeleton data-testid="template-invocation-skeleton" />;
@@ -156,13 +105,7 @@ export const TemplateInvocation = ({
156
105
  );
157
106
  }
158
107
 
159
- const {
160
- preview,
161
- output,
162
- input_values: inputValues,
163
- task,
164
- permissions,
165
- } = response;
108
+ const { preview, input_values: inputValues, task, permissions } = response;
166
109
  const { id: taskID, cancellable: taskCancellable } = task || {};
167
110
 
168
111
  return (
@@ -183,7 +126,7 @@ export const TemplateInvocation = ({
183
126
  isInTableView={isInTableView}
184
127
  copyToClipboard={
185
128
  <CopyToClipboard
186
- fullOutput={output
129
+ fullOutput={liveOutput
187
130
  ?.filter(
188
131
  ({ output_type: outputType }) => showOutputType[outputType]
189
132
  )
@@ -226,7 +169,7 @@ export const TemplateInvocation = ({
226
169
  </>
227
170
  )}
228
171
  <OutputCodeBlock
229
- code={output || []}
172
+ code={liveOutput}
230
173
  showOutputType={showOutputType}
231
174
  scrollElement={
232
175
  isInTableView
@@ -9,58 +9,87 @@ import { Button } from '@patternfly/react-core';
9
9
  import { translate as __ } from 'foremanReact/common/I18n';
10
10
  import { MS_PER_SECOND } from 'foremanReact/constants';
11
11
 
12
+ // eslint-disable-next-line no-control-regex
13
+ const COLOR_PATTERN = /\x1b\[[\d;]*m/g;
14
+ const CONSOLE_COLOR = {
15
+ '31': 'red',
16
+ '32': 'lightgreen',
17
+ '33': 'orange',
18
+ '34': 'deepskyblue',
19
+ '35': 'mediumpurple',
20
+ '36': 'cyan',
21
+ '37': 'grey',
22
+ '91': 'red',
23
+ '92': 'lightgreen',
24
+ '93': 'yellow',
25
+ '94': 'lightblue',
26
+ '95': 'violet',
27
+ '96': 'turquoise',
28
+ '0': 'default',
29
+ '39': 'default',
30
+ };
31
+ const parsedOutput = new WeakMap();
32
+
33
+ const colorizeLine = line => {
34
+ line = line.replace(COLOR_PATTERN, seq => {
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}}}}`;
39
+ });
40
+
41
+ let currentColor = 'default';
42
+ const parts = line.split(/({{{format.*?}}})/).filter(Boolean);
43
+ if (parts.length === 0) return <span>{'\n'}</span>;
44
+
45
+ // eslint-disable-next-line array-callback-return, consistent-return
46
+ return parts.map((consoleLine, index) => {
47
+ if (consoleLine.includes('{{{format')) {
48
+ const colorMatch = consoleLine.match(/color:(\d+)/);
49
+ if (colorMatch) {
50
+ const colorIndex = colorMatch[1];
51
+ currentColor = CONSOLE_COLOR[colorIndex] || 'default';
52
+ }
53
+ } else {
54
+ return (
55
+ <span key={index} style={{ color: currentColor }}>
56
+ {consoleLine.length ? consoleLine : '\n'}
57
+ </span>
58
+ );
59
+ }
60
+ });
61
+ };
62
+
63
+ const getLineOutputs = lineSet => {
64
+ if (parsedOutput.has(lineSet)) return parsedOutput.get(lineSet);
65
+
66
+ const lineOutputs =
67
+ lineSet.output === '\n'
68
+ ? []
69
+ : lineSet.output
70
+ .replace(/\r\n/g, '\n')
71
+ .replace(/\n$/, '')
72
+ .split('\n');
73
+ parsedOutput.set(lineSet, lineOutputs);
74
+ return lineOutputs;
75
+ };
76
+
77
+ const OutputLineSet = React.memo(({ lineSet, startLine }) =>
78
+ getLineOutputs(lineSet).map((lineOutput, index) => (
79
+ <div key={index} className={`line ${lineSet.output_type}`}>
80
+ <span
81
+ className="counter"
82
+ title={new Date(lineSet.timestamp * MS_PER_SECOND).toISOString()}
83
+ >
84
+ {(startLine + index).toString().padStart(4, '\u00A0')}:{' '}
85
+ </span>
86
+ <div className="content">{colorizeLine(lineOutput)}</div>
87
+ </div>
88
+ ))
89
+ );
90
+
12
91
  export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
13
92
  let lineCounter = 0;
14
- // eslint-disable-next-line no-control-regex
15
- const COLOR_PATTERN = /\x1b\[[\d;]*m/g;
16
- const CONSOLE_COLOR = {
17
- '31': 'red',
18
- '32': 'lightgreen',
19
- '33': 'orange',
20
- '34': 'deepskyblue',
21
- '35': 'mediumpurple',
22
- '36': 'cyan',
23
- '37': 'grey',
24
- '91': 'red',
25
- '92': 'lightgreen',
26
- '93': 'yellow',
27
- '94': 'lightblue',
28
- '95': 'violet',
29
- '96': 'turquoise',
30
- '0': 'default',
31
- '39': 'default',
32
- };
33
-
34
- const colorizeLine = line => {
35
- line = line.replace(COLOR_PATTERN, seq => {
36
- const codes = seq.match(/(\d+)/g) || [];
37
- const lastColorCode =
38
- [...codes].reverse().find(code_ => code_ in CONSOLE_COLOR) || '0';
39
- return `{{{format color:${lastColorCode}}}}`;
40
- });
41
-
42
- let currentColor = 'default';
43
- const parts = line.split(/({{{format.*?}}})/).filter(Boolean);
44
- if (parts.length === 0) {
45
- return <span>{'\n'}</span>;
46
- }
47
- // eslint-disable-next-line array-callback-return, consistent-return
48
- return parts.map((consoleLine, index) => {
49
- if (consoleLine.includes('{{{format')) {
50
- const colorMatch = consoleLine.match(/color:(\d+)/);
51
- if (colorMatch) {
52
- const colorIndex = colorMatch[1];
53
- currentColor = CONSOLE_COLOR[colorIndex] || 'default';
54
- }
55
- } else {
56
- return (
57
- <span key={index} style={{ color: currentColor }}>
58
- {consoleLine.length ? consoleLine : '\n'}
59
- </span>
60
- );
61
- }
62
- });
63
- };
64
93
  const filteredCode = code.filter(
65
94
  ({ output_type: outputType }) => showOutputType[outputType]
66
95
  );
@@ -109,28 +138,16 @@ export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
109
138
  if (!filteredCode.length) {
110
139
  return <div>{__('No output for the selected filters')}</div>;
111
140
  }
112
- const codeParse = filteredCode.map(line => {
113
- if (line.output === '\n') {
114
- return null;
115
- }
116
- const lineOutputs = line.output
117
- .replace(/\r\n/g, '\n')
118
- .replace(/\n$/, '')
119
- .split('\n');
120
- return lineOutputs.map((lineOutput, index) => {
121
- lineCounter += 1;
122
- return (
123
- <div key={index} className={`line ${line.output_type}`}>
124
- <span
125
- className="counter"
126
- title={new Date(line.timestamp * MS_PER_SECOND).toISOString()}
127
- >
128
- {lineCounter.toString().padStart(4, '\u00A0')}:{' '}
129
- </span>
130
- <div className="content">{colorizeLine(lineOutput)}</div>
131
- </div>
132
- );
133
- });
141
+ const codeParse = filteredCode.map((lineSet, index) => {
142
+ const startLine = lineCounter + 1;
143
+ lineCounter += getLineOutputs(lineSet).length;
144
+ return (
145
+ <OutputLineSet
146
+ key={`${lineSet.timestamp}-${index}`}
147
+ lineSet={lineSet}
148
+ startLine={startLine}
149
+ />
150
+ );
134
151
  });
135
152
 
136
153
  return (
@@ -167,3 +184,8 @@ OutputCodeBlock.propTypes = {
167
184
  showOutputType: PropTypes.object.isRequired,
168
185
  scrollElement: PropTypes.string.isRequired,
169
186
  };
187
+
188
+ OutputLineSet.propTypes = {
189
+ lineSet: PropTypes.object.isRequired,
190
+ startLine: PropTypes.number.isRequired,
191
+ };