foreman_remote_execution 8.1.1 → 9.0.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/.github/workflows/ruby_ci.yml +3 -1
- data/app/lib/actions/remote_execution/proxy_action.rb +3 -2
- data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +4 -1
- data/app/models/job_invocation.rb +3 -2
- data/app/models/template_invocation.rb +1 -0
- data/lib/foreman_remote_execution/engine.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/job_invocation_test.rb +1 -0
- data/webpack/JobWizard/JobWizard.scss +1 -5
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
- data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +4 -28
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +0 -4
- data/webpack/JobWizard/steps/form/Formatter.js +8 -30
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.js +45 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +26 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +13 -1
- data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +5 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +3 -0
- data/webpack/react_app/components/TargetingHosts/index.js +23 -3
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +25 -9
- data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +6 -2
- data/webpack/react_app/components/jobInvocations/index.js +19 -2
- data/webpack/react_app/redux/actions/jobInvocations/index.js +8 -0
- data/webpack/react_app/redux/reducers/jobInvocations/index.js +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4f0a9c455ec484caac7631cec3d1ed8eff22263c96036d7837fdbb86f3faab8
|
4
|
+
data.tar.gz: a73788ec230b20cfd6050733d40f27a59d7bf26d334ce51f0b422aae1a64a68e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb1ef923190e046bc4f9c688ac1040f26e521e7a96a7a154fd461a650d25b04875572e26b75f57dfb425401ae2bf8e82447eb01c635006eb9b51b36df168b999
|
7
|
+
data.tar.gz: 3e47f08ded8554ff2b38c50f7d16b8705441ece2753df227181d7782426c7c320476220f5b12dee496216cda119467ff3a0348f7821811dda702e8a9d852c1f3
|
@@ -13,6 +13,8 @@ jobs:
|
|
13
13
|
runs-on: ubuntu-latest
|
14
14
|
steps:
|
15
15
|
- uses: actions/checkout@v2
|
16
|
+
- run: sudo apt-get update
|
17
|
+
- run: sudo apt-get install libyaml-dev
|
16
18
|
- name: Setup Ruby
|
17
19
|
uses: ruby/setup-ruby@v1
|
18
20
|
with:
|
@@ -42,7 +44,7 @@ jobs:
|
|
42
44
|
node-version: [12]
|
43
45
|
steps:
|
44
46
|
- run: sudo apt-get update
|
45
|
-
- run: sudo apt-get install build-essential libcurl4-openssl-dev zlib1g-dev libpq-dev
|
47
|
+
- run: sudo apt-get install build-essential libcurl4-openssl-dev zlib1g-dev libpq-dev libyaml-dev
|
46
48
|
- uses: actions/checkout@v2
|
47
49
|
with:
|
48
50
|
repository: theforeman/foreman
|
@@ -33,11 +33,12 @@ module Actions
|
|
33
33
|
}
|
34
34
|
end
|
35
35
|
if data['exit_status']
|
36
|
+
last = events.last || {:sequence_id => -1, :timestamp => Time.zone.now}
|
36
37
|
events << {
|
37
|
-
sequence_id:
|
38
|
+
sequence_id: last[:sequence_id] + 1,
|
38
39
|
template_invocation_id: template_invocation.id,
|
39
40
|
event: data['exit_status'],
|
40
|
-
timestamp:
|
41
|
+
timestamp: last[:timestamp],
|
41
42
|
event_type: 'exit',
|
42
43
|
}
|
43
44
|
end
|
@@ -6,10 +6,13 @@ module Actions
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def log_template_invocation_exception(exception)
|
9
|
+
last = template_invocation.template_invocation_events.order(:sequence_id).last
|
10
|
+
id = last ? last.sequence_id + 1 : 0
|
9
11
|
template_invocation.template_invocation_events.create!(
|
10
12
|
:event_type => 'debug',
|
11
13
|
:event => "#{exception.class}: #{exception.message}",
|
12
|
-
:timestamp => Time.zone.now
|
14
|
+
:timestamp => Time.zone.now,
|
15
|
+
:sequence_id => id
|
13
16
|
)
|
14
17
|
end
|
15
18
|
|
@@ -253,8 +253,9 @@ class JobInvocation < ApplicationRecord
|
|
253
253
|
acc.merge(key => 0)
|
254
254
|
end
|
255
255
|
else
|
256
|
-
counts
|
257
|
-
|
256
|
+
counts = task.sub_tasks_counts
|
257
|
+
counts.default = 0
|
258
|
+
done = counts.values_at(*map.results).reduce(:+)
|
258
259
|
percent = progress(counts[:total], done)
|
259
260
|
counts.merge(:progress => percent, :failed => counts.values_at(*map.status_to_task_result(:failed)).reduce(:+))
|
260
261
|
end
|
@@ -47,7 +47,7 @@ module ForemanRemoteExecution
|
|
47
47
|
|
48
48
|
initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
|
49
49
|
Foreman::Plugin.register :foreman_remote_execution do
|
50
|
-
requires_foreman '>= 3.
|
50
|
+
requires_foreman '>= 3.6'
|
51
51
|
register_global_js_file 'global'
|
52
52
|
|
53
53
|
apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
.job-wizard {
|
2
|
+
font-size: var(--pf-global--FontSize--md);
|
2
3
|
.wizard-title {
|
3
4
|
margin-bottom: 25px;
|
4
5
|
}
|
@@ -134,11 +135,6 @@
|
|
134
135
|
margin-left: 10px;
|
135
136
|
}
|
136
137
|
}
|
137
|
-
.foreman-search-field {
|
138
|
-
.autocomplete-search-btn {
|
139
|
-
display: none;
|
140
|
-
}
|
141
|
-
}
|
142
138
|
.pf-c-radio__body {
|
143
139
|
font-size: var(--pf-c-radio__label--FontSize);
|
144
140
|
}
|
@@ -1,33 +1,10 @@
|
|
1
|
-
import React
|
2
|
-
import { useSelector, useDispatch } from 'react-redux';
|
1
|
+
import React from 'react';
|
3
2
|
import PropTypes from 'prop-types';
|
4
3
|
import SearchBar from 'foremanReact/components/SearchBar';
|
5
4
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
6
|
-
import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
|
7
|
-
import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
|
8
5
|
import { hostsController, hostQuerySearchID } from '../../JobWizardConstants';
|
9
|
-
import { noop } from '../../../helpers';
|
10
6
|
|
11
7
|
export const HostSearch = ({ value, setValue }) => {
|
12
|
-
const searchQuery = useSelector(
|
13
|
-
state => state.autocomplete?.[hostQuerySearchID]?.searchQuery
|
14
|
-
);
|
15
|
-
useEffect(() => {
|
16
|
-
setValue(searchQuery || '');
|
17
|
-
}, [setValue, searchQuery]);
|
18
|
-
const dispatch = useDispatch();
|
19
|
-
const setSearch = newSearchQuery => {
|
20
|
-
dispatch(
|
21
|
-
getResults({
|
22
|
-
url: '/hosts/auto_complete_search',
|
23
|
-
searchQuery: newSearchQuery,
|
24
|
-
controller: 'hostsController',
|
25
|
-
trigger: TRIGGERS.INPUT_CHANGE,
|
26
|
-
id: hostQuerySearchID,
|
27
|
-
})
|
28
|
-
);
|
29
|
-
};
|
30
|
-
|
31
8
|
const props = getControllerSearchProps(hostsController, hostQuerySearchID);
|
32
9
|
return (
|
33
10
|
<div className="foreman-search-field">
|
@@ -37,12 +14,11 @@ export const HostSearch = ({ value, setValue }) => {
|
|
37
14
|
autocomplete: {
|
38
15
|
id: hostQuerySearchID,
|
39
16
|
url: '/hosts/auto_complete_search',
|
40
|
-
|
17
|
+
searchQuery: value,
|
41
18
|
},
|
42
19
|
}}
|
43
|
-
onSearch={
|
44
|
-
|
45
|
-
onBookmarkClick={search => setSearch(search)}
|
20
|
+
onSearch={null}
|
21
|
+
onSearchChange={search => setValue(search)}
|
46
22
|
/>
|
47
23
|
</div>
|
48
24
|
);
|
@@ -13,7 +13,6 @@ import { FilterIcon } from '@patternfly/react-icons';
|
|
13
13
|
import { debounce } from 'lodash';
|
14
14
|
import { get } from 'foremanReact/redux/API';
|
15
15
|
import { translate as __ } from 'foremanReact/common/I18n';
|
16
|
-
import { resetData } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
|
17
16
|
import {
|
18
17
|
selectTemplateInputs,
|
19
18
|
selectWithKatello,
|
@@ -31,8 +30,6 @@ import {
|
|
31
30
|
HOST_COLLECTIONS,
|
32
31
|
HOST_GROUPS,
|
33
32
|
hostMethods,
|
34
|
-
hostsController,
|
35
|
-
hostQuerySearchID,
|
36
33
|
HOSTS_API,
|
37
34
|
HOSTS_TO_PREVIEW_AMOUNT,
|
38
35
|
DEBOUNCE_API,
|
@@ -100,7 +97,6 @@ const HostsAndInputs = ({
|
|
100
97
|
};
|
101
98
|
|
102
99
|
const clearSearch = () => {
|
103
|
-
dispatch(resetData(hostsController, hostQuerySearchID));
|
104
100
|
setHostsSearchQuery('');
|
105
101
|
};
|
106
102
|
return (
|
@@ -1,16 +1,12 @@
|
|
1
|
-
import React
|
2
|
-
import { useSelector, useDispatch } from 'react-redux';
|
1
|
+
import React from 'react';
|
3
2
|
import { FormGroup, TextArea } from '@patternfly/react-core';
|
4
3
|
import PropTypes from 'prop-types';
|
5
4
|
import SearchBar from 'foremanReact/components/SearchBar';
|
6
5
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
7
|
-
import { TRIGGERS } from 'foremanReact/components/AutoComplete/AutoCompleteConstants';
|
8
|
-
import { getResults } from 'foremanReact/components/AutoComplete/AutoCompleteActions';
|
9
6
|
import { helpLabel, ResetDefault } from './FormHelpers';
|
10
7
|
import { SelectField } from './SelectField';
|
11
8
|
import { ResourceSelect } from './ResourceSelect';
|
12
9
|
import { DateTimePicker } from '../form/DateTimePicker';
|
13
|
-
import { noop } from '../../../helpers';
|
14
10
|
|
15
11
|
const TemplateSearchField = ({
|
16
12
|
name,
|
@@ -22,51 +18,33 @@ const TemplateSearchField = ({
|
|
22
18
|
setValue,
|
23
19
|
values,
|
24
20
|
}) => {
|
25
|
-
const searchQuery = useSelector(
|
26
|
-
state => state.autocomplete?.[name]?.searchQuery
|
27
|
-
);
|
28
|
-
const dispatch = useDispatch();
|
29
|
-
useEffect(() => {
|
30
|
-
setValue({ ...values, [name]: searchQuery });
|
31
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
32
|
-
}, [searchQuery]);
|
33
21
|
const id = name.replace(/ /g, '-');
|
34
22
|
const props = getControllerSearchProps(controller.replace('/', '_'), name);
|
35
|
-
|
36
|
-
const setSearch = newSearchQuery => {
|
37
|
-
dispatch(
|
38
|
-
getResults({
|
39
|
-
url,
|
40
|
-
searchQuery: newSearchQuery,
|
41
|
-
controller,
|
42
|
-
trigger: TRIGGERS.INPUT_CHANGE,
|
43
|
-
id: name,
|
44
|
-
})
|
45
|
-
);
|
46
|
-
};
|
47
23
|
return (
|
48
24
|
<FormGroup
|
49
25
|
label={name}
|
50
26
|
labelIcon={helpLabel(labelText, name)}
|
51
27
|
labelInfo={
|
52
|
-
<ResetDefault
|
28
|
+
<ResetDefault
|
29
|
+
defaultValue={defaultValue}
|
30
|
+
setValue={search => setValue({ ...values, [name]: search })}
|
31
|
+
/>
|
53
32
|
}
|
54
33
|
fieldId={id}
|
55
34
|
isRequired={required}
|
56
35
|
className="foreman-search-field"
|
57
36
|
>
|
58
37
|
<SearchBar
|
59
|
-
initialQuery={values[name]}
|
60
38
|
data={{
|
61
39
|
...props,
|
62
40
|
autocomplete: {
|
63
41
|
id: name,
|
64
42
|
url,
|
65
|
-
|
43
|
+
searchQuery: values[name],
|
66
44
|
},
|
67
45
|
}}
|
68
|
-
onSearch={
|
69
|
-
|
46
|
+
onSearch={null}
|
47
|
+
onSearchChange={search => setValue({ ...values, [name]: search })}
|
70
48
|
/>
|
71
49
|
</FormGroup>
|
72
50
|
);
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Row, Label } from 'patternfly-react';
|
4
|
+
import { noop } from 'foremanReact/common/helpers';
|
5
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
6
|
+
|
7
|
+
import './TargetingHostsLabelsRow.scss';
|
8
|
+
|
9
|
+
const TargetingHostsLabelsRow = ({ query, updateQuery }) => {
|
10
|
+
const onDeleteClick = keyToDelete => {
|
11
|
+
const { [keyToDelete]: deleted, ...queryWithoutDeleted } = query;
|
12
|
+
updateQuery(queryWithoutDeleted);
|
13
|
+
};
|
14
|
+
|
15
|
+
const queryEntries = Object.entries(query);
|
16
|
+
|
17
|
+
return (
|
18
|
+
queryEntries.length > 0 && (
|
19
|
+
<Row className="tasks-labels-row">
|
20
|
+
<span className="title">{__('Active Filters:')}</span>
|
21
|
+
{queryEntries.map(([key, value]) => (
|
22
|
+
<Label
|
23
|
+
bsStyle="info"
|
24
|
+
key={key}
|
25
|
+
onRemoveClick={() => onDeleteClick(key)}
|
26
|
+
>
|
27
|
+
{`${key} = ${value}`}
|
28
|
+
</Label>
|
29
|
+
))}
|
30
|
+
</Row>
|
31
|
+
)
|
32
|
+
);
|
33
|
+
};
|
34
|
+
|
35
|
+
TargetingHostsLabelsRow.propTypes = {
|
36
|
+
query: PropTypes.object,
|
37
|
+
updateQuery: PropTypes.func,
|
38
|
+
};
|
39
|
+
|
40
|
+
TargetingHostsLabelsRow.defaultProps = {
|
41
|
+
query: {},
|
42
|
+
updateQuery: noop,
|
43
|
+
};
|
44
|
+
|
45
|
+
export default TargetingHostsLabelsRow;
|
@@ -0,0 +1,26 @@
|
|
1
|
+
@import '~@theforeman/vendor/scss/variables';
|
2
|
+
|
3
|
+
.tasks-labels-row {
|
4
|
+
margin: 0;
|
5
|
+
padding: 10px;
|
6
|
+
.title {
|
7
|
+
font-weight: 600;
|
8
|
+
font-size: 13px;
|
9
|
+
}
|
10
|
+
.label {
|
11
|
+
font-size: 100%;
|
12
|
+
|
13
|
+
margin-left: 5px;
|
14
|
+
margin-right: 5px;
|
15
|
+
a {
|
16
|
+
padding-left: 10px;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
.pficon-close {
|
20
|
+
color: $color-pf-white;
|
21
|
+
}
|
22
|
+
.compound-label-pf {
|
23
|
+
margin-left: 0;
|
24
|
+
margin: 10px;
|
25
|
+
}
|
26
|
+
}
|
@@ -5,14 +5,18 @@ import { Grid } from 'patternfly-react';
|
|
5
5
|
import SearchBar from 'foremanReact/components/SearchBar';
|
6
6
|
import Pagination from 'foremanReact/components/Pagination';
|
7
7
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
8
|
+
import { noop } from 'foremanReact/common/helpers';
|
8
9
|
|
9
10
|
import TargetingHosts from './TargetingHosts';
|
10
11
|
import { TARGETING_HOSTS_AUTOCOMPLETE } from './TargetingHostsConsts';
|
11
12
|
import './TargetingHostsPage.scss';
|
13
|
+
import TargetingHostsLabelsRow from './TargetingHostsLabelsRow';
|
12
14
|
|
13
15
|
const TargetingHostsPage = ({
|
14
16
|
handleSearch,
|
15
17
|
searchQuery,
|
18
|
+
statusFilter,
|
19
|
+
statusFilterReset,
|
16
20
|
apiStatus,
|
17
21
|
items,
|
18
22
|
totalHosts,
|
@@ -23,7 +27,7 @@ const TargetingHostsPage = ({
|
|
23
27
|
<Grid.Row>
|
24
28
|
<Grid.Col md={6} className="title_filter">
|
25
29
|
<SearchBar
|
26
|
-
onSearch={query => handleSearch(query)}
|
30
|
+
onSearch={query => handleSearch(query, statusFilter)}
|
27
31
|
data={{
|
28
32
|
...getControllerSearchProps('hosts', TARGETING_HOSTS_AUTOCOMPLETE),
|
29
33
|
autocomplete: {
|
@@ -37,6 +41,10 @@ const TargetingHostsPage = ({
|
|
37
41
|
/>
|
38
42
|
</Grid.Col>
|
39
43
|
</Grid.Row>
|
44
|
+
<TargetingHostsLabelsRow
|
45
|
+
query={statusFilter}
|
46
|
+
updateQuery={statusFilterReset}
|
47
|
+
/>
|
40
48
|
<br />
|
41
49
|
<TargetingHosts apiStatus={apiStatus} items={items} />
|
42
50
|
<Pagination
|
@@ -53,6 +61,8 @@ TargetingHostsPage.propTypes = {
|
|
53
61
|
handleSearch: PropTypes.func.isRequired,
|
54
62
|
searchQuery: PropTypes.string.isRequired,
|
55
63
|
apiStatus: PropTypes.string,
|
64
|
+
statusFilter: PropTypes.object,
|
65
|
+
statusFilterReset: PropTypes.func,
|
56
66
|
items: PropTypes.array.isRequired,
|
57
67
|
totalHosts: PropTypes.number.isRequired,
|
58
68
|
handlePagination: PropTypes.func.isRequired,
|
@@ -61,6 +71,8 @@ TargetingHostsPage.propTypes = {
|
|
61
71
|
|
62
72
|
TargetingHostsPage.defaultProps = {
|
63
73
|
apiStatus: null,
|
74
|
+
statusFilter: {},
|
75
|
+
statusFilterReset: noop,
|
64
76
|
};
|
65
77
|
|
66
78
|
export default TargetingHostsPage;
|
@@ -18,3 +18,8 @@ export const selectTotalHosts = state =>
|
|
18
18
|
|
19
19
|
export const selectIntervalExists = state =>
|
20
20
|
selectDoesIntervalExist(state, TARGETING_HOSTS);
|
21
|
+
|
22
|
+
const defaultStatusFilter = {};
|
23
|
+
export const selectStatusFilter = state =>
|
24
|
+
state.foremanRemoteExecutionReducers.jobInvocations
|
25
|
+
.jobInvocationStateFilter || defaultStatusFilter;
|
@@ -14,10 +14,23 @@ import {
|
|
14
14
|
selectAutoRefresh,
|
15
15
|
selectTotalHosts,
|
16
16
|
selectIntervalExists,
|
17
|
+
selectStatusFilter,
|
17
18
|
} from './TargetingHostsSelectors';
|
18
19
|
import { getApiUrl } from './TargetingHostsHelpers';
|
19
20
|
import { TARGETING_HOSTS } from './TargetingHostsConsts';
|
20
21
|
import TargetingHostsPage from './TargetingHostsPage';
|
22
|
+
import { chartFilter } from '../../redux/actions/jobInvocations';
|
23
|
+
|
24
|
+
const buildSearchQuery = (query, stateFilter) => {
|
25
|
+
const filters = Object.entries(stateFilter).map(
|
26
|
+
([key, value]) => `${key} = ${value}`
|
27
|
+
);
|
28
|
+
return [query, filters]
|
29
|
+
.flat()
|
30
|
+
.filter(x => x)
|
31
|
+
.map(x => `(${x})`)
|
32
|
+
.join(' AND ');
|
33
|
+
};
|
21
34
|
|
22
35
|
const WrappedTargetingHosts = () => {
|
23
36
|
const dispatch = useDispatch();
|
@@ -27,6 +40,7 @@ const WrappedTargetingHosts = () => {
|
|
27
40
|
const items = useSelector(selectItems);
|
28
41
|
const apiStatus = useSelector(selectApiStatus);
|
29
42
|
const totalHosts = useSelector(selectTotalHosts);
|
43
|
+
const statusFilter = useSelector(selectStatusFilter);
|
30
44
|
const [searchQuery, setSearchQuery] = useState('');
|
31
45
|
const [pagination, setPagination] = useState({
|
32
46
|
page: 1,
|
@@ -36,11 +50,11 @@ const WrappedTargetingHosts = () => {
|
|
36
50
|
const [apiUrl, setApiUrl] = useState(getApiUrl(searchQuery, pagination));
|
37
51
|
const intervalExists = useSelector(selectIntervalExists);
|
38
52
|
|
39
|
-
const handleSearch = query => {
|
53
|
+
const handleSearch = (query, status) => {
|
40
54
|
const defaultPagination = { page: 1, per_page: pagination.per_page };
|
41
55
|
stopApiInterval();
|
42
56
|
|
43
|
-
setApiUrl(getApiUrl(query, defaultPagination));
|
57
|
+
setApiUrl(getApiUrl(buildSearchQuery(query, status), defaultPagination));
|
44
58
|
setSearchQuery(query);
|
45
59
|
setPagination(defaultPagination);
|
46
60
|
};
|
@@ -48,7 +62,7 @@ const WrappedTargetingHosts = () => {
|
|
48
62
|
const handlePagination = args => {
|
49
63
|
stopApiInterval();
|
50
64
|
setPagination(args);
|
51
|
-
setApiUrl(getApiUrl(searchQuery, args));
|
65
|
+
setApiUrl(getApiUrl(buildSearchQuery(searchQuery, statusFilter), args));
|
52
66
|
};
|
53
67
|
|
54
68
|
const stopApiInterval = () => {
|
@@ -81,10 +95,16 @@ const WrappedTargetingHosts = () => {
|
|
81
95
|
};
|
82
96
|
}, [dispatch, apiUrl, autoRefresh]);
|
83
97
|
|
98
|
+
useEffect(() => {
|
99
|
+
handleSearch(searchQuery, statusFilter);
|
100
|
+
}, [statusFilter, searchQuery]);
|
101
|
+
|
84
102
|
return (
|
85
103
|
<TargetingHostsPage
|
86
104
|
handleSearch={handleSearch}
|
87
105
|
searchQuery={searchQuery}
|
106
|
+
statusFilter={statusFilter}
|
107
|
+
statusFilterReset={_x => chartFilter(null)(dispatch, null)}
|
88
108
|
apiStatus={apiStatus}
|
89
109
|
items={items}
|
90
110
|
totalHosts={totalHosts}
|
@@ -1,33 +1,48 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
3
|
|
4
|
-
const AggregateStatus = ({ statuses }) => (
|
4
|
+
const AggregateStatus = ({ statuses, chartFilter }) => (
|
5
5
|
<div id="aggregate_statuses">
|
6
6
|
<p className="card-pf-aggregate-status-notifications">
|
7
|
-
<
|
7
|
+
<a
|
8
|
+
className="card-pf-aggregate-status-notification"
|
9
|
+
onClick={() => chartFilter('success')}
|
10
|
+
>
|
8
11
|
<span id="success_count">
|
9
12
|
<span className="pficon pficon-ok" />
|
10
13
|
{statuses.success}
|
11
14
|
</span>
|
12
|
-
</
|
13
|
-
|
15
|
+
</a>
|
16
|
+
|
17
|
+
<a
|
18
|
+
className="card-pf-aggregate-status-notification"
|
19
|
+
onClick={() => chartFilter('failed')}
|
20
|
+
>
|
14
21
|
<span id="failed_count">
|
15
22
|
<span className="pficon pficon-error-circle-o" />
|
16
23
|
{statuses.failed}
|
17
24
|
</span>
|
18
|
-
</
|
19
|
-
|
25
|
+
</a>
|
26
|
+
|
27
|
+
<a
|
28
|
+
className="card-pf-aggregate-status-notification"
|
29
|
+
onClick={() => chartFilter('pending')}
|
30
|
+
>
|
20
31
|
<span id="pending_count">
|
21
32
|
<span className="pficon pficon-running" />
|
22
33
|
{statuses.pending}
|
23
34
|
</span>
|
24
|
-
</
|
25
|
-
|
35
|
+
</a>
|
36
|
+
|
37
|
+
<a
|
38
|
+
className="card-pf-aggregate-status-notification"
|
39
|
+
onClick={() => chartFilter('cancelled')}
|
40
|
+
>
|
26
41
|
<span id="cancelled_count">
|
27
42
|
<span className="pficon pficon-close" />
|
28
43
|
{statuses.cancelled}
|
29
44
|
</span>
|
30
|
-
</
|
45
|
+
</a>
|
31
46
|
</p>
|
32
47
|
</div>
|
33
48
|
);
|
@@ -39,6 +54,7 @@ AggregateStatus.propTypes = {
|
|
39
54
|
pending: PropTypes.number,
|
40
55
|
success: PropTypes.number,
|
41
56
|
}).isRequired,
|
57
|
+
chartFilter: PropTypes.func.isRequired,
|
42
58
|
};
|
43
59
|
|
44
60
|
export default AggregateStatus;
|
@@ -7,7 +7,9 @@ jest.unmock('./index.js');
|
|
7
7
|
describe('AggregateStatus', () => {
|
8
8
|
describe('has no data', () => {
|
9
9
|
it('renders cards with no data', () => {
|
10
|
-
const chartNumbers = shallow(
|
10
|
+
const chartNumbers = shallow(
|
11
|
+
<AggregateStatus statuses={{}} chartFilter={_x => {}} />
|
12
|
+
);
|
11
13
|
const success = chartNumbers.find('#success_count').text();
|
12
14
|
const failed = chartNumbers.find('#failed_count').text();
|
13
15
|
const pending = chartNumbers.find('#pending_count').text();
|
@@ -25,7 +27,9 @@ describe('AggregateStatus', () => {
|
|
25
27
|
cancelled: 31,
|
26
28
|
pending: 3,
|
27
29
|
};
|
28
|
-
const chartNumbers = shallow(
|
30
|
+
const chartNumbers = shallow(
|
31
|
+
<AggregateStatus statuses={statuses} chartFilter={_x => {}} />
|
32
|
+
);
|
29
33
|
const success = chartNumbers.find('#success_count').text();
|
30
34
|
const failed = chartNumbers.find('#failed_count').text();
|
31
35
|
const pending = chartNumbers.find('#pending_count').text();
|
@@ -10,6 +10,13 @@ import * as JobInvocationActions from '../../redux/actions/jobInvocations';
|
|
10
10
|
const colIndexOfMaxValue = columns =>
|
11
11
|
columns.reduce((iMax, x, i, arr) => (x[1] > arr[iMax][1] ? i : iMax), 0);
|
12
12
|
|
13
|
+
const colorMap = {
|
14
|
+
'#5CB85C': 'success',
|
15
|
+
'#D9534F': 'failed',
|
16
|
+
'#DEDEDE': 'pending',
|
17
|
+
'#B7312D': 'cancelled',
|
18
|
+
};
|
19
|
+
|
13
20
|
class JobInvocationContainer extends React.Component {
|
14
21
|
componentDidMount() {
|
15
22
|
const {
|
@@ -21,9 +28,14 @@ class JobInvocationContainer extends React.Component {
|
|
21
28
|
}
|
22
29
|
|
23
30
|
render() {
|
24
|
-
const { jobInvocations, statuses } = this.props;
|
31
|
+
const { jobInvocations, statuses, chartFilter } = this.props;
|
25
32
|
const iMax = colIndexOfMaxValue(jobInvocations);
|
26
33
|
|
34
|
+
const map = jobInvocations.reduce(
|
35
|
+
(acc, [label, _count, color]) => ({ [label]: colorMap[color], ...acc }),
|
36
|
+
{}
|
37
|
+
);
|
38
|
+
|
27
39
|
return (
|
28
40
|
<div id="job_invocations_chart_container">
|
29
41
|
<DonutChart
|
@@ -32,8 +44,11 @@ class JobInvocationContainer extends React.Component {
|
|
32
44
|
type: 'percent',
|
33
45
|
secondary: (jobInvocations[iMax] || [])[0],
|
34
46
|
}}
|
47
|
+
onclick={(d, element) => {
|
48
|
+
chartFilter(map[d.name]);
|
49
|
+
}}
|
35
50
|
/>
|
36
|
-
<AggregateStatus statuses={statuses} />
|
51
|
+
<AggregateStatus statuses={statuses} chartFilter={chartFilter} />
|
37
52
|
</div>
|
38
53
|
);
|
39
54
|
}
|
@@ -63,10 +78,12 @@ JobInvocationContainer.propTypes = {
|
|
63
78
|
pending: PropTypes.number,
|
64
79
|
success: PropTypes.number,
|
65
80
|
}),
|
81
|
+
chartFilter: PropTypes.func,
|
66
82
|
};
|
67
83
|
|
68
84
|
JobInvocationContainer.defaultProps = {
|
69
85
|
startJobInvocationsPolling: JobInvocationActions.startJobInvocationsPolling,
|
86
|
+
chartFilter: JobInvocationActions.chartFilter,
|
70
87
|
data: {},
|
71
88
|
jobInvocations: [['property', 3, 'color']],
|
72
89
|
statuses: {},
|
@@ -76,3 +76,11 @@ export const startJobInvocationsPolling = url => (dispatch, getState) => {
|
|
76
76
|
});
|
77
77
|
dispatch(getJobInvocations(url));
|
78
78
|
};
|
79
|
+
|
80
|
+
export const chartFilter = state => (dispatch, getState) => {
|
81
|
+
const filter = state ? { 'job_invocation.result': state.toLowerCase() } : {};
|
82
|
+
dispatch({
|
83
|
+
type: 'JOB_INVOCATION_CHART_FILTER',
|
84
|
+
payload: { filter },
|
85
|
+
});
|
86
|
+
};
|
@@ -26,6 +26,8 @@ export default (state = initialState, action) => {
|
|
26
26
|
return state
|
27
27
|
.set('jobInvocations', payload.jobInvocations.job_invocations)
|
28
28
|
.set('statuses', payload.jobInvocations.statuses);
|
29
|
+
case 'JOB_INVOCATION_CHART_FILTER':
|
30
|
+
return state.set('jobInvocationStateFilter', payload.filter);
|
29
31
|
default:
|
30
32
|
return state;
|
31
33
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_remote_execution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 9.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Foreman Remote Execution team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-12-
|
11
|
+
date: 2022-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deface
|
@@ -508,6 +508,8 @@ files:
|
|
508
508
|
- webpack/react_app/components/TargetingHosts/TargetingHosts.js
|
509
509
|
- webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js
|
510
510
|
- webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js
|
511
|
+
- webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.js
|
512
|
+
- webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss
|
511
513
|
- webpack/react_app/components/TargetingHosts/TargetingHostsPage.js
|
512
514
|
- webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss
|
513
515
|
- webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js
|
@@ -556,7 +558,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
556
558
|
- !ruby/object:Gem::Version
|
557
559
|
version: '0'
|
558
560
|
requirements: []
|
559
|
-
rubygems_version: 3.
|
561
|
+
rubygems_version: 3.1.6
|
560
562
|
signing_key:
|
561
563
|
specification_version: 4
|
562
564
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|