foreman_host_reports 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/host_reports_controller.rb +5 -2
  3. data/app/controllers/concerns/foreman_host_reports/controller/parameters/host_report.rb +2 -1
  4. data/app/models/host_report.rb +1 -31
  5. data/app/models/report_keyword.rb +5 -0
  6. data/app/views/api/v2/host_reports/main.json.rabl +2 -1
  7. data/db/migrate/20211011141813_change_status_column.rb +10 -0
  8. data/db/seeds.d/60-reports_feature.rb +2 -0
  9. data/lib/foreman_host_reports/version.rb +1 -1
  10. data/package.json +10 -10
  11. data/test/controllers/api/v2/host_reports_controller_test.rb +43 -20
  12. data/test/factories/foreman_host_reports_factories.rb +4 -1
  13. data/webpack/__mocks__/foremanReact/common/HOC.js +30 -0
  14. data/webpack/__mocks__/foremanReact/common/I18n.js +7 -0
  15. data/webpack/__mocks__/foremanReact/common/helpers.js +7 -0
  16. data/webpack/__mocks__/foremanReact/common/urlHelpers.js +8 -0
  17. data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalActions.js +2 -0
  18. data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalHooks.js +10 -0
  19. data/webpack/__mocks__/foremanReact/components/ForemanModal/index.js +23 -0
  20. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +4 -0
  21. data/webpack/__mocks__/foremanReact/components/common/EmptyState.js +5 -0
  22. data/webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js +7 -0
  23. data/webpack/__mocks__/foremanReact/components/common/forms/ForemanForm.js +9 -0
  24. data/webpack/__mocks__/foremanReact/components/common/forms/FormField.js +3 -0
  25. data/webpack/__mocks__/foremanReact/components/common/table.js +26 -0
  26. data/webpack/__mocks__/foremanReact/constants.js +24 -0
  27. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +6 -0
  28. data/webpack/__mocks__/foremanReact/redux/API/index.js +10 -0
  29. data/webpack/__mocks__/foremanReact/redux/actions/common/forms.js +1 -0
  30. data/webpack/__mocks__/foremanReact/redux/actions/toasts.js +8 -0
  31. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  32. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/components/ExportButton/ExportButton.js +10 -0
  33. data/webpack/src/Router/HostReports/IndexPage/Components/HostReportsTable/Components/Formatters/statusFormatter.js +9 -3
  34. data/webpack/src/Router/HostReports/IndexPage/Components/HostReportsTable/Components/StatusCell.js +24 -66
  35. data/webpack/src/Router/HostReports/IndexPage/__tests__/HostReportsIndexPage.test.js +40 -0
  36. data/webpack/src/Router/HostReports/IndexPage/__tests__/__snapshots__/HostReportsIndexPage.test.js.snap +67 -0
  37. metadata +27 -3
  38. data/webpack/test_setup.js +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df2c9864c95d5918c25782bc4fd90c8b19dbf3417bb7f0b00d1350ca55adc399
4
- data.tar.gz: 4c7d17ca2b8dbb6ffda51dbf35d92c83ae523dc7b295067076f3d9579dd5da82
3
+ metadata.gz: 488222c4e90b62231ba01365932c00ccb2619ea7f8225757bf84c423cd56b13a
4
+ data.tar.gz: 39f11ab9a01e780bff2105f09726f95b564d45169b6c9c2466e9846f0433aff8
5
5
  SHA512:
6
- metadata.gz: 41eb977481bf368bbcafdc37adad69a0cb85f3b61d052e549af5aadcaa35e33746317663f25ffbdbb34e4cf9583b315d52ccaed031334be946adcd8cd23bd907
7
- data.tar.gz: 26ff05b79fbf665b9858c11eebbe5082ed479afc0bbd3810efa0950fb10fde3e51c2e092e431b1bbf52749a57f044da777e3cdf5bf2ed7fac223c9cb703998ed
6
+ metadata.gz: 9970feb6d0dd2593977eded7cade385a1a17743da6af330f3c5fb1c338dd62a63716b07b19bc5fd7fef1b22bd20147d08d9fc41639f570c6c2b43cd02100a36d
7
+ data.tar.gz: 76f80c8de18583d372beee125c791f8eef7fd41bbcb501c318951ab7708a311eb6905f3e94b5df841700c9c2f15a1157985cb23d8ea5661aac7f45d738286879
@@ -11,7 +11,7 @@ module Api
11
11
  before_action :find_resource, only: %i[destroy]
12
12
  before_action :resolve_ids, only: %i[create]
13
13
 
14
- add_smart_proxy_filters :create, features: proc { HostReport.authorized_smart_proxy_features }
14
+ add_smart_proxy_filters :create, features: ['Reports']
15
15
 
16
16
  api :GET, '/host_reports/', N_('List host reports')
