foreman_openscap 4.3.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/compliance/arf_reports_controller.rb +0 -6
  3. data/app/controllers/api/v2/compliance/oval_policies_controller.rb +1 -1
  4. data/app/graphql/mutations/oval_contents/delete.rb +9 -0
  5. data/app/graphql/mutations/oval_policies/delete.rb +9 -0
  6. data/app/graphql/mutations/oval_policies/update.rb +15 -0
  7. data/app/graphql/types/oval_check.rb +11 -0
  8. data/app/graphql/types/oval_content.rb +2 -0
  9. data/app/graphql/types/oval_policy.rb +3 -0
  10. data/app/helpers/arf_report_dashboard_helper.rb +2 -4
  11. data/app/helpers/compliance_hosts_helper.rb +1 -1
  12. data/app/helpers/policies_helper.rb +1 -1
  13. data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
  14. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +15 -0
  15. data/app/models/foreman_openscap/oval_content.rb +2 -0
  16. data/app/services/foreman_openscap/client_config/base.rb +1 -0
  17. data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
  18. data/app/services/foreman_openscap/oval/configure.rb +1 -1
  19. data/app/services/foreman_openscap/oval/setup.rb +5 -5
  20. data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
  21. data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
  22. data/app/views/arf_reports/_metrics.html.erb +4 -4
  23. data/app/views/compliance_hosts/show.html.erb +4 -6
  24. data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
  25. data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
  26. data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
  27. data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
  28. data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
  29. data/lib/foreman_openscap/engine.rb +5 -7
  30. data/lib/foreman_openscap/version.rb +1 -1
  31. data/package.json +3 -6
  32. data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
  33. data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
  34. data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
  35. data/test/graphql/queries/oval_content_query_test.rb +29 -0
  36. data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
  37. data/test/helpers/policy_dashboard_helper_test.rb +1 -1
  38. data/test/test_plugin_helper.rb +9 -4
  39. data/test/unit/policy_test.rb +1 -1
  40. data/test/unit/services/config_name_service_test.rb +1 -0
  41. data/test/unit/services/hostgroup_overrider_test.rb +2 -1
  42. data/test/unit/services/lookup_key_overrider_test.rb +4 -1
  43. data/test/unit/services/oval/setup_check_test.rb +37 -0
  44. data/webpack/components/ConfirmModal.js +63 -0
  45. data/webpack/components/ConfirmModal.scss +3 -0
  46. data/webpack/components/EditableInput.js +157 -0
  47. data/webpack/components/EditableInput.scss +3 -0
  48. data/webpack/components/EmptyState.js +12 -3
  49. data/webpack/components/IndexLayout.js +11 -4
  50. data/webpack/components/IndexTable/index.js +17 -18
  51. data/webpack/components/LinkButton.js +26 -0
  52. data/webpack/components/withDeleteModal.js +51 -0
  53. data/webpack/components/withLoading.js +41 -4
  54. data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
  55. data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
  56. data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
  57. data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
  58. data/webpack/graphql/queries/cves.gql +5 -0
  59. data/webpack/graphql/queries/hostgroups.gql +14 -0
  60. data/webpack/graphql/queries/ovalContent.gql +8 -0
  61. data/webpack/graphql/queries/ovalContents.gql +8 -0
  62. data/webpack/graphql/queries/ovalPolicies.gql +8 -0
  63. data/webpack/graphql/queries/ovalPolicy.gql +5 -0
  64. data/webpack/helpers/formFieldsHelper.js +63 -0
  65. data/webpack/helpers/mutationHelper.js +68 -0
  66. data/webpack/helpers/pathsHelper.js +5 -0
  67. data/webpack/helpers/permissionsHelper.js +42 -0
  68. data/webpack/helpers/toastHelper.js +3 -0
  69. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
  70. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
  71. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
  72. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
  73. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +93 -77
  74. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
  75. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
  76. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
  77. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
  78. data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
  79. data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
  80. data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
  81. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
  82. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
  83. data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
  84. data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
  85. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
  86. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +16 -3
  87. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
  88. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
  89. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
  90. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
  91. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
  92. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
  93. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +85 -0
  94. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
  95. data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
  96. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -12
  97. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +77 -0
  98. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
  99. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +175 -0
  100. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +40 -4
  101. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
  102. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
  103. data/webpack/routes/routes.js +14 -0
  104. data/webpack/testHelper.js +42 -2
  105. metadata +53 -7
