foreman_remote_execution 1.5.0 → 1.5.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.babelrc +9 -0
  3. data/.eslintignore +3 -0
  4. data/.eslintrc +49 -0
  5. data/.gitignore +1 -0
  6. data/.hound.yml +5 -0
  7. data/.rubocop.yml +4 -1
  8. data/.rubocop_todo.yml +63 -35
  9. data/.travis.yml +6 -0
  10. data/app/assets/javascripts/foreman_remote_execution/template_invocation.js +1 -1
  11. data/app/assets/stylesheets/foreman_remote_execution/job_invocations.css.scss +0 -14
  12. data/app/controllers/job_invocations_controller.rb +18 -0
  13. data/app/helpers/job_invocations_chart_helper.rb +77 -0
  14. data/app/helpers/job_invocations_helper.rb +79 -0
  15. data/app/helpers/remote_execution_helper.rb +10 -50
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/remote_execution_provider.rb +4 -0
  18. data/app/views/job_invocations/_card_results.html.erb +11 -0
  19. data/app/views/job_invocations/_card_schedule.html.erb +32 -0
  20. data/app/views/job_invocations/_card_target_hosts.html.erb +33 -0
  21. data/app/views/job_invocations/_card_user_input.html.erb +16 -0
  22. data/app/views/job_invocations/_tab_hosts.html.erb +1 -1
  23. data/app/views/job_invocations/_tab_overview.html.erb +25 -55
  24. data/app/views/job_invocations/_tab_preview_templates.html.erb +20 -0
  25. data/app/views/job_invocations/_user_input.html.erb +21 -0
  26. data/app/views/job_invocations/show.html.erb +14 -8
  27. data/app/views/job_invocations/show.js.erb +3 -6
  28. data/config/routes.rb +1 -0
  29. data/lib/foreman_remote_execution/engine.rb +2 -2
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/package.json +62 -0
  32. data/test/unit/job_invocation_test.rb +15 -0
  33. data/webpack/index.js +29 -0
  34. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +34 -0
  35. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +36 -0
  36. data/webpack/react_app/components/jobInvocations/index.js +58 -0
  37. data/webpack/react_app/redux/actions/jobInvocations/index.js +74 -0
  38. data/webpack/react_app/redux/consts.js +6 -0
  39. data/webpack/react_app/redux/reducers/index.js +6 -0
  40. data/webpack/react_app/redux/reducers/jobInvocations/index.fixtures.js +78 -0
  41. data/webpack/react_app/redux/reducers/jobInvocations/index.js +32 -0
  42. data/webpack/react_app/redux/reducers/jobInvocations/index.test.js +37 -0
  43. data/webpack/test_setup.js +11 -0
  44. metadata +26 -2
@@ -1,24 +1,30 @@
1
1
  <% title @job_invocation.description, trunc_with_tooltip(@job_invocation.description, 120) %>
2
2
  <% stylesheet 'foreman_remote_execution/job_invocations' %>
3
3
  <% javascript 'charts', 'foreman_remote_execution/template_invocation' %>
4
+ <%= javascript_include_tag *webpack_asset_paths('remoteexecution', :extension => 'js'), "data-turbolinks-track" => true, 'defer' => 'defer' %>
4
5
 
5
6
  <% if @job_invocation.task %>
6
7
  <% title_actions(button_group(job_invocation_task_buttons(@job_invocation.task))) %>
7
8
  <% end %>
8
9
 
9
10
  <ul class="nav nav-tabs" data-tabs="tabs">
10
- <li class="<%= job_invocation_active_tab(:overview, params) %>"><a href="#primary" data-toggle="tab"><%= _('Overview') %></a></li>
11
- <li class="<%= job_invocation_active_tab(:hosts, params) %>"><a href="#hosts" data-toggle="tab"><%= _('Hosts') %></a></li>
12
- <% unless @job_invocation.recurring_logic.nil? %><li><a href="#recurring_logic" data-toggle="tab"><%= _('Recurring logic') %></a></li><% end %>
11
+ <li class="active">
12
+ <a href="#primary" data-toggle="tab"><%= _('Overview') %></a>
13
+ </li>
14
+ <li>
15
+ <a href="#preview_templates" data-toggle="tab"><%= _('Preview templates') %></a>
16
+ </li>
17
+ <% if @job_invocation.recurring_logic.present? %>
18
+ <li><a href="#recurring_logic" data-toggle="tab"><%= _('Recurring logic') %></a></li>
19
+ <% end %>
13
20
  </ul>
14
21
 
15
22
  <div class="tab-content">