17
17
  param_group :search_and_pagination, ::Api::V2::BaseController
@@ -34,7 +34,10 @@ module Api
34
34
  param :host, String, required: true, desc: N_("Hostname of the report's host origin")
35
35
  param :format, HostReport.formats.keys, required: false, desc: N_('Format of the report, e.g. Ansible')
36
36
  param :reported_at, String, required: true, desc: N_('UTC time of the report')
37
- param :status, Integer, required: true, desc: N_('Bitfield with arbitrary amount of status counters')
37
+ param :applied, Integer, required: false, desc: N_('Number of applied resources or tasks')
38
+ param :failed, Integer, required: false, desc: N_('Number of failed resources or tasks')
39
+ param :pending, Integer, required: false, desc: N_('Number of pending resources or tasks')
40
+ param :other, Integer, required: false, desc: N_('Number of other resources or tasks')
38
41
  param :body, String, required: true, desc: N_('String with JSON formatted body of the report')
39
42
  param :proxy, String, required: false, desc: N_('Hostname of the proxy processed the report')
40
43
  param :keywords, Array, of: String, required: false, desc: N_('A list of keywords to associate with the report for better searching')
@@ -10,7 +10,8 @@ module ForemanHostReports
10
10
  def host_report_params_filter
11
11
  Foreman::ParameterFilter.new(::HostReport).tap do |filter|
12
12
  # body is permitted in controller
13
- filter.permit :format, :version, :host, :proxy, :reported_at, :status, :proxy_id, :host_id
13
+ filter.permit :format, :version, :host, :proxy, :reported_at,
14
+ :proxy_id, :host_id, :applied, :failed, :pending, :other
14
15
  filter.permit :keywords => []
15
16
  end
16
17
  end
@@ -10,7 +10,7 @@ class HostReport < ApplicationRecord
10
10
  has_one :organization, through: :host
11
11
  has_one :location, through: :host
12
12
 
13
- validates :host_id, :reported_at, :status, presence: true
13
+ validates :host_id, :reported_at, presence: true
14
14
 
15
15
  enum format: {
16
16
  # plain text report (no processing)
@@ -20,10 +20,6 @@ class HostReport < ApplicationRecord
20
20
  ansible: 2,
21
21
  }.freeze
22
22
 
23
- STATUS = {
24
- plain: %w[debug normal warning error],
25
- }.freeze
26
-
27
23
  scoped_search relation: :host, on: :name, complete_value: true, rename: :host, aliases: %i[host_name]
28
24
  scoped_search relation: :proxy, on: :name, complete_value: true, rename: :proxy
29
25
  scoped_search relation: :organization, on: :name, complete_value: true, rename: :organization
@@ -45,36 +41,10 @@ class HostReport < ApplicationRecord
45
41
 
46
42
  default_scope -> { order('reported_at DESC') }
47
43
 
48
- # TODO: temporary until we decide what will status bitfiled be exactly
49
- # rubocop:disable Style/RescueModifier
50
- def status
51
- @status ||= case format
52
- when 'puppet'
53
- JSON.parse(body)['metrics']['resources']['values'] rescue []
54
- when 'ansible'
55
- JSON.parse(body)['status'] rescue {}
56
- else
57
- super
58
- end
59
- end
60
- # rubocop:enable Style/RescueModifier
61
-
62
44
  def report_keywords
63
45
  ReportKeyword.where(id: report_keyword_ids)
64
46
  end
65
47
 
66
- def self.authorized_smart_proxy_features
67
- @authorized_smart_proxy_features ||= %w[Puppet Ansible]
68
- end
69
-
70
- def self.register_smart_proxy_feature(feature)
71
- @authorized_smart_proxy_features = (authorized_smart_proxy_features + [feature]).uniq
72
- end
73
-
74
- def self.unregister_smart_proxy_feature(feature)
75
- @authorized_smart_proxy_features -= [feature]
76
- end
77
-
78
48
  def self.search_by_keyword(_key, operator, value)
79
49
  conditions = sanitize_sql_for_conditions(["report_keywords.name #{operator} ?", value_to_sql(operator, value)])
80
50
  keyword_ids = ReportKeyword.where(conditions).distinct.pluck(:id)
@@ -8,4 +8,9 @@ class ReportKeyword < ApplicationRecord
8
8
  def self.klass
9
9
  self
10
10
  end
11
+
12
+ # Needed for ActiveRecord to simulate ActiveRecord::Associations::CollectionProxy
13
+ def self.macro
14
+ :has_many
15
+ end
11
16
  end
@@ -5,7 +5,8 @@ object @host_report
5
5
  extends 'api/v2/host_reports/base'
6
6
  extends 'api/v2/layouts/permissions'
7
7
 
8
- attributes :format, :host_id, :proxy_id, :reported_at, :status
8
+ attributes :format, :host_id, :proxy_id, :reported_at, :applied, :failed,
9
+ :pending, :other
9
10
 
