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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/compliance/arf_reports_controller.rb +0 -6
- data/app/controllers/api/v2/compliance/oval_policies_controller.rb +1 -1
- data/app/graphql/mutations/oval_contents/delete.rb +9 -0
- data/app/graphql/mutations/oval_policies/delete.rb +9 -0
- data/app/graphql/mutations/oval_policies/update.rb +15 -0
- data/app/graphql/types/oval_check.rb +11 -0
- data/app/graphql/types/oval_content.rb +2 -0
- data/app/graphql/types/oval_policy.rb +3 -0
- data/app/helpers/arf_report_dashboard_helper.rb +2 -4
- data/app/helpers/compliance_hosts_helper.rb +1 -1
- data/app/helpers/policies_helper.rb +1 -1
- data/app/models/concerns/foreman_openscap/host_extensions.rb +0 -6
- data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +15 -0
- data/app/models/foreman_openscap/oval_content.rb +2 -0
- data/app/services/foreman_openscap/client_config/base.rb +1 -0
- data/app/services/foreman_openscap/client_config/puppet.rb +6 -2
- data/app/services/foreman_openscap/oval/configure.rb +1 -1
- data/app/services/foreman_openscap/oval/setup.rb +5 -5
- data/app/services/foreman_openscap/oval/setup_check.rb +5 -2
- data/app/views/api/v2/compliance/oval_contents/destroy.json.rabl +3 -0
- data/app/views/arf_reports/_metrics.html.erb +4 -4
- data/app/views/compliance_hosts/show.html.erb +4 -6
- data/app/views/dashboard/_compliance_reports_breakdown_widget.html.erb +4 -3
- data/app/views/policy_dashboard/_policy_chart_widget.html.erb +3 -2
- data/db/migrate/20200117135424_migrate_port_overrides_to_int.rb +2 -1
- data/db/migrate/20201202110213_update_puppet_port_param_type.rb +2 -1
- data/db/migrate/20210819143316_drop_unused_tables.rb +6 -0
- data/lib/foreman_openscap/engine.rb +5 -7
- data/lib/foreman_openscap/version.rb +1 -1
- data/package.json +3 -6
- data/test/functional/api/v2/compliance/oval_reports_controller_test.rb +1 -1
- data/test/functional/api/v2/compliance/policies_controller_test.rb +2 -0
- data/test/graphql/mutations/oval_policies/delete_mutation_test.rb +63 -0
- data/test/graphql/queries/oval_content_query_test.rb +29 -0
- data/test/helpers/arf_report_dashboard_helper_test.rb +9 -10
- data/test/helpers/policy_dashboard_helper_test.rb +1 -1
- data/test/test_plugin_helper.rb +9 -4
- data/test/unit/policy_test.rb +1 -1
- data/test/unit/services/config_name_service_test.rb +1 -0
- data/test/unit/services/hostgroup_overrider_test.rb +2 -1
- data/test/unit/services/lookup_key_overrider_test.rb +4 -1
- data/test/unit/services/oval/setup_check_test.rb +37 -0
- data/webpack/components/ConfirmModal.js +63 -0
- data/webpack/components/ConfirmModal.scss +3 -0
- data/webpack/components/EditableInput.js +157 -0
- data/webpack/components/EditableInput.scss +3 -0
- data/webpack/components/EmptyState.js +12 -3
- data/webpack/components/IndexLayout.js +11 -4
- data/webpack/components/IndexTable/index.js +17 -18
- data/webpack/components/LinkButton.js +26 -0
- data/webpack/components/withDeleteModal.js +51 -0
- data/webpack/components/withLoading.js +41 -4
- data/webpack/graphql/mutations/deleteOvalContent.gql +9 -0
- data/webpack/graphql/mutations/deleteOvalPolicy.gql +9 -0
- data/webpack/graphql/mutations/updateOvalPolicy.gql +14 -0
- data/webpack/graphql/queries/currentUserAttributes.gql +11 -0
- data/webpack/graphql/queries/cves.gql +5 -0
- data/webpack/graphql/queries/hostgroups.gql +14 -0
- data/webpack/graphql/queries/ovalContent.gql +8 -0
- data/webpack/graphql/queries/ovalContents.gql +8 -0
- data/webpack/graphql/queries/ovalPolicies.gql +8 -0
- data/webpack/graphql/queries/ovalPolicy.gql +5 -0
- data/webpack/helpers/formFieldsHelper.js +63 -0
- data/webpack/helpers/mutationHelper.js +68 -0
- data/webpack/helpers/pathsHelper.js +5 -0
- data/webpack/helpers/permissionsHelper.js +42 -0
- data/webpack/helpers/toastHelper.js +3 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +26 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +50 -5
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.fixtures.js +105 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +124 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +93 -77
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +53 -6
- data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -1
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js +138 -0
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss +3 -0
- data/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js +73 -0
- data/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js +104 -0
- data/webpack/routes/OvalContents/OvalContentsNew/index.js +13 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.js +62 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShow.test.js +45 -0
- data/webpack/routes/OvalContents/OvalContentsShow/OvalContentsShowHelper.js +0 -0
- data/webpack/routes/OvalContents/OvalContentsShow/index.js +35 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +18 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +16 -3
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.fixtures.js +101 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesDestroy.test.js +117 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +71 -21
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +34 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -1
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +1 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +85 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTab.js +49 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/HostgroupsTable.js +38 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +15 -12
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +77 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.fixtures.js +48 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +175 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +40 -4
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +64 -4
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +4 -0
- data/webpack/routes/routes.js +14 -0
- data/webpack/testHelper.js +42 -2
- 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 =
|
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
|
-
|
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 =
|
26
|
-
assert_equal "Number of Events", res[
|
27
|
-
assert_equal "Rule Results", res[
|
28
|
-
assert_equal 3, res[
|
29
|
-
assert_include res[
|
30
|
-
assert_include res[
|
31
|
-
assert_include res[
|
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 =
|
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]]
|
data/test/test_plugin_helper.rb
CHANGED
@@ -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
|
|
data/test/unit/policy_test.rb
CHANGED
@@ -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, :
|
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
|
-
|
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,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;
|
@@ -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={
|
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 =
|
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(
|
19
|
+
refreshPage(history, { page: 1, perPage });
|
13
20
|
};
|
14
21
|
|
15
22
|
const handlePageSelected = (event, page) => {
|
16
|
-
refreshPage(
|
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>{
|
31
|
+
<FlexItem>{toolbarBtns}</FlexItem>
|
25
32
|
<FlexItem align={{ default: 'alignRight' }}>
|
26
33
|
<Pagination
|
27
|
-
itemCount={
|
28
|
-
page={
|
29
|
-
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.
|
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;
|