16
- <div class="tab-pane <%= job_invocation_active_tab(:overview, params) %>" id="primary">
17
- <%= render 'tab_overview', :job_invocation => @job_invocation %>
23
+ <div class="tab-pane active" id="primary">
24
+ <%= render 'tab_overview', :job_invocation => @job_invocation, :hosts => @hosts %>
18
25
  </div>
19
-
20
- <div class="tab-pane <%= job_invocation_active_tab(:hosts, params) %>" id="hosts" data-refresh_required="<%= @job_invocation.resolved? ? '' : 'true' %>">
21
- <%= render 'tab_hosts', :job_invocation => @job_invocation, :hosts => @hosts %>
26
+ <div class="tab-pane" id="preview_templates">
27
+ <%= render 'tab_preview_templates', :job_invocation => @job_invocation %>
22
28
  </div>
23
29
 
24
30
  <% unless @job_invocation.recurring_logic.nil? %>
@@ -1,11 +1,8 @@
1
1
  $('div#title_action div.btn-group').html('<%= button_group(job_invocation_task_buttons(@job_invocation.task)).html_safe %>');
2
- $('div#status_chart').html('<%=j job_invocation_chart(@job_invocation) %>');
3
- $('div#status').flot_pie();
4
-
5
2
  <% if params[:hosts_needs_refresh] == 'true' && @job_invocation.resolved? %>
6
- var hosts_tab = $('div#hosts');
7
- hosts_tab.html('<%=j render('tab_hosts', :job_invocation => @job_invocation, :hosts => @hosts) %>');
8
- hosts_tab.data('refresh_required', '');
3
+ var hosts_table = $('div#hosts');
4
+ hosts_table.html('<%=j render('tab_hosts', :job_invocation => @job_invocation, :hosts => @hosts) %>');
5
+ hosts_table.data('refresh_required', '');
9
6
  <% end %>
10
7
 
11
8
  <% ['name', 'status', 'actions', 'provider'].each do |attribute| %>
data/config/routes.rb CHANGED
@@ -19,6 +19,7 @@ Rails.application.routes.draw do
19
19
  resources :job_invocations, :only => [:new, :create, :show, :index] do
20
20
  collection do
21
21
  post 'refresh'
22
+ get 'chart'
22
23
  get 'preview_hosts'
23
24
  get 'auto_complete_search'
24
25
  end
@@ -31,7 +31,7 @@ module ForemanRemoteExecution
31
31
 
32
32
  initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
33
33
  Foreman::Plugin.register :foreman_remote_execution do
34
- requires_foreman '>= 1.17'
34
+ requires_foreman '>= 1.18'
35
35
 
36
36
  apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
37
37
 
@@ -54,7 +54,7 @@ module ForemanRemoteExecution
54
54
  permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
55
55
  permission :create_job_invocations, { :job_invocations => [:new, :create, :refresh, :rerun, :preview_hosts],
56
56
  'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
57
- permission :view_job_invocations, { :job_invocations => [:index, :show, :auto_complete_search], :template_invocations => [:show],
57
+ permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search], :template_invocations => [:show],
58
58
  'api/v2/job_invocations' => [:index, :show, :output] }, :resource_type => 'JobInvocation'
59
59
  permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
60
60
  permission :cancel_job_invocations, { :job_invocations => [:cancel], 'api/v2/job_invocations' => [:cancel] }, :resource_type => 'JobInvocation'
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '1.5.0'.freeze
2
+ VERSION = '1.5.1'.freeze
3
3
  end
