foreman_remote_execution 8.1.1 → 9.0.0

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