foreman_remote_execution 8.1.2 → 8.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|