data/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "foreman_remote_execution",
3
+ "version": "1.0.0",
4
+ "license": "GPL-3.0",
5
+ "scripts": {
6
+ "lint": "./node_modules/.bin/eslint -c .eslintrc webpack/ script/ || exit 0",
7
+ "test": "node node_modules/.bin/jest webpack",
8
+ "test:watch": "node node_modules/.bin/jest webpack --watchAll",
9
+ "test:current": "node node_modules/.bin/jest webpack --watch"
10
+ },
11
+ "jest": {
12
+ "verbose": true,
13
+ "moduleDirectories": [
14
+ "node_modules",
15
+ "webpack"
16
+ ],
17
+ "setupFiles": [
18
+ "raf/polyfill",
19
+ "./webpack/test_setup.js"
20
+ ],
21
+ "testPathIgnorePatterns": [
22
+ "/node_modules/",
23
+ "<rootDir>/foreman/"
24
+ ]
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/foreman_remote_execution/foreman_remote_execution.git"
29
+ },
30
+ "bugs": {
31
+ "url": "http://projects.theforeman.org/projects/foreman_remote_execution/issues"
32
+ },
33
+ "devDependencies": {
34
+ "babel-eslint": "^8.2.1",
35
+ "babel-preset-env": "^1.6.0",
36
+ "babel-preset-react": "^6.24.1",
37
+ "babel-plugin-lodash": "^3.3.2",
38
+ "babel-plugin-transform-object-assign": "^6.22.0",
39
+ "babel-plugin-transform-class-properties": "^6.24.1",
40
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
41
+ "enzyme": "^3.2.0",
42
+ "enzyme-adapter-react-16": "^1.1.0",
43
+ "enzyme-to-json": "^3.1.2",
44
+ "eslint": "^4.10.0",
45
+ "eslint-config-airbnb": "^16.0.0",
46
+ "eslint-plugin-import": "^2.8.0",
47
+ "eslint-plugin-jest": "^21.2.0",
48
+ "eslint-plugin-jsx-a11y": "^6.0.2",
49
+ "eslint-plugin-react": "^7.4.0",
50
+ "jest": "^21.2.1"
51
+ },
52
+ "dependencies": {
53
+ "babel-polyfill": "^6.26.0",
54
+ "prop-types": "^15.6.0",
55
+ "react": "^16.2.0",
56
+ "react-dom": "^16.2.0",
57
+ "react-redux": "^5.0.6",
58
+ "redux": "^3.7.2",
59
+ "seamless-immutable": "^7.1.3",
60
+ "urijs": "^1.19.0"
61
+ }
62
+ }
@@ -173,4 +173,19 @@ class JobInvocationTest < ActiveSupport::TestCase
173
173
  end
174
174
  end
175
175
 
176
+ describe '#finished?' do
177
+ let(:task) { ForemanTasks::Task.new }
178
+ before { job_invocation.task = task }
179
+
180
+ it 'returns false if task state is pending' do
181
+ job_invocation.task.expects(:pending?).returns(true)
182
+ refute job_invocation.finished?
183
+ end
184
+
185
+ it 'returns true if task is not pending' do
186
+ job_invocation.task.expects(:pending?).returns(false)
187
+ assert job_invocation.finished?
188
+ end
189
+ end
190
+
176
191
  end
