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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +3 -1
  3. data/app/lib/actions/remote_execution/proxy_action.rb +3 -2
  4. data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +4 -1
  5. data/app/models/job_invocation.rb +3 -2
  6. data/app/models/template_invocation.rb +1 -0
  7. data/lib/foreman_remote_execution/engine.rb +1 -1
  8. data/lib/foreman_remote_execution/version.rb +1 -1
  9. data/test/unit/job_invocation_test.rb +1 -0
  10. data/webpack/JobWizard/JobWizard.scss +1 -5
  11. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
  12. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +4 -28
  13. data/webpack/JobWizard/steps/HostsAndInputs/index.js +0 -4
  14. data/webpack/JobWizard/steps/form/Formatter.js +8 -30
  15. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.js +45 -0
  16. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +26 -0
  17. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +13 -1
  18. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +5 -0
  19. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +3 -0
  20. data/webpack/react_app/components/TargetingHosts/index.js +23 -3
  21. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +25 -9
  22. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +6 -2
  23. data/webpack/react_app/components/jobInvocations/index.js +19 -2
  24. data/webpack/react_app/redux/actions/jobInvocations/index.js +8 -0
  25. data/webpack/react_app/redux/reducers/jobInvocations/index.js +2 -0
  26. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13f80e6dffc1166fb7f5e423ea6543b6d89a5f8f45930c29073bf28e232bde0b
4
- data.tar.gz: 0f489c08c07a1eb217feec2d1b9325f3e5c9ce248becbaf68562471aac889cd0
3
+ metadata.gz: a4f0a9c455ec484caac7631cec3d1ed8eff22263c96036d7837fdbb86f3faab8
4
+ data.tar.gz: a73788ec230b20cfd6050733d40f27a59d7bf26d334ce51f0b422aae1a64a68e
5
5
  SHA512:
6
- metadata.gz: 88032e02e3375cb7d19992efaad18932e7eef3948e8822dc8ad573a790ce4077273012f861b49a8351cda44f4b272b84ef0bc8e8a7124a2d2a71c759026a1d87
7
- data.tar.gz: 8d0aec0264bdae9ba1530940088daeef369546a2ad9102d7d4eb1b7527f2cd1402d37915d4e9f659d845706938cd0dc7fc8299d1559c21b17cd404cdf91a22df
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: events.last[:sequence_id] + 1,
38
+ sequence_id: last[:sequence_id] + 1,
38
39
  template_invocation_id: template_invocation.id,
39
40
  event: data['exit_status'],
40
- timestamp: events.last[: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 = 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
@@ -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.4'
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,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '8.1.1'.freeze
2
+ VERSION = '9.0.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 }
@@ -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
  }
@@ -49,7 +49,7 @@ Array [
49
49
  "port": null,
50
50
  "preventInvalidHostname": false,
51
51
  "protocol": null,
52
- "query": "resource=ForemanTasks%3A%3ATask",
52
+ "query": "resource=ForemanTasks%3A%3ATask&name=some+search",
53
53
  "urn": null,
54
54
  "username": null,
55
55
  },
@@ -1,33 +1,10 @@
1
- import React, { useEffect } from '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
- useKeyShortcuts: true,
17
+ searchQuery: value,
41
18
  },
42
19
  }}
43
- onSearch={noop}
44
- initialQuery={value}
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, { useEffect } from '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 defaultValue={defaultValue} setValue={setSearch} />
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
- useKeyShortcuts: true,
43
+ searchQuery: values[name],
66
44
  },
67
45
  }}
68
- onSearch={noop}
69
- onBookmarkClick={search => setSearch(search)}
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;
@@ -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.1
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-01 00:00:00.000000000 Z
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.3.20
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