@@ -7,14 +7,13 @@ class ArfReportDashboardHelperTest < ActionView::TestCase
7
7
  categories = { :passed => 'passed', :failed => 'failed' }
8
8
  report = { :passed => 23, :failed => 24 }
9
9
  colors = { :passed => '#FFF', :failed => '#000' }
10
- res = JSON.parse(breakdown_chart_data(categories, report, colors))
10
+ res = breakdown_chart_data(categories, report, colors)
11
11
  assert_equal ["passed", 23, "#FFF"], res.first
12
12
  assert_equal ["failed", 24, "#000"], res.last
13
13
  end
14
14
 
15
15
  test 'should return breakdown chart data for donut as json' do
16
- report = { :passed => 4, :failed => 7, :othered => 5 }
17
- res = JSON.parse(donut_breakdown_chart_data(report))
16
+ res = donut_breakdown_chart_data(:passed => 4, :failed => 7, :othered => 5)
18
17
  assert_equal 3, res.size
19
18
  assert_include res, ["Passed", 4, ArfReportDashboardHelper::COLORS[:passed]]
20
19
  assert_include res, ["Failed", 7, ArfReportDashboardHelper::COLORS[:failed]]
@@ -22,12 +21,12 @@ class ArfReportDashboardHelperTest < ActionView::TestCase
22
21
  end
23
22
 
24
23
  test 'should return data for report status chart' do
25
- res = JSON.parse(arf_report_status_chart_data(:passed => 6, :failed => 7, :othered => 8))
26
- assert_equal "Number of Events", res['yAxisLabel']
27
- assert_equal "Rule Results", res['xAxisLabel']
28
- assert_equal 3, res['data'].size
29
- assert_include res['data'], ["passed", 6]
30
- assert_include res['data'], ["failed", 7]
31
- assert_include res['data'], ["othered", 8]
24
+ res = arf_report_status_chart_data(:passed => 6, :failed => 7, :othered => 8)
25
+ assert_equal "Number of Events", res[:yAxisLabel]
26
+ assert_equal "Rule Results", res[:xAxisLabel]
27
+ assert_equal 3, res[:data].size
28
+ assert_include res[:data], [:passed, 6]
29
+ assert_include res[:data], [:failed, 7]
30
+ assert_include res[:data], [:othered, 8]
32
31
  end
33
32
  end
@@ -11,7 +11,7 @@ class PolicyDashboardHelperTest < ActionView::TestCase
11
11
  :inconclusive_hosts => 7,
12
12
  :report_missing => 8
13
13
  }
14
- res = JSON.parse(policy_breakdown_chart_data(report))
14
+ res = policy_breakdown_chart_data(report)
15
15
  assert_equal 4, res.size
16
16
  assert_include res, ['Compliant hosts', 5, PolicyDashboardHelper::COLORS[:compliant_hosts]]
17
17
  assert_include res, ['Incompliant hosts', 6, PolicyDashboardHelper::COLORS[:incompliant_hosts]]
@@ -5,19 +5,24 @@ require 'test_helper'
5
5
  FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
6
6
  # Add factories from foreman_ansible
7
7
  FactoryBot.definition_file_paths << File.join(ForemanAnsible::Engine.root, '/test/factories')
8
+ FactoryBot.definition_file_paths << File.join(ForemanPuppet::Engine.root, '/test/factories') if defined?(ForemanPuppet)
8
9
  FactoryBot.reload
9
10
 
10
11
  require "#{ForemanOpenscap::Engine.root}/test/fixtures/cve_fixtures"
11
12
 
12
13
  module ScapClientPuppetclass