10
11
  node(:host_name) do |report|
11
12
  report.host.name
@@ -0,0 +1,10 @@
1
+ class ChangeStatusColumn < ActiveRecord::Migration[6.0]
2
+ def change
3
+ remove_column :host_reports, :status, :bigint
4
+
5
+ add_column :host_reports, :applied, :integer, default: 0
6
+ add_column :host_reports, :failed, :integer, default: 0
7
+ add_column :host_reports, :pending, :integer, default: 0
8
+ add_column :host_reports, :other, :integer, default: 0
9
+ end
10
+ end
@@ -0,0 +1,2 @@
1
+ f = Feature.where(name: 'Reports').first_or_create
2
+ raise "Unable to create host reports proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
@@ -1,3 +1,3 @@
1
1
  module ForemanHostReports
2
- VERSION = '0.0.3'.freeze
2
+ VERSION = '0.0.4'.freeze
3
3
  end
data/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "lint": "tfm-lint --plugin -d /webpack",
8
- "test": "tfm-test --config jest.config.js",
8
+ "test": "tfm-test --plugin",
9
9
  "test:watch": "tfm-test --plugin --watchAll",
10
10
  "test:current": "tfm-test --plugin --watch",
11
11
  "publish-coverage": "tfm-publish-coverage",
@@ -21,25 +21,25 @@
21
21
  "url": "http://projects.theforeman.org/projects/foreman_host_reports/issues"
22
22
  },
23
23
  "peerDependencies": {
24
- "@theforeman/vendor": ">= 6.0.0"
24
+ "@theforeman/vendor": "^8.15.0"
25
25
  },
26
26
  "dependencies": {
27
- "react-intl": "^2.8.0",
28
27
  "react-json-tree": "^0.11.0"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@babel/core": "^7.7.0",
32
31
  "@sheerun/mutationobserver-shim": "^0.3.3",
33
- "@theforeman/builder": "^8.4.1",
34
- "@theforeman/eslint-plugin-foreman": "8.4.1",
35
- "@theforeman/find-foreman": "^8.4.1",
36
- "@theforeman/stories": "^8.4.1",
37
- "@theforeman/test": "^8.4.1",
38
- "@theforeman/vendor-dev": "^8.4.1",
32
+ "@theforeman/builder": "^8.15.0",
33
+ "@theforeman/eslint-plugin-foreman": "^8.15.0",
34
+ "@theforeman/find-foreman": "^8.15.0",
35
+ "@theforeman/stories": "^8.15.0",
36
+ "@theforeman/test": "^8.15.0",
37
+ "@theforeman/vendor-dev": "^8.15.0",
39
38
  "babel-eslint": "^10.0.3",
40
39
  "eslint": "^6.7.2",
41
40
  "prettier": "^1.19.1",
42
41
  "stylelint-config-standard": "^18.0.0",
43
- "stylelint": "^9.3.0"
42
+ "stylelint": "^9.3.0",
43
+ "jed": "^1.1.1"
44
44
  }
45
45
  }
@@ -1,6 +1,15 @@
1
1
  require 'test_plugin_helper'
2
2
 
3
3
  class Api::V2::HostReportsControllerTest < ActionController::TestCase
4
+ setup do
5
+ @proxy = FactoryBot.create(:smart_proxy,
6
+ url: "http://reports.foreman.example.com",
7
+ features: [FactoryBot.create(:feature, name: 'Reports')])
8
+ Resolv.any_instance.stubs(:getnames).returns([URI.parse(@proxy.url).host])
9
+ SmartProxy.any_instance.stubs(:with_features).returns([@proxy])
10
+ ProxyAPI::V2::Features.any_instance.stubs(:features).returns({ "reports" => { "state" => "running" } })
11
+ end
12
+
4
13
  context 'when user does not have permission to view hosts' do
5
14
  let :host_report do
6
15
  as_admin { FactoryBot.create(:host_report) }
@@ -38,7 +47,7 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
38
47
  post :create, params: {
39
48
  host_report: {
40
49
  host: host.name, body: report_body, reported_at: Time.current,
41
- status: 0
50
+ applied: 5, failed: 1, pending: 1, other: 0
42
51
  },
43
52
  }, session: set_session_user
44
53
  assert_response :success
@@ -50,12 +59,30 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
50
59
  assert_response :unprocessable_entity
51
60
  end
52
61
 
62
+ test 'store statuses' do
63
+ User.current = nil
64
+ post :create, params: {
65
+ host_report: {
66
+ host: host.name, body: report_body, reported_at: Time.current,
67
+ keywords: %w[HasError HasFailedResource],
68
+ applied: 5, failed: 1, pending: 1, other: 6
69
+ },
70
+ }, session: set_session_user
71
+ report = ActiveSupport::JSON.decode(@response.body)
72
+ assert_response :created
73
+ assert_equal 5, report['applied']
74
+ assert_equal 1, report['failed']
75
+ assert_equal 1, report['pending']
76
+ assert_equal 6, report['other']
77
+ end
78
+
53
79
  test 'assign keywords' do
