foreman_remote_execution 8.1.2 → 8.2.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/models/job_invocation.rb +3 -2
- data/app/models/template_invocation.rb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/job_invocation_test.rb +1 -0
- 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 +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 438d5bf70f43c95193398aa589bdb0970fca5fdb5fd4a5a7afc60a28edff9abc
|
4
|
+
data.tar.gz: 63726b6cb27feea113d85eee1bfe3b6600ea91856ca8ad38ecd3c9be24686cba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 996b52c405b4cab9c1c67bc7350a068bc95f73786d6bb86057b5c91537af6501218ce0bf0d1ce52f888bab5d9f9203d756c78479cac4b37bff2c479c1383edbc
|
7
|
+
data.tar.gz: 57e732c23d9cae6a7d576b394f2176359ad08e09574bff8a70db9171f56fc8fd106db61e24b9a3dc6c3860cd372b7fb6f8a79a8868a05d03ccda31ab0daee2f1
|
@@ -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
|
@@ -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: 8.
|
4
|
+
version: 8.2.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:
|
11
|
+
date: 2023-01-18 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
|