14
+ def puppet_available?
15
+ defined?(ForemanPuppet)
16
+ end
17
+
13
18
  def setup_puppet_class
14
19
  puppet_config = ::ForemanOpenscap::ClientConfig::Puppet.new
15
- Puppetclass.find_by(:name => puppet_config.puppetclass_name)&.destroy
20
+ ForemanPuppet::Puppetclass.find_by(:name => puppet_config.puppetclass_name)&.destroy
16
21
 
17
22
  puppet_class = FactoryBot.create(:puppetclass, :name => puppet_config.puppetclass_name)
18
- server_param = FactoryBot.create(:puppetclass_lookup_key, :key => puppet_config.server_param, :default_value => nil)
19
- port_param = FactoryBot.create(:puppetclass_lookup_key, :key => puppet_config.port_param, :default_value => nil)
20
- policies_param = FactoryBot.create(:puppetclass_lookup_key, :key => puppet_config.policies_param, :default_value => nil)
23
+ server_param = FactoryBot.create(:puppetclass_lookup_key, :key => puppet_config.server_param, :default_value => nil, :override => false)
24
+ port_param = FactoryBot.create(:puppetclass_lookup_key, :key => puppet_config.port_param, :default_value => nil, :override => false)
25
+ policies_param = FactoryBot.create(:puppetclass_lookup_key, :key => puppet_config.policies_param, :default_value => nil, :override => false)
21
26
 
22
27
  env = FactoryBot.create :environment
23
28
 
@@ -75,7 +75,6 @@ class PolicyTest < ActiveSupport::TestCase
75
75
  asset = FactoryBot.create(:asset, :assetable_id => hg.id, :assetable_type => 'Hostgroup')
76
76
  policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
77
77
  policy.save!
78
- hg.hostgroup_classes.destroy_all
79
78
  hg.destroy
80
79
  assert_equal 0, policy.hostgroups.count
81
80
  end
@@ -314,6 +313,7 @@ class PolicyTest < ActiveSupport::TestCase
314
313
  end
315
314
 
316
315
  test "should change deploy type" do
316
+ skip unless puppet_available?
317
317
  policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
318
318
  setup_puppet_class
319
319
  policy.change_deploy_type({ :deploy_by => 'puppet' })
@@ -24,6 +24,7 @@ class ConfigNameServiceTest < ActiveSupport::TestCase
24
24
  end
25
25
 
26
26
  test 'should find all available except Manual' do
27
+ skip unless puppet_available?
27
28
  ForemanOpenscap::ClientConfig::Ansible.any_instance.stubs(:available?).returns(false)
28
29
  configs = @name_service.all_available_except(:manual)
29
30
  assert_equal 1, configs.size
@@ -9,11 +9,12 @@ class HostgroupOverriderTest < ActiveSupport::TestCase
9
9
  end
10
10
 
11
11
  test 'should populate puppet overrides' do
12
+ skip unless puppet_available?
12
13
  puppet_class, env, port_param, server_param = setup_puppet_class.values_at :puppet_class, :env, :port_param, :server_param
13
14
 
14
15
  proxy = FactoryBot.create(:openscap_proxy, :url => 'https://override-keys.example.com:8998')
15
16
 
16
- hostgroup = FactoryBot.create(:hostgroup, :environment_id => env.id, :openscap_proxy_id => proxy.id)
17
+ hostgroup = FactoryBot.create(:hostgroup, :environment => env, :openscap_proxy_id => proxy.id, :puppet => FactoryBot.create(:hostgroup_puppet_facet))
17
18
  refute hostgroup.puppetclasses.include? puppet_class
