foreman_remote_execution 18.0.0 → 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/de/foreman_remote_execution.js +54 -9
- data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +54 -9
- data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +54 -9
- data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +66 -21
- data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +86 -41
- data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +111 -66
- data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +67 -22
- data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +54 -9
- data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +54 -9
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +60 -15
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +54 -9
- 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/app/models/remote_execution_provider.rb +1 -1
- 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/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/de/foreman_remote_execution.po +54 -9
- data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/en_GB/foreman_remote_execution.po +54 -9
- data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/es/foreman_remote_execution.po +54 -9
- data/locale/foreman_remote_execution.pot +296 -230
- data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/fr/foreman_remote_execution.po +66 -21
- data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ja/foreman_remote_execution.po +86 -41
- data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ka/foreman_remote_execution.po +111 -66
- data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ko/foreman_remote_execution.po +67 -22
- data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/pt_BR/foreman_remote_execution.po +54 -9
- data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ru/foreman_remote_execution.po +54 -9
- data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_CN/foreman_remote_execution.po +60 -15
- data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_TW/foreman_remote_execution.po +54 -9
- data/package.json +1 -14
- data/test/functional/job_invocations_controller_test.rb +0 -77
- data/test/unit/remote_execution_provider_test.rb +1 -1
- 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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const getLastOutputTimestamp = output =>
|
|
2
|
+
output.length ? output[output.length - 1].timestamp : null;
|
|
3
|
+
|
|
4
|
+
export const normalizeOutput = output =>
|
|
5
|
+
output.reduce((normalized, lineSet) => {
|
|
6
|
+
const previousLineSet = normalized[normalized.length - 1];
|
|
7
|
+
|
|
8
|
+
if (
|
|
9
|
+
previousLineSet &&
|
|
10
|
+
previousLineSet.output_type === lineSet.output_type &&
|
|
11
|
+
!previousLineSet.output.endsWith('\n')
|
|
12
|
+
) {
|
|
13
|
+
const firstCompleteLine = lineSet.output.match(/^[^\n]*\n/);
|
|
14
|
+
if (firstCompleteLine) {
|
|
15
|
+
normalized[normalized.length - 1] = {
|
|
16
|
+
...previousLineSet,
|
|
17
|
+
output: previousLineSet.output + firstCompleteLine[0],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const remainder = lineSet.output.slice(firstCompleteLine[0].length);
|
|
21
|
+
if (remainder) normalized.push({ ...lineSet, output: remainder });
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
normalized.push(lineSet);
|
|
27
|
+
return normalized;
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
export const mergeOutput = (currentOutput, newOutput) => {
|
|
31
|
+
if (!newOutput.length) return currentOutput;
|
|
32
|
+
return normalizeOutput([...currentOutput, ...newOutput]);
|
|
33
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import '@testing-library/jest-dom
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
4
|
import { createMemoryHistory } from 'history';
|
|
5
5
|
import { Router } from 'react-router-dom';
|
|
6
6
|
import JobInvocationEmptyState from '../JobInvocationEmptyState';
|
|
@@ -245,12 +245,6 @@ describe('JobInvocationDetailPage', () => {
|
|
|
245
245
|
expect(screen.getByText('Abort')).toBeInTheDocument();
|
|
246
246
|
expect(screen.queryByText('Enable recurring')).not.toBeInTheDocument();
|
|
247
247
|
expect(screen.queryByText('Cancel recurring')).not.toBeInTheDocument();
|
|
248
|
-
expect(
|
|
249
|
-
screen
|
|
250
|
-
.getByText('Legacy UI')
|
|
251
|
-
.closest('a')
|
|
252
|
-
.getAttribute('href')
|
|
253
|
-
).toEqual(`/legacy/job_invocations/${jobId}`);
|
|
254
248
|
});
|
|
255
249
|
|
|
256
250
|
it('keeps toolbar buttons mounted while job invocation data is refreshing', async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
-
import '@testing-library/jest-dom
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
4
|
import { OutputCodeBlock } from '../TemplateInvocationComponents/OutputCodeBlock';
|
|
5
5
|
import { jobInvocationOutput } from './fixtures';
|
|
6
6
|
|
|
@@ -2,13 +2,16 @@ import React from 'react';
|
|
|
2
2
|
import configureMockStore from 'redux-mock-store';
|
|
3
3
|
import { Provider } from 'react-redux';
|
|
4
4
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
5
|
-
import '@testing-library/jest-dom
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
7
|
+
import API from 'foremanReact/API';
|
|
6
8
|
import * as api from 'foremanReact/redux/API';
|
|
7
9
|
import * as selectors from '../JobInvocationSelectors';
|
|
8
10
|
import { TemplateInvocation } from '../TemplateInvocation';
|
|
9
11
|
import { mockTemplateInvocationResponse } from './fixtures';
|
|
10
12
|
|
|
11
13
|
jest.spyOn(api, 'get');
|
|
14
|
+
jest.mock('foremanReact/API');
|
|
12
15
|
jest.mock('../JobInvocationSelectors');
|
|
13
16
|
|
|
14
17
|
jest.mock('foremanReact/components/ToastsList', () => ({
|
|
@@ -47,6 +50,7 @@ const mockProps = {
|
|
|
47
50
|
|
|
48
51
|
describe('TemplateInvocation', () => {
|
|
49
52
|
beforeEach(() => {
|
|
53
|
+
API.get.mockImplementation(() => new Promise(() => {}));
|
|
50
54
|
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
51
55
|
'RESOLVED'
|
|
52
56
|
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLastOutputTimestamp,
|
|
3
|
+
mergeOutput,
|
|
4
|
+
normalizeOutput,
|
|
5
|
+
} from '../TemplateInvocationHelpers';
|
|
6
|
+
|
|
7
|
+
describe('TemplateInvocationHelpers', () => {
|
|
8
|
+
describe('getLastOutputTimestamp', () => {
|
|
9
|
+
it('returns null for empty output', () => {
|
|
10
|
+
expect(getLastOutputTimestamp([])).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns the timestamp of the last output chunk', () => {
|
|
14
|
+
expect(
|
|
15
|
+
getLastOutputTimestamp([
|
|
16
|
+
{ timestamp: 1, output: 'first\n' },
|
|
17
|
+
{ timestamp: 2, output: 'second\n' },
|
|
18
|
+
])
|
|
19
|
+
).toBe(2);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('normalizeOutput', () => {
|
|
24
|
+
it('joins lines split between chunks in one response', () => {
|
|
25
|
+
const result = normalizeOutput([
|
|
26
|
+
{ timestamp: 1, output_type: 'stdout', output: 'hel' },
|
|
27
|
+
{
|
|
28
|
+
timestamp: 2,
|
|
29
|
+
output_type: 'stdout',
|
|
30
|
+
output: 'lo\r\nworld\r\n',
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual([
|
|
35
|
+
{ timestamp: 1, output_type: 'stdout', output: 'hello\r\n' },
|
|
36
|
+
{ timestamp: 2, output_type: 'stdout', output: 'world\r\n' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('mergeOutput', () => {
|
|
42
|
+
it('appends new chunks without changing existing objects', () => {
|
|
43
|
+
const existingChunk = { timestamp: 1, output: 'first\n' };
|
|
44
|
+
const newChunk = { timestamp: 2, output: 'second\n' };
|
|
45
|
+
|
|
46
|
+
const result = mergeOutput([existingChunk], [newChunk]);
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual([existingChunk, newChunk]);
|
|
49
|
+
expect(result[0]).toBe(existingChunk);
|
|
50
|
+
expect(result[1]).toBe(newChunk);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('joins a line split across two polling responses', () => {
|
|
54
|
+
const result = mergeOutput(
|
|
55
|
+
[{ timestamp: 1, output: 'partial ' }],
|
|
56
|
+
[{ timestamp: 2, output: 'line\nnext line\n' }]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual([
|
|
60
|
+
{ timestamp: 1, output: 'partial line\n' },
|
|
61
|
+
{ timestamp: 2, output: 'next line\n' },
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not join chunks of different output types', () => {
|
|
66
|
+
const result = mergeOutput(
|
|
67
|
+
[{ timestamp: 1, output_type: 'stdout', output: 'partial ' }],
|
|
68
|
+
[{ timestamp: 2, output_type: 'stderr', output: 'error\n' }]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual([
|
|
72
|
+
{ timestamp: 1, output_type: 'stdout', output: 'partial ' },
|
|
73
|
+
{ timestamp: 2, output_type: 'stderr', output: 'error\n' },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not append an empty chunk after completing a split line', () => {
|
|
78
|
+
const result = mergeOutput(
|
|
79
|
+
[{ timestamp: 1, output_type: 'stdout', output: 'partial ' }],
|
|
80
|
+
[{ timestamp: 2, output_type: 'stdout', output: 'line\n' }]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual([
|
|
84
|
+
{ timestamp: 1, output_type: 'stdout', output: 'partial line\n' },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns the current output when there is nothing to append', () => {
|
|
89
|
+
const currentOutput = [{ timestamp: 1, output: 'first\n' }];
|
|
90
|
+
|
|
91
|
+
expect(mergeOutput(currentOutput, [])).toBe(currentOutput);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|