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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e893a2f960dd325ecee23b756b471f36814aae3f9f902421b18635bb69b6323a
4
- data.tar.gz: d916b70de7e3419e6d546942b112adfb8733ab7b62fe168651404568e5489d35
3
+ metadata.gz: 438d5bf70f43c95193398aa589bdb0970fca5fdb5fd4a5a7afc60a28edff9abc
4
+ data.tar.gz: 63726b6cb27feea113d85eee1bfe3b6600ea91856ca8ad38ecd3c9be24686cba
5
5
  SHA512:
6
- metadata.gz: 27b59e2db3824b75f8038f214e76dc5abbf941ae91ed188c4d6dae97351d189bba6502b2e71e7f4b41b6f21b46a8772e56ee5fc0ee5095d5a281333b7c86cec8
7
- data.tar.gz: 23231766b5d3f07590acc887d36da711b3859a3bbd581b5192a4bce3a3db45f263984a57993949768dab62cefcccc7d0dde75489f722ed3600732155407182cf
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 = task.sub_tasks_counts
257
- done = counts.values_at(*map.results).reduce(:+)
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
@@ -34,6 +34,7 @@ class TemplateInvocation < ApplicationRecord
34
34
  :cancelled => :cancelled,
35
35
  :error => :failed,
36
36
  :pending => :pending,
37
+ :running => :pending,
37
38
  :success => :success,
38
39
  :warning => :failed,
39
40
  }.with_indifferent_access
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '8.1.2'.freeze
2
+ VERSION = '8.2.0'.freeze
3
3
  end
@@ -130,6 +130,7 @@ class JobInvocationTest < ActiveSupport::TestCase
130
130
  :failed => 0,
131
131
  :pending => 0,
132
132
  :progress => 0,
133
+ :running => 0,
133
134
  }
134
135
  end
135
136
  before { job_invocation.task = task }
@@ -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;
@@ -38,6 +38,9 @@ exports[`TargetingHostsPage renders 1`] = `
38
38
  />
39
39
  </Col>
40
40
  </Row>
41
+ <TargetingHostsLabelsRow
42
+ query={Object {}}
43
+ />
41
44
  <br />
42
45
  <TargetingHosts
43
46
  apiStatus="RESOLVED"
@@ -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
- <span className="card-pf-aggregate-status-notification">
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
- </span>
13
- <span className="card-pf-aggregate-status-notification">
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
- </span>
19
- <span className="card-pf-aggregate-status-notification">
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
- </span>
25
- <span className="card-pf-aggregate-status-notification">
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
- </span>
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(<AggregateStatus statuses={{}} />);
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(<AggregateStatus statuses={statuses} />);
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.1.2
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: 2022-12-13 00:00:00.000000000 Z
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