54
80
  User.current = nil
55
81
  post :create, params: {
56
82
  host_report: {
57
83
  host: host.name, body: report_body, reported_at: Time.current,
58
- status: 0, keywords: %w[HasError HasFailedResource]
84
+ keywords: %w[HasError HasFailedResource],
85
+ applied: 5, failed: 1, pending: 1, other: 0
59
86
  },
60
87
  }, session: set_session_user
61
88
  report = ActiveSupport::JSON.decode(@response.body)
@@ -69,14 +96,16 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
69
96
  post :create, params: {
70
97
  host_report: {
71
98
  host: host.name, body: report_body, reported_at: Time.current,
72
- status: 0, keywords: %w[HasError HasFailedResource]
99
+ keywords: %w[HasError HasFailedResource],
100
+ applied: 5, failed: 1, pending: 1, other: 0
73
101
  },
74
102
  }, session: set_session_user
75
103
 
76
104
  post :create, params: {
77
105
  host_report: {
78
106
  host: host.name, body: report_body, reported_at: Time.current,
79
- status: 0, keywords: %w[HasError HasFailedResource]
107
+ keywords: %w[HasError HasFailedResource],
108
+ applied: 5, failed: 1, pending: 1, other: 0
80
109
  },
81
110
  }, session: set_session_user
82
111
  end
@@ -90,7 +119,7 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
90
119
  post :create, params: {
91
120
  host_report: {
92
121
  host: host.name, body: report_body, reported_at: Time.current,
93
- status: 0
122
+ applied: 5, failed: 1, pending: 1, other: 0
94
123
  },
95
124
  }
96
125
  assert_nil @controller.detected_proxy
@@ -101,18 +130,13 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
101
130
  Setting[:restrict_registered_smart_proxies] = true
102
131
  Setting[:require_ssl_smart_proxies] = false
103
132
 
104
- stub_smart_proxy_v2_features
105
- proxy = smart_proxies(:puppetmaster)
106
- as_admin { proxy.update_attribute(:url, 'http://configreports.foreman') }
107
- proxy_host = URI.parse(proxy.url).host
108
- Resolv.any_instance.stubs(:getnames).returns([proxy_host])
109
133
  post :create, params: {
110
134
  host_report: {
111
135
  host: host.name, body: report_body, reported_at: Time.current,
112
- status: 0
136
+ applied: 5, failed: 1, pending: 1, other: 0
113
137
  },
114
138
  }
115
- assert_equal proxy, @controller.detected_proxy
139
+ assert_equal @proxy, @controller.detected_proxy
116
140
  assert_response :created
117
141
  end
118
142
 
@@ -124,7 +148,7 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
124
148
  post :create, params: {
125
149
  host_report: {
126
150
  host: host.name, body: report_body, reported_at: Time.current,
127
- status: 0
151
+ applied: 5, failed: 1, pending: 1, other: 0
128
152
  },
129
153
  }
130
154
  assert_response :forbidden
@@ -135,12 +159,12 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
135
159
  Setting[:require_ssl_smart_proxies] = true
136
160
 
137
161
  @request.env['HTTPS'] = 'on'
138
- @request.env['SSL_CLIENT_S_DN'] = 'CN=else.where'
162
+ @request.env['SSL_CLIENT_S_DN'] = 'CN=reports.foreman.example.com'
139
163
  @request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
140
164
  post :create, params: {
141
165
  host_report: {
142
166
  host: host.name, body: report_body, reported_at: Time.current,
143
- status: 0
167
+ applied: 5, failed: 1, pending: 1, other: 0
144
168
  },
145
169
  }
146
170
  assert_response :created
@@ -156,7 +180,7 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
156
180
  post :create, params: {
157
181
  host_report: {
158
182
  host: host.name, body: report_body, reported_at: Time.current,
159
- status: 0
183
+ applied: 5, failed: 1, pending: 1, other: 0
160
184
  },
161
185
  }
162
186
  assert_response :forbidden
@@ -172,7 +196,7 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
172
196
  post :create, params: {
173
197
  host_report: {
174
198
  host: host.name, body: report_body, reported_at: Time.current,
175
- status: 0
199
+ applied: 5, failed: 1, pending: 1, other: 0
176
200
  },
177
201
  }
178
202
  assert_response :forbidden
@@ -187,7 +211,7 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
187
211
  post :create, params: {
188
212
  host_report: {
189
213
  host: host.name, body: report_body, reported_at: Time.current,
190
- status: 0
214
+ applied: 5, failed: 1, pending: 1, other: 0
191
215
  },
192
216
  }