18
19
  assert LookupValue.where(:match => "hostgroup=#{hostgroup.to_label}",
19
20
  :lookup_key_id => port_param.id,
@@ -8,6 +8,7 @@ class LookupKeyOverriderTest < ActiveSupport::TestCase
8
8
  end
9
9
 
10
10
  test 'should override puppet class parameters' do
11
+ skip unless puppet_available?
11
12
  server_param, port_param, policies_param = setup_puppet_class.values_at :server_param, :port_param, :policies_param
12
13
  refute server_param.override
13
14
  refute port_param.override
@@ -21,7 +22,8 @@ class LookupKeyOverriderTest < ActiveSupport::TestCase
21
22
  end
22
23
 
23
24
  test 'should add error when no puppet class found' do
24
- puppet_class = Puppetclass.find_by :name => ForemanOpenscap::ClientConfig::Puppet.new.puppetclass_name
25
+ skip unless puppet_available?
26
+ puppet_class = ::ForemanPuppet::Puppetclass.find_by :name => ForemanOpenscap::ClientConfig::Puppet.new.puppetclass_name
25
27
  puppet_class.destroy if puppet_class
26
28
  policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_content_profile, :deploy_by => :puppet)
27
29
  ForemanOpenscap::LookupKeyOverrider.new(policy).override
@@ -41,6 +43,7 @@ class LookupKeyOverriderTest < ActiveSupport::TestCase
41
43
  end
42
44
 
43
45
  test 'should add error when lookup keys not present' do
46
+ skip unless puppet_available?
44
47
  server_param, port_param, policies_param = setup_puppet_class.values_at :server_param, :port_param, :policies_param
45
48
  server_param.destroy
46
49
  port_param.destroy