data/webpack/index.js ADDED
@@ -0,0 +1,29 @@
1
+ import URI from 'urijs';
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import { mount, registerReducer } from 'foremanReact/common/MountingService';
4
+ // eslint-disable-next-line import/no-extraneous-dependencies
5
+ import componentRegistry from 'foremanReact/components/componentRegistry';
6
+ import JobInvocationContainer from './react_app/components/jobInvocations';
7
+ import rootReducer from './react_app/redux/reducers';
8
+
9
+ componentRegistry.register({
10
+ name: 'JobInvocationContainer',
11
+ type: JobInvocationContainer,
12
+ });
13
+
14
+ registerReducer('foremanRemoteExecutionReducers', rootReducer);
15
+
16
+ if (window.location.href.match(/job_invocations/)) {
17
+ const jobInvocationId = parseInt(
18
+ new URI(window.location.href).filename(),
19
+ 10,
20
+ );
21
+
22
+ const mountJobInvocationContainer = () => {
23
+ mount('JobInvocationContainer', '#status_chart', {
24
+ url: `/job_invocations/chart?id=${jobInvocationId}`,
25
+ });
26
+ };
27
+
28
+ document.addEventListener('page:change', mountJobInvocationContainer);
29
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+
3
+ const AggregateStatus = ({ statuses }) => (
4
+ <div id="aggregate_statuses">
5
+ <p className="card-pf-aggregate-status-notifications">
6
+ <span className="card-pf-aggregate-status-notification">
7
+ <span id="success_count">
8
+ <span className="pficon pficon-ok" />
9
+ {statuses.success}
10
+ </span>
11
+ </span>
12
+ <span className="card-pf-aggregate-status-notification">
13
+ <span id="failed_count">
14
+ <span className="pficon pficon-error-circle-o" />
15
+ {statuses.failed}
16
+ </span>
17
+ </span>
18
+ <span className="card-pf-aggregate-status-notification">
19
+ <span id="pending_count">
20
+ <span className="pficon pficon-running" />
21
+ {statuses.pending}
22
+ </span>
23
+ </span>
24
+ <span className="card-pf-aggregate-status-notification">
25
+ <span id="cancelled_count">
26
+ <span className="pficon pficon-close" />
27
+ {statuses.cancelled}
28
+ </span>
29
+ </span>
30
+ </p>
31
+ </div>
32
+ );
33
+
34
+ export default AggregateStatus;
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { shallow } from 'enzyme';
3
+ import AggregateStatus from './index.js';
4
+
5
+ jest.unmock('./index.js');
6
+
7
+ describe('AggregateStatus', () => {
8
+ describe('has no data', () => {
9
+ it('renders cards with no data', () => {
10
+ const chartNumbers = shallow(<AggregateStatus statuses={{}} />);
11
+ const success = chartNumbers.find('#success_count').text();
12
+ const failed = chartNumbers.find('#failed_count').text();
13
+ const pending = chartNumbers.find('#pending_count').text();
14
+ const cancelled = chartNumbers.find('#cancelled_count').text();
15
+ expect(success).toBe('');
16
+ expect(failed).toBe('');
17
+ expect(cancelled).toBe('');
18
+ expect(pending).toBe('');
19
+ });
20
+
21
+ it('renders cards with props passed', () => {
22
+ const statuses = {
23
+ success: 19, failed: 20, cancelled: 31, pending: 3,
24
+ };
25
+ const chartNumbers = shallow(<AggregateStatus statuses={statuses} />);
26
+ const success = chartNumbers.find('#success_count').text();
27
+ const failed = chartNumbers.find('#failed_count').text();
28
+ const pending = chartNumbers.find('#pending_count').text();
29
+ const cancelled = chartNumbers.find('#cancelled_count').text();
30
+ expect(success).toBe(statuses.success.toString());
31
+ expect(failed).toBe(statuses.failed.toString());
32
+ expect(cancelled).toBe(statuses.cancelled.toString());
33
+ expect(pending).toBe(statuses.pending.toString());
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,58 @@
1
+ import { connect } from 'react-redux';
2
+ import React from 'react';
3
+ import Immutable from 'seamless-immutable';
4
+ import PropTypes from 'prop-types';
5
+ // eslint-disable-next-line import/no-extraneous-dependencies
6
+ import DonutChart from 'foremanReact/components/common/charts/DonutChart';
7
+ import AggregateStatus from './AggregateStatus/index.js';
8
+ import * as JobInvocationActions from '../../redux/actions/jobInvocations';
9
+
10
+ class JobInvocationContainer extends React.Component {
11
+ componentDidMount() {
12
+ const { startJobInvocationsPolling, data: { url } } = this.props;
13
+
14
+ startJobInvocationsPolling(url);
15
+ }
16
+
17
+ render() {
18
+ const { jobInvocations, statuses } = this.props;
19
+
20
+ return (
21
+ <div id="job_invocations_chart_container">
22
+ <DonutChart data={Immutable.asMutable(jobInvocations)} />
23
+ <AggregateStatus statuses={statuses} />
24
+ </div>
25
+ );
26
+ }
27
+ }
28
+
29
+ const mapStateToProps = (state) => {
30
+ const {
31
+ jobInvocations,
32
+ statuses,
33
+ } = state.foremanRemoteExecutionReducers.jobInvocations;
34
+
35
+ return {
36
+ jobInvocations,
37
+ statuses,
38
+ };
39
+ };
40
+
41
+ JobInvocationContainer.propTypes = {
42
+ startJobInvocationsPolling: PropTypes.func,
43
+ data: PropTypes.string,
44
+ jobInvocations: PropTypes.arrayOf(PropTypes.arrayOf(
45
+ PropTypes.string,
46
+ PropTypes.number,
47
+ PropTypes.string,
48
+ )),
49
+ statuses: PropTypes.shape({}),
50
+ };
51
+
52
+ JobInvocationContainer.defaultProps = {
53
+ startJobInvocationsPolling: JobInvocationActions.startJobInvocationsPolling,
54
+ data: '',
55
+ jobInvocations: [['property', 3, 'color']],
56
+ statuses: {},
57
+ };
58
+ export default connect(mapStateToProps, JobInvocationActions)(JobInvocationContainer);
@@ -0,0 +1,74 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import API from 'foremanReact/API';
3
+
4
+ import {
5
+ JOB_INVOCATIONS_GET_JOB_INVOCATIONS,
6
+ JOB_INVOCATIONS_POLLING_STARTED,
7
+ JOB_INVOCATIONS_JOB_FINISHED,
8
+ } from '../../consts';
9
+
10
+ const defaultJobInvocationsPollingInterval = 1000;
11
+ const jobInvocationsInterval = process.env.JOB_INVOCATIONS_POLLING ||
12
+ defaultJobInvocationsPollingInterval;
13
+
14
+ const getJobInvocations = url => (dispatch, getState) => {
15
+ function onGetJobInvocationsSuccess({ data }) {
16
+ // If the job has finished, stop polling
17
+ if (data.finished) {
18
+ dispatch({
19
+ type: JOB_INVOCATIONS_JOB_FINISHED,
20
+ payload: {
21
+ jobInvocations: data,
22
+ },
23
+ });
24
+ } else {
25
+ dispatch({
26
+ type: JOB_INVOCATIONS_GET_JOB_INVOCATIONS,
27
+ payload: {
28
+ jobInvocations: data,
29
+ },
30
+ });
31
+ }
32
+ }
33
+
34
+ function onGetJobInvocationsFailed(error) {
35
+ if (error.response.status === 401) {
36
+ window.location.replace('/users/login');
37
+ }
38
+ }
39
+
40
+ function triggerPolling() {
41
+ if (jobInvocationsInterval) {
42
+ setTimeout(
43
+ () => dispatch(getJobInvocations(url)),
44
+ jobInvocationsInterval,
45
+ );
46
+ }
47
+ }
48
+
49
+ const isDocumentVisible =
50
+ document.visibilityState === 'visible' ||
51
+ document.visibilityState === 'prerender';
52
+
53
+ if (getState().foremanRemoteExecutionReducers.jobInvocations.isPolling) {
54
+ if (isDocumentVisible) {
55
+ API.get(url)
56
+ .then(onGetJobInvocationsSuccess)
57
+ .catch(onGetJobInvocationsFailed)
58
+ .then(triggerPolling);
59
+ } else {
60
+ // document is not visible, keep polling without api call
61
+ triggerPolling();
62
+ }
63
+ }
64
+ };
65
+
66
+ export const startJobInvocationsPolling = url => (dispatch, getState) => {
67
+ if (getState().foremanRemoteExecutionReducers.jobInvocations.isPolling) {
68
+ return;
69
+ }
70
+ dispatch({
71
+ type: JOB_INVOCATIONS_POLLING_STARTED,
72
+ });
73
+ dispatch(getJobInvocations(url));
74
+ };
@@ -0,0 +1,6 @@
1
+ export const JOB_INVOCATIONS_POLLING_STARTED =
2
+ 'JOB_INVOCATIONS_POLLING_STARTED';
3
+ export const JOB_INVOCATIONS_GET_JOB_INVOCATIONS =
4
+ 'JOB_INVOCATIONS_GET_JOB_INVOCATIONS';
5
+ export const JOB_INVOCATIONS_JOB_FINISHED =
6
+ 'JOB_INVOCATIONS_JOB_FINISHED';
@@ -0,0 +1,6 @@
1
+ import { combineReducers } from 'redux';
2
+ import jobInvocations from './jobInvocations/';
3
+
4
+ export default combineReducers({
5
+ jobInvocations,
6
+ });
@@ -0,0 +1,78 @@
1
+ import Immutable from 'seamless-immutable';
2
+
3
+ export const initialState = Immutable({
4
+ isPolling: false,
5
+ jobInvocations: [],
6
+ statuses: [],
7
+ });
8
+
9
+ export const pollingStarted = Immutable({
10
+ isPolling: true,
11
+ jobInvocations: [],
12
+ statuses: [],
13
+ });
14
+
15
+ export const jobInvocationsPayload = Immutable({
16
+ jobInvocations: {
17
+ job_invocations: [
18
+ [
19
+ 'Success',
20
+ 100,
21
+ '#B7312D',
22
+ ],
23
+ [
24
+ 'Failed',
25
+ 20,
26
+ '#B7312D',
27
+ ],
28
+ [
29
+ 'Pending',
30
+ 40,
31
+ '#B7312D',
32
+ ],
33
+ [
34
+ 'Cancelled',
35
+ 0,
36
+ '#B7312D',
37
+ ],
38
+ ],
39
+ statuses: {
40
+ cancelled: 0,
41
+ failed: 0,
42
+ pending: 0,
43
+ success: 1,
44
+ },
45
+ },
46
+ });
47
+
48
+ export const jobInvocationsReceived = Immutable({
49
+ isPolling: true,
50
+ jobInvocations: [
51
+ [
52
+ 'Success',
53
+ 100,
54
+ '#B7312D',
55
+ ],
56
+ [
57
+ 'Failed',
58
+ 20,
59
+ '#B7312D',
60
+ ],
61
+ [
62
+ 'Pending',
63
+ 40,
64
+ '#B7312D',
65
+ ],
66
+ [
67
+ 'Cancelled',
68
+ 0,
69
+ '#B7312D',
70
+ ],
71
+ ],
72
+ statuses: {
73
+ cancelled: 0,
74
+ failed: 0,
75
+ pending: 0,
76
+ success: 1,
77
+ },
78
+ });