193
217
  assert_response :forbidden
@@ -199,11 +223,10 @@ class Api::V2::HostReportsControllerTest < ActionController::TestCase
199
223
  Setting[:require_ssl_smart_proxies] = true
200
224
  SETTINGS[:require_ssl] = false
201
225
 
202
- Resolv.any_instance.stubs(:getnames).returns(['else.where'])
203
226
  post :create, params: {
204
227
  host_report: {
205
228
  host: host.name, body: report_body, reported_at: Time.current,
206
- status: 0
229
+ applied: 5, failed: 1, pending: 1, other: 0
207
230
  },
208
231
  }
209
232
  assert_response :created
@@ -2,7 +2,10 @@ FactoryBot.define do
2
2
  factory :host_report do
3
3
  host
4
4
  reported_at { Time.now.utc }
5
- status { 0 }
5
+ applied { 0 }
6
+ failed { 0 }
7
+ pending { 0 }
8
+ other { 0 }
6
9
  body { 'report data' }
7
10
  end
8
11
 
@@ -0,0 +1,30 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ export const withRenderHandler = ({ Component }) => componentProps => (
4
+ <Component {...componentProps} />
5
+ );
6
+
7
+ export const callOnMount = callback => WrappedComponent => componentProps => {
8
+ callback(componentProps);
9
+
10
+ return <WrappedComponent {...componentProps} />;
11
+ };
12
+
13
+ export const callOnPopState = callback => WrappedComponent => componentProps => {
14
+ const didMount = useRef(false);
15
+ const {
16
+ history: {
17
+ action,
18
+ location: { search },
19
+ },
20
+ } = componentProps;
21
+ useEffect(() => {
22
+ if (action === 'POP' && didMount.current) {
23
+ callback(componentProps);
24
+ } else {
25
+ didMount.current = true;
26
+ }
27
+ }, [search, action, componentProps]);
28
+
29
+ return <WrappedComponent {...componentProps} />;
30
+ };
@@ -0,0 +1,7 @@
1
+ export { sprintf } from 'jed';
2
+
3
+ export const translate = s => s;
4
+
5
+ export const ngettext = s => s;
6
+
7
+ export const documentLocale = () => 'en';
@@ -0,0 +1,7 @@
1
+ export const getURIQuery = jest.fn(() => ({}));
2
+
3
+ export const deepPropsToCamelCase = jest.fn(props => props);
4
+
5
+ export const foremanUrl = jest.fn(() => '');
6
+
7
+ export const noop = Function.prototype;
@@ -0,0 +1,8 @@
1
+ export const getURIsearch = () => 'a=b';
2
+
3
+ export const getParams = () => ({
4
+ page: 1,
5
+ perPage: 20,
6
+ searchQuery: '',
7
+ sort: {},
8
+ });
@@ -0,0 +1,2 @@
1
+ const ForemanModalActions = () => jest.fn();
2
+ export default ForemanModalActions;
@@ -0,0 +1,10 @@
1
+ const modalOpen = true;
2
+ const setModalOpen = jest.fn();
3
+ const setModalClosed = jest.fn();
4
+
5
+ export const useForemanModal = jest.fn(() => ({
6
+ modalOpen,
7
+ setModalOpen,
8
+ setModalClosed,
9
+ }));
10
+ export default useForemanModal;
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const ForemanModal = ({ children }) => <div className="modal">{children}</div>;
5
+ ForemanModal.Header = ({ children }) => (
6
+ <div className="modal-header">{children}</div>
7
+ );
8
+ ForemanModal.Footer = ({ children }) => (
9
+ <div className="modal-footer">{children}</div>
10
+ );
11
+
12
+ ForemanModal.propTypes = {
13
+ children: PropTypes.node,
14
+ };
15
+
16
+ ForemanModal.defaultProps = {
17
+ children: [],
18
+ };
19
+
20
+ ForemanModal.Header.propTypes = ForemanModal.propTypes;
21
+ ForemanModal.Footer.propTypes = ForemanModal.propTypes;
22
+
23
+ export default ForemanModal;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+
3
+ const PaginationWrapper = () => <></>;
4
+ export default PaginationWrapper;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ export const DefaultEmptyState = () => (
4
+ <div className="empty-state-description" />
5
+ );
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ const RelativeDateTime = (date, defaultValue, context) => (
4
+ <span>{defaultValue || date}</span>
5
+ );
6
+
7
+ export default RelativeDateTime;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+
3
+ const ForemanForm = () => (
4
+ <React.Fragment>
5
+ <form />
6
+ </React.Fragment>
7
+ );
8
+
9
+ export default ForemanForm;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+
3
+ export const ForemanField = props => <div />;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ export const Table = () => <div className="table" />;
4
+ export const createTableReducer = jest.fn(controller => controller);
5
+ export const cellFormatter = cell => cell;
6
+ export const deleteActionCellFormatter = cell => cell;
7
+ export const sortableColumn = jest.fn();
8
+ export const column = (
9
+ property,
10
+ label,
11
+ headFormat,
12
+ cellFormat,
13
+ headProps = {},
14
+ cellProps = {}
15
+ ) => ({
16
+ property,
17
+ header: {
18
+ label,
19
+ props: headProps,
20
+ formatters: headFormat,
21
+ },
22
+ cell: {
23
+ props: cellProps,
24
+ formatters: cellFormat,
25
+ },
26
+ });
@@ -0,0 +1,24 @@
1
+ export const STATUS = {
2
+ PENDING: 'PENDING',
3
+ RESOLVED: 'RESOLVED',
4
+ ERROR: 'ERROR',
5
+ };
6
+
7
+ export const getControllerSearchProps = (
8
+ controller,
9
+ id = 'searchBar',
10
+ canCreateBookmarks = true
11
+ ) => ({
12
+ controller,
13
+ autocomplete: {
14
+ id,
15
+ searchQuery: '',
16
+ url: `${controller}/auto_complete_search`,
17
+ useKeyShortcuts: true,
18
+ },
19
+ bookmarks: {
20
+ url: '/api/bookmarks',
21
+ canCreateBookmarks,
22
+ documentationUrl: `4.1.5Searching`,
23
+ },
24
+ });
@@ -0,0 +1,6 @@
1
+ export const selectAPIResponse = (state, key) =>
2
+ selectAPIByKey(state, key).response;
3
+
4
+ export const selectAPIStatus = (state, key) => 'PENDING';
5
+ export const selectAPIByKey = (state, key) => state.API[key];
6
+ export const selectAPIError = (state, key) => ({ error: `${key} ERRROR` });
@@ -0,0 +1,10 @@
1
+ export const API = {
2
+ get: jest.fn(),
3
+ put: jest.fn(),
4
+ post: jest.fn(),
5
+ delete: jest.fn(),
6
+ patch: jest.fn(),
7
+ };
8
+
9
+ export const get = data => ({ type: 'get-some-type', ...data });
10
+ export const post = data => ({ type: 'post-some-type', ...data });
@@ -0,0 +1 @@
1
+ export const submitForm = jest.fn(() => '');
@@ -0,0 +1,8 @@
1
+ export const addToast = toast => ({
2
+ type: 'TOASTS_ADD',
3
+ payload: {
4
+ message: toast,
5
+ },
6
+ });
7
+
8
+ export default addToast;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const PageLayout = ({ children }) => <div>{children}</div>;
5
+
6
+ PageLayout.propTypes = {
7
+ children: PropTypes.node.isRequired,
8
+ };
9
+
10
+ export default PageLayout;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { Button } from 'patternfly-react';
3
+
4
+ const ExportButton = props => (
5
+ <Button className="export-csv" href="#" title="Export">
6
+ Export
7
+ </Button>
8
+ );
9
+
10
+ export default ExportButton;
@@ -2,8 +2,14 @@ import React from 'react';
2
2
 
