foreman_remote_execution 1.5.0 → 1.5.1

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