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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +28 -28
- data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +48 -48
- data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +29 -29
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +22 -22
- data/app/assets/javascripts/foreman_remote_execution/template_invocation.js +0 -170
- data/app/assets/stylesheets/foreman_remote_execution/job_invocations.scss +0 -6
- data/app/controllers/job_invocations_controller.rb +1 -98
- data/app/helpers/job_invocations_helper.rb +0 -49
- data/app/helpers/remote_execution_helper.rb +23 -123
- data/config/routes.rb +0 -6
- data/lib/foreman_remote_execution/plugin.rb +2 -2
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/foreman_remote_execution.pot +2 -2
- data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/fr/foreman_remote_execution.po +28 -28
- data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ja/foreman_remote_execution.po +48 -48
- data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ko/foreman_remote_execution.po +29 -29
- data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_CN/foreman_remote_execution.po +22 -22
- data/package.json +1 -14
- data/test/functional/job_invocations_controller_test.rb +0 -77
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +5 -0
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +0 -10
- data/webpack/JobInvocationDetail/TemplateInvocation.js +12 -69
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +94 -72
- data/webpack/JobInvocationDetail/TemplateInvocationHelpers.js +33 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +0 -6
- data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +5 -1
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationHelpers.test.js +94 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +338 -139
- data/webpack/JobInvocationDetail/useTemplateInvocationOutputPolling.js +185 -0
- data/webpack/JobWizard/JobWizardPageRerun.js +0 -10
- data/webpack/JobWizard/__tests__/autofill.test.js +23 -0
- data/webpack/JobWizard/__tests__/integration.test.js +1 -1
- data/webpack/JobWizard/autofill.js +18 -4
- data/webpack/JobWizard/index.js +1 -18
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +49 -1
- data/webpack/global_index.js +0 -2
- data/webpack/index.js +0 -15
- data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +1 -1
- metadata +6 -53
- data/app/assets/stylesheets/foreman_remote_execution/modal_window.scss +0 -4
- data/app/helpers/job_invocations_chart_helper.rb +0 -80
- data/app/views/job_invocations/_card_results.html.erb +0 -12
- data/app/views/job_invocations/_card_schedule.html.erb +0 -33
- data/app/views/job_invocations/_card_target_hosts.html.erb +0 -57
- data/app/views/job_invocations/_card_user_input.html.erb +0 -16
- data/app/views/job_invocations/_description_fields.html.erb +0 -4
- data/app/views/job_invocations/_form.html.erb +0 -144
- data/app/views/job_invocations/_preview_hosts_modal.html.erb +0 -15
- data/app/views/job_invocations/_rerun_taxonomies.html.erb +0 -22
- data/app/views/job_invocations/_tab_hosts.html.erb +0 -11
- data/app/views/job_invocations/_tab_overview.html.erb +0 -41
- data/app/views/job_invocations/_tab_preview_templates.html.erb +0 -20
- data/app/views/job_invocations/_user_input.html.erb +0 -21
- data/app/views/job_invocations/new.html.erb +0 -9
- data/app/views/job_invocations/refresh.js.erb +0 -4
- data/app/views/job_invocations/show.html.erb +0 -59
- data/app/views/job_invocations/show.js.erb +0 -12
- data/app/views/job_invocations/show.json.erb +0 -5
- data/test/support/remote_execution_helper.rb +0 -5
- data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +0 -62
- data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +0 -2
- data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +0 -10
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.js +0 -45
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +0 -30
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +0 -78
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
- data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +0 -25
- data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +0 -76
- data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +0 -6
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +0 -6
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +0 -9
- data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +0 -31
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +0 -12
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +0 -96
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +0 -75
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +0 -16
- data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +0 -56
- data/webpack/react_app/components/TargetingHosts/components/HostItem.js +0 -39
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +0 -54
- data/webpack/react_app/components/TargetingHosts/index.js +0 -150
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +0 -60
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +0 -76
- data/webpack/react_app/components/jobInvocations/index.js +0 -94
- data/webpack/react_app/extend/reducers.js +0 -5
- data/webpack/react_app/redux/actions/jobInvocations/index.js +0 -87
- data/webpack/react_app/redux/consts.js +0 -5
- data/webpack/react_app/redux/reducers/index.js +0 -6
- data/webpack/react_app/redux/reducers/jobInvocations/index.fixtures.js +0 -46
- data/webpack/react_app/redux/reducers/jobInvocations/index.js +0 -34
- 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 "
|
|
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": "
|
|
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
|
|
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 {
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
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={
|
|
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={
|
|
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(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
};
|