3
3
  import StatusCell from '../StatusCell';
4
4
 
5
- const statusFormatter = () => (value, { rowData: { format, status } }) => (
6
- <StatusCell format={format} value={value} />
7
- );
5
+ const statusFormatter = () => (_, { rowData }) => {
6
+ const statuses = {
7
+ applied: rowData.applied,
8
+ failed: rowData.failed,
9
+ pending: rowData.pending,
10
+ other: rowData.other,
11
+ };
12
+ return <StatusCell statuses={statuses} />;
13
+ };
8
14
 
9
15
  export default statusFormatter;
@@ -4,74 +4,32 @@ import { capitalize } from 'lodash';
4
4
 
5
5
  import './StatusCell.scss';
6
6
 
7
- const StatusCell = ({ format, value }) => {
8
- switch (format) {
9
- case 'puppet':
10
- return (
11
- <td>
12
- <ul className="status-list">
13
- {value.map(status => {
14
- let style = '';
15
- switch (status[0]) {
16
- case 'failed':
17
- style = 'label-danger';
18
- break;
19
- case 'failed_to_restart':
20
- style = 'label-warning';
21
- break;
22
- default:
23
- style = 'label-info';
24
- }
25
- if (!status[2]) style = 'label-default';
26
- return (
27
- <li key={status[0]}>
28
- {`${status[1]}: `}
29
- <span className={`label ${style}`}>{status[2]}</span>
30
- </li>
31
- );
32
- })}
33
- </ul>
34
- </td>
35
- );
36
- case 'ansible':
37
- return (
38
- <td>
39
- <ul className="status-list">
40
- {Object.keys(value).map(status => {
41
- let style = '';
42
- switch (status) {
43
- case 'failed':
44
- style = 'label-danger';
45
- break;
46
- case 'skipped':
47
- style = 'label-warning';
48
- break;
49
- default:
50
- style = 'label-info';
51
- }
52
- if (!value[status]) style = 'label-default';
53
- return (
54
- <li key={status}>
55
- {`${capitalize(status)}: `}
56
- <span className={`label ${style}`}>{value[status]}</span>
57
- </li>
58
- );
59
- })}
60
- </ul>
61
- </td>
62
- );
63
- default:
64
- return <td>N/A</td>;
65
- }
66
- };
7
+ const StatusCell = ({ statuses }) => (
8
+ <td>
9
+ <ul className="status-list">
10
+ {Object.keys(statuses).map(status => {
11
+ let style = '';
12
+ switch (status) {
13
+ case 'failed':
14
+ style = 'label-danger';
15
+ break;
16
+ default:
17
+ style = 'label-info';
18
+ }
19
+ if (!statuses[status]) style = 'label-default';
20
+ return (
21
+ <li key={status}>
22
+ {`${capitalize(status)}: `}
23
+ <span className={`label ${style}`}>{statuses[status]}</span>
24
+ </li>
25
+ );
26
+ })}
27
+ </ul>
28
+ </td>
29
+ );
67
30
 