@@ -0,0 +1,37 @@
1
+ require 'test_plugin_helper'
2
+
3
+ class ForemanOpenscap::Oval::SetupCheckTest < ActiveSupport::TestCase
4
+ test 'should show error message with filled in data' do
5
+ check = ::ForemanOpenscap::Oval::SetupCheck.new(
6
+ :id => :test_check,
7
+ :title => _("Will it pass?"),
8
+ :fail_msg => ->(hash) { "There was an error in #{hash[:name]}, you need to #{hash[:action]}" }
9
+ )
10
+
11
+ check.fail_with!(:name => 'your engine', :action => 'run')
12
+ assert_equal 'There was an error in your engine, you need to run', check.fail_msg
13
+ end
14
+
15
+ test 'should show error message when it is a string' do
16
+ msg = "Do not panic"
17
+ check = ::ForemanOpenscap::Oval::SetupCheck.new(
18
+ :id => :test_check,
19
+ :title => _("Will it pass?"),
20
+ :fail_msg => msg
21
+ )
22
+ check.fail!
23
+ assert_equal msg, check.fail_msg
24
+ end
25
+
26
+ test 'should not show error message when check not failed' do
27
+ check = ::ForemanOpenscap::Oval::SetupCheck.new(
28
+ :id => :test_check,
29
+ :title => _("Will it pass?"),
30
+ :fail_msg => 'foo'
31
+ )
32
+
33
+ assert_nil check.fail_msg
34
+ check.fail!
35
+ assert_not_nil check.fail_msg
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Modal, Button, ModalVariant, Spinner } from '@patternfly/react-core';
4
+
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import './ConfirmModal.scss';
8
+
9
+ const ConfirmModal = props => {
10
+ const [callMutation, { loading }] = props.prepareMutation();
11
+
12
+ const actions = [
13
+ <Button
14
+ key="confirm"
15
+ variant="primary"
16
+ onClick={() => props.onConfirm(callMutation, props.record.id)}
17
+ isDisabled={loading}
18
+ >
19
+ {__('Confirm')}
20
+ </Button>,
21
+ <Button
22
+ key="cancel"
23
+ variant="link"
24
+ onClick={event => props.onClose()}
25
+ isDisabled={loading}
26
+ >
27
+ {__('Cancel')}
28
+ </Button>,
29
+ ];
30
+
31
+ if (loading) {
32
+ actions.push(<Spinner key="spinner" size="lg" />);
33
+ }
34
+
35
+ return (
36
+ <Modal
37
+ variant={ModalVariant.medium}
38
+ title={props.title}
39
+ isOpen={props.isOpen}
40
+ className="foreman-modal"
41
+ showClose={false}
42
+ actions={actions}
43
+ >
44
+ {props.text}
45
+ </Modal>
46
+ );
47
+ };
48
+
49
+ ConfirmModal.propTypes = {
50
+ prepareMutation: PropTypes.func.isRequired,
51
+ onConfirm: PropTypes.func.isRequired,
52
+ record: PropTypes.object,
53
+ onClose: PropTypes.func.isRequired,
54
+ title: PropTypes.string.isRequired,
55
+ isOpen: PropTypes.bool.isRequired,
56
+ text: PropTypes.string.isRequired,
57
+ };
58
+
59
+ ConfirmModal.defaultProps = {
60
+ record: null,
61
+ };
62
+
63
+ export default ConfirmModal;
@@ -0,0 +1,3 @@
1
+ .pf-c-backdrop {
2
+ z-index: 1040;
3
+ }
@@ -0,0 +1,157 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import {
5
+ Button,
6
+ Split,
7
+ SplitItem,
8
+ Spinner,
9
+ FormGroup,
10
+ } from '@patternfly/react-core';
11
+ import {
12
+ TimesIcon,
13
+ CheckIcon,
14
+ PencilAltIcon,
15
+ ExclamationCircleIcon,
16
+ } from '@patternfly/react-icons';
17
+
18
+ import './EditableInput.scss';
19
+
20
+ const EditableInput = props => {
21
+ const [editing, setEditing] = useState(false);
22
+ const [submitting, setSubmitting] = useState(false);
23
+ const [inputValue, setInputValue] = useState(props.value);
24
+ const [error, setError] = useState('');
25
+ const [touched, setTouched] = useState(false);
26
+
27
+ const stopSubmitting = () => setSubmitting(false);
28
+
29
+ const handleSubmit = event => {
30
+ event.preventDefault();
31
+ onSubmit();
32
+ };
33
+
34
+ const onFinish = () => {
35
+ setSubmitting(false);
36
+ setEditing(false);
37
+ };
38
+
39
+ const onSubmit = () => {
40
+ setSubmitting(true);
41
+ props.onConfirm(inputValue, onFinish, stopSubmitting, onError);
42
+ };
43
+
44
+ const onError = err => {
45
+ setTouched(false);
46
+ setError(err);
47
+ };
48
+
49
+ const onCancel = () => {
50
+ setInputValue(props.value);
51
+ setEditing(false);
52
+ };
53
+
54
+ const onChange = value => {
55
+ if (!touched) {
56
+ setTouched(true);
57
+ }
58
+ setInputValue(value);
59
+ };
60
+
61
+ if (!editing) {
62
+ return (
63
+ <Split>
64
+ <SplitItem>{props.value || <i>{__('None provided')}</i>}</SplitItem>
65
+ <SplitItem>
66
+ <Button
67
+ className="inline-edit-icon"
68
+ aria-label={`edit ${props.attrName}`}
69
+ variant="plain"
70
+ onClick={() => setEditing(true)}
71
+ >
72
+ <PencilAltIcon />
73
+ </Button>
74
+ </SplitItem>
75
+ </Split>
76
+ );
77
+ }
78
+
79
+ const Component = props.component;
80
+
81
+ const shouldValidate = (isTouched, err) => {
82
+ if (!isTouched) {
83
+ return err ? 'error' : 'success';
84
+ }
85
+ return 'noval';
86
+ };
87
+
88
+ const valid = shouldValidate(touched, error);
89
+
90
+ return (
91
+ <Split>
92
+ <SplitItem>
93
+ <form onSubmit={handleSubmit} className="pf-c-form">
94
+ <FormGroup
95
+ helperTextInvalid={error}
96
+ helperTextInvalidIcon={<ExclamationCircleIcon />}
97
+ validated={valid}
98
+ >
99
+ <Component
100
+ {...props.inputProps}
101
+ type="text"
102
+ aria-label={`${props.attrName} text input`}
103
+ isDisabled={submitting}
104
+ value={inputValue || ''}
105
+ onChange={onChange}
106
+ validated={valid}
107
+ />
108
+ </FormGroup>
109
+ </form>
110
+ </SplitItem>
111
+ <SplitItem>
112
+ <Button
113
+ aria-label={`submit ${props.attrName}`}
114
+ variant="plain"
115
+ onClick={onSubmit}
116
+ isDisabled={submitting}
117
+ >
118
+ <CheckIcon />
119
+ </Button>
120
+ </SplitItem>
121
+ <SplitItem>
122
+ <Button
123
+ aria-label={`cancel editing ${props.attrName}`}
124
+ variant="plain"
125
+ onClick={onCancel}
126
+ isDisabled={submitting}
127
+ >
128
+ <TimesIcon />
129
+ </Button>
130
+ </SplitItem>
131
+ <SplitItem>
132
+ {submitting && (
133
+ <Spinner
134
+ key="spinner"
135
+ size="lg"
136
+ id={`edit-${props.attrName}-spinner`}
137
+ />
138
+ )}
139
+ </SplitItem>
140
+ </Split>
141
+ );
142
+ };
143
+
144
+ EditableInput.propTypes = {
145
+ value: PropTypes.string,
146
+ onConfirm: PropTypes.func.isRequired,
147
+ attrName: PropTypes.string.isRequired,
148
+ component: PropTypes.object.isRequired,
149
+ inputProps: PropTypes.object,
150
+ };
151
+
152
+ EditableInput.defaultProps = {
153
+ inputProps: {},
154
+ value: '',
155
+ };
156
+
157
+ export default EditableInput;
@@ -0,0 +1,3 @@
1
+ .inline-edit-icon {
2
+ padding-top: 2px;
3
+ }
@@ -12,10 +12,11 @@ import {
12
12
  CubeIcon,
13
13
  ExclamationCircleIcon,
14
14
  SearchIcon,
15
+ LockIcon,
15
16
  } from '@patternfly/react-icons';