68
31
  StatusCell.propTypes = {
69
- format: PropTypes.string,
70
- value: PropTypes.any.isRequired,
71
- };
72
-
73
- StatusCell.defaultProps = {
74
- format: 'plain',
32
+ statuses: PropTypes.object.isRequired,
75
33
  };
76
34
 
77
35
  export default StatusCell;
@@ -0,0 +1,40 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import HostReportsIndexPage from '../IndexPage';
3
+
4
+ const fixtures = {
5
+ 'render with minimal props': {
6
+ search: '',
7
+ history: { location: { search: '' } },
8
+ hostId: '1',
9
+ fetchAndPush: jest.fn(),
10
+ reloadWithSearch: jest.fn(),
11
+ isLoading: false,
12
+ hasError: false,
13
+ hasData: true,
14
+ itemCount: 1,
15
+ canCreate: true,
16
+ sort: {},
17
+ page: 1,
18
+ perPage: 20,
19
+ reports: [
20
+ {
21
+ id: '1',
22
+ hostId: '1',
23
+ hostName: 'foreman.example.com',
24
+ proxyId: '1',
25
+ proxyName: 'foreman.example.com',
26
+ format: 'plain',
27
+ reportedAt: '2021-01-19T12:34:02.841645028Z',
28
+ applied: 0,
29
+ failed: 0,
30
+ pending: 0,
31
+ other: 0,
32
+ },
33
+ ],
34
+ },
35
+ };
36
+
37
+ describe('HostReportsIndexPage', () => {
38
+ describe('redering', () =>
39
+ testComponentSnapshotsWithFixtures(HostReportsIndexPage, fixtures));
40
+ });
@@ -0,0 +1,67 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`HostReportsIndexPage redering render with minimal props 1`] = `
4
+ <PageLayout
5
+ header="Host Reports"
6
+ isLoading={false}
7
+ onBookmarkClick={[MockFunction]}
8
+ onSearch={[MockFunction]}
9
+ searchProps={
10
+ Object {
11
+ "autocomplete": Object {
12
+ "id": "searchBar",
13
+ "searchQuery": "",
14
+ "url": "host_reports/auto_complete_search",
15
+ "useKeyShortcuts": true,
16
+ },
17
+ "bookmarks": Object {
18
+ "canCreateBookmarks": true,
19
+ "documentationUrl": "4.1.5Searching",
20
+ "url": "/api/bookmarks",
21
+ },
22
+ "controller": "host_reports",
23
+ }
24
+ }
25
+ searchQuery=""
26
+ searchable={true}
27
+ toolbarButtons={
28
+ <ExportButton
29
+ title="Export"
30
+ url="/api/v2/host_reports/export"
31
+ />
32
+ }
33
+ >
34
+ <WrappedHostReportsTable
35
+ fetchAndPush={[MockFunction]}
36
+ hostId="1"
37
+ itemCount={1}
38
+ pagination={
39
+ Object {
40
+ "page": 1,
41
+ "perPage": 20,
42
+ }
43
+ }
44
+ reloadWithSearch={[MockFunction]}
45
+ results={
46
+ Array [
47
+ Object {
48
+ "applied": 0,
49
+ "failed": 0,
50
+ "format": "plain",
51
+ "hostId": "1",
52
+ "hostName": "foreman.example.com",
53
+ "id": "1",
54
+ "other": 0,
55
+ "pending": 0,
56
+ "proxyId": "1",
57
+ "proxyName": "foreman.example.com",
58
+ "reportedAt": "2021-01-19T12:34:02.841645028Z",
59
+ },
60
+ ]
61
+ }
62
+ setToDelete={[Function]}
63
+ sort={Object {}}
64
+ toDelete={Object {}}
65
+ />
66
+ </PageLayout>
67
+ `;
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_host_reports
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukas Zapletal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-18 00:00:00.000000000 Z
11
+ date: 2021-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rdoc
@@ -48,6 +48,8 @@ files:
48
48
  - config/routes.rb