16
17
  import { global_danger_color_200 as dangerColor } from '@patternfly/react-tokens';
17
18
 
18
- const EmptyStateIcon = ({ error, search }) => {
19
+ const EmptyStateIcon = ({ error, search, lock }) => {
19
20
  if (error)
20
21
  return (
21
22
  <PfEmptyStateIcon
@@ -23,18 +24,20 @@ const EmptyStateIcon = ({ error, search }) => {
23
24
  color={dangerColor.value}
24
25
  />
25
26
  );
27
+ if (lock) return <PfEmptyStateIcon icon={LockIcon} />;
26
28
  if (search) return <PfEmptyStateIcon icon={SearchIcon} />;
27
29
  return <PfEmptyStateIcon icon={CubeIcon} />;
28
30
  };
29
31
 
30
- const EmptyState = ({ title, body, error, search }) => (
32
+ const EmptyState = ({ title, body, error, search, lock, primaryButton }) => (
31
33
  <Bullseye>
32
34
  <PfEmptyState variant={EmptyStateVariant.small}>
33
- <EmptyStateIcon error={!!error} search={search} />
35
+ <EmptyStateIcon error={!!error} search={search} lock={lock} />
34
36
  <Title headingLevel="h2" size="lg">
35
37
  {title}
36
38
  </Title>
37
39
  <EmptyStateBody>{body}</EmptyStateBody>
40
+ {primaryButton}
38
41
  </PfEmptyState>
39
42
  </Bullseye>
40
43
  );
@@ -42,11 +45,13 @@ const EmptyState = ({ title, body, error, search }) => (
42
45
  EmptyStateIcon.propTypes = {
43
46
  error: PropTypes.bool,
44
47
  search: PropTypes.bool,
48
+ lock: PropTypes.bool,
45
49
  };
46
50
 
47
51
  EmptyStateIcon.defaultProps = {
48
52
  error: false,
49
53
  search: false,
54
+ lock: false,
50
55
  };
51
56
 
52
57
  EmptyState.propTypes = {
@@ -54,6 +59,8 @@ EmptyState.propTypes = {
54
59
  body: PropTypes.string,
55
60
  error: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.string]),
56
61
  search: PropTypes.bool,
62
+ lock: PropTypes.bool,
63
+ primaryButton: PropTypes.node,
57
64
  };
58
65
 
59
66
  EmptyState.defaultProps = {
@@ -62,6 +69,8 @@ EmptyState.defaultProps = {
62
69
  'There was an error retrieving data from the server. Check your connection and try again.',
63
70
  error: undefined,
64
71
  search: false,
72
+ lock: false,
73
+ primaryButton: null,
65
74
  };
66
75
 
67
76
  export default EmptyState;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Helmet } from 'react-helmet';
4
+ import ToastsList from 'foremanReact/components/ToastsList';
4
5
  import {
5
6
  Grid,
6
7
  GridItem,
@@ -11,25 +12,31 @@ import {
11
12
 
12
13
  import './IndexLayout.scss';
13
14
 
14
- const IndexLayout = ({ pageTitle, children }) => (
15
+ const IndexLayout = ({ pageTitle, children, contentWidthSpan }) => (
15
16
  <React.Fragment>
16
17
  <Helmet>
17
18
  <title>{pageTitle}</title>
18
19
  </Helmet>
20
+ <ToastsList />
19
21
  <Grid className="scap-page-grid">
20
- <GridItem span={12}>
22
+ <GridItem span={12} className="pf-u-pb-xl">
21
23
  <TextContent>
22
24
  <Text component={TextVariants.h1}>{pageTitle}</Text>
23
25
  </TextContent>
24
26
  </GridItem>
25
- <GridItem span={12}>{children}</GridItem>
27
+ <GridItem span={contentWidthSpan}>{children}</GridItem>
26
28
  </Grid>
27
29
  </React.Fragment>
28
30
  );
29
31
 
30
32
  IndexLayout.propTypes = {
31
33
  pageTitle: PropTypes.string.isRequired,
32
- children: PropTypes.object.isRequired,
34
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]).isRequired,
35
+ contentWidthSpan: PropTypes.number,
36
+ };
37
+
38
+ IndexLayout.defaultProps = {
39
+ contentWidthSpan: 12,
33
40
  };
34
41
 
35
42
  export default IndexLayout;
@@ -2,18 +2,25 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Table, TableHeader, TableBody } from '@patternfly/react-table';
4
4
  import { Pagination, Flex, FlexItem } from '@patternfly/react-core';
5
- import '@patternfly/patternfly/patternfly-addons.scss';
6
5
  import { usePaginationOptions } from 'foremanReact/components/Pagination/PaginationHooks';
7
6
 
8
7
  import { preparePerPageOptions, refreshPage } from './IndexTableHelper';
9
8
 
10
- const IndexTable = props => {
9
+ const IndexTable = ({
10
+ history,
11
+ pagination,
12
+ totalCount,
13
+ toolbarBtns,
14
+ ariaTableLabel,
15
+ columns,
16
+ ...rest
17
+ }) => {
11
18
  const handlePerPageSelected = (event, perPage) => {
12
- refreshPage(props.history, { page: 1, perPage });
19
+ refreshPage(history, { page: 1, perPage });
13
20
  };
14
21
 
15
22
  const handlePageSelected = (event, page) => {
16
- refreshPage(props.history, { ...props.pagination, page });
23
+ refreshPage(history, { ...pagination, page });
17
24
  };
18
25
 
19
26
  const perPageOptions = preparePerPageOptions(usePaginationOptions());
@@ -21,12 +28,12 @@ const IndexTable = props => {
21
28
  return (
22
29
  <React.Fragment>
23
30
  <Flex className="pf-u-pt-md">
24
- <FlexItem>{props.toolbarBtns}</FlexItem>
31
+ <FlexItem>{toolbarBtns}</FlexItem>
25
32
  <FlexItem align={{ default: 'alignRight' }}>
26
33
  <Pagination
27
- itemCount={props.totalCount}
28
- page={props.pagination.page}
29
- perPage={props.pagination.perPage}
34
+ itemCount={totalCount}
35
+ page={pagination.page}
36
+ perPage={pagination.perPage}
30
37
  onSetPage={handlePageSelected}
31
38
  onPerPageSelect={handlePerPageSelected}
32
39
  perPageOptions={perPageOptions}
@@ -34,12 +41,7 @@ const IndexTable = props => {
34
41
  />
35
42
  </FlexItem>
36
43
  </Flex>
37
- <Table
38
- aria-label={props.ariaTableLabel}
39
- cells={props.columns}
40
- rows={props.rows}
41
- actions={props.actions}
42
- >
44
+ <Table aria-label={ariaTableLabel} cells={columns} {...rest}>
43
45
  <TableHeader />
44
46
  <TableBody />
45
47
  </Table>
@@ -50,17 +52,14 @@ const IndexTable = props => {
50
52
  IndexTable.propTypes = {
51
53
  history: PropTypes.object.isRequired,
52
54
  pagination: PropTypes.object.isRequired,
53
- toolbarBtns: PropTypes.array,
55
+ toolbarBtns: PropTypes.node,
54
56
  totalCount: PropTypes.number.isRequired,
55
57
  ariaTableLabel: PropTypes.string.isRequired,
56
58
  columns: PropTypes.array.isRequired,
57
- rows: PropTypes.array.isRequired,
58
- actions: PropTypes.array,
59
59
  };
60
60
 
61
61
  IndexTable.defaultProps = {
62
62
  toolbarBtns: [],
63
- actions: [],
64
63
  };
65
64
 
66
65
  export default IndexTable;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { Button } from '@patternfly/react-core';
4
+ import PropTypes from 'prop-types';
5
+
6
+ const LinkButton = ({ path, btnVariant, btnText, isDisabled }) => (
7
+ <Link to={path}>
8
+ <Button variant={btnVariant} isDisabled={isDisabled}>
9
+ {btnText}
10
+ </Button>
11
+ </Link>
12
+ );
13
+
14
+ LinkButton.propTypes = {
15
+ path: PropTypes.string.isRequired,
16
+ btnText: PropTypes.string.isRequired,
17
+ btnVariant: PropTypes.string,
18
+ isDisabled: PropTypes.bool,
19
+ };
20
+
21
+ LinkButton.defaultProps = {
22
+ btnVariant: 'primary',
23
+ isDisabled: false,
24
+ };
25
+
26
+ export default LinkButton;
@@ -0,0 +1,51 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
4
+ import ConfirmModal from './ConfirmModal';
5
+
6
+ const withDelete = Component => {
7
+ const Subcomponent = ({
8
+ confirmDeleteTitle,
9
+ submitDelete,
10
+ prepareMutation,
11
+ ...rest
12
+ }) => {
13
+ const [toDelete, setToDelete] = useState(null);
14
+
15
+ const toggleModal = (item = null) => {
16
+ setToDelete(item);
17
+ };
18
+
19
+ return (
20
+ <React.Fragment>
21
+ <Component {...rest} toggleModal={toggleModal} />
22
+ <ConfirmModal
23
+ title={confirmDeleteTitle}
24
+ text={
25
+ toDelete
26
+ ? sprintf(
27
+ __('Are you sure you want to delete %s?'),
28
+ toDelete.name
29
+ )
30
+ : ''
31
+ }
32
+ onClose={toggleModal}
33
+ isOpen={!!toDelete}
34
+ onConfirm={submitDelete}
35
+ prepareMutation={() => prepareMutation(toggleModal)}
36
+ record={toDelete}
37
+ />
38
+ </React.Fragment>
39
+ );
40
+ };
41
+
42
+ Subcomponent.propTypes = {
43
+ confirmDeleteTitle: PropTypes.string.isRequired,
44
+ submitDelete: PropTypes.func.isRequired,
45
+ prepareMutation: PropTypes.func.isRequired,
46
+ };
47
+
48
+ return Subcomponent;
49
+ };
50
+
51
+ export default withDelete;