49
49
  - db/migrate/20210112183526_add_host_reports.rb
50
50
  - db/migrate/20210616133601_create_report_keywords.rb
51
+ - db/migrate/20211011141813_change_status_column.rb
52
+ - db/seeds.d/60-reports_feature.rb
51
53
  - lib/foreman_host_reports.rb
52
54
  - lib/foreman_host_reports/engine.rb
53
55
  - lib/foreman_host_reports/version.rb
@@ -62,6 +64,26 @@ files:
62
64
  - test/snapshots/foreman-web.json
63
65
  - test/test_plugin_helper.rb
64
66
  - test/unit/foreman_host_reports_test.rb
67
+ - webpack/__mocks__/foremanReact/common/HOC.js
68
+ - webpack/__mocks__/foremanReact/common/I18n.js
69
+ - webpack/__mocks__/foremanReact/common/helpers.js
70
+ - webpack/__mocks__/foremanReact/common/urlHelpers.js
71
+ - webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalActions.js
72
+ - webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalHooks.js
73
+ - webpack/__mocks__/foremanReact/components/ForemanModal/index.js
74
+ - webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js
75
+ - webpack/__mocks__/foremanReact/components/common/EmptyState.js
76
+ - webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js
77
+ - webpack/__mocks__/foremanReact/components/common/forms/ForemanForm.js
78
+ - webpack/__mocks__/foremanReact/components/common/forms/FormField.js
79
+ - webpack/__mocks__/foremanReact/components/common/table.js
80
+ - webpack/__mocks__/foremanReact/constants.js
81
+ - webpack/__mocks__/foremanReact/redux/API/APISelectors.js
82
+ - webpack/__mocks__/foremanReact/redux/API/index.js
83
+ - webpack/__mocks__/foremanReact/redux/actions/common/forms.js
84
+ - webpack/__mocks__/foremanReact/redux/actions/toasts.js
85
+ - webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js
86
+ - webpack/__mocks__/foremanReact/routes/common/PageLayout/components/ExportButton/ExportButton.js
65
87
  - webpack/global_index.js
66
88
  - webpack/global_test_setup.js
67
89
  - webpack/index.js
@@ -87,6 +109,8 @@ files:
87
109
  - webpack/src/Router/HostReports/IndexPage/IndexPageActions.js
88
110
  - webpack/src/Router/HostReports/IndexPage/IndexPageHelpers.js
89
111
  - webpack/src/Router/HostReports/IndexPage/IndexPageSelectors.js
112
+ - webpack/src/Router/HostReports/IndexPage/__tests__/HostReportsIndexPage.test.js
113
+ - webpack/src/Router/HostReports/IndexPage/__tests__/__snapshots__/HostReportsIndexPage.test.js.snap
90
114
  - webpack/src/Router/HostReports/IndexPage/constants.js
91
115
  - webpack/src/Router/HostReports/IndexPage/index.js
92
116
  - webpack/src/Router/HostReports/ShowPage/Components/HostReportMetrics/HostReportMetrics.scss
@@ -101,7 +125,6 @@ files:
101
125
  - webpack/src/Router/HostReports/ShowPage/index.js
102
126
  - webpack/src/Router/HostReports/constants.js
103
127
  - webpack/src/Router/routes.js
104
- - webpack/test_setup.js
105
128
  homepage: https://github.com/theforeman/foreman_host_reports
106
129
  licenses:
107
130
  - GPL-3.0
@@ -131,3 +154,4 @@ test_files:
131
154
  - test/controllers/api/v2/host_reports_controller_test.rb
132
155
  - test/snapshots/foreman-web.json
133
156
  - test/test_plugin_helper.rb
157
+ - webpack/src/Router/HostReports/IndexPage/__tests__/HostReportsIndexPage.test.js
@@ -1,17 +0,0 @@
1
- import 'core-js/shim';
2
- import 'regenerator-runtime/runtime';
3
- import MutationObserver from '@sheerun/mutationobserver-shim';
4
-
5
- import { configure } from 'enzyme';
6
- import Adapter from 'enzyme-adapter-react-16';
7
-
8
- configure({ adapter: new Adapter() });
9
-
10
- // Mocking translation function
11
- global.__ = text => text; // eslint-disable-line
12
-
13
- // Mocking locales to prevent unnecessary fallback messages
14
- window.locales = { en: { domain: 'app', locale_data: { app: { '': {} } } } };
15
-
16
- // see https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0
17
- window.MutationObserver = MutationObserver;