foreman_openscap 5.1.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/graphql/mutations/oval_policies/create.rb +33 -0
- data/app/helpers/policies_helper.rb +1 -1
- data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +1 -0
- data/app/models/concerns/foreman_openscap/policy_common.rb +1 -1
- data/app/services/foreman_openscap/oval/configure.rb +16 -13
- data/app/services/foreman_openscap/oval/setup_check.rb +1 -1
- data/lib/foreman_openscap/engine.rb +1 -0
- data/lib/foreman_openscap/version.rb +1 -1
- data/webpack/components/EditableInput.js +16 -10
- data/webpack/components/IndexTable/index.js +7 -2
- data/webpack/components/LinkButton.js +14 -2
- data/webpack/components/withLoading.js +3 -1
- data/webpack/graphql/mutations/createOvalPolicy.gql +22 -0
- data/webpack/graphql/queries/ovalPolicy.gql +3 -0
- data/webpack/helpers/formFieldsHelper.js +51 -1
- data/webpack/helpers/globalIdHelper.js +4 -2
- data/webpack/helpers/pathsHelper.js +5 -3
- data/webpack/helpers/toastsHelper.js +3 -0
- data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +6 -1
- data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +18 -1
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/HostgroupSelect.js +135 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyForm.js +119 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyFormHelpers.js +107 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/OvalPoliciesNew.js +32 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.fixtures.js +147 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.test.js +172 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesNew/index.js +11 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +2 -2
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +2 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +4 -3
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +27 -0
- data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +11 -1
- data/webpack/routes/routes.js +7 -0
- data/webpack/testHelper.js +22 -0
- metadata +39 -42
- data/locale/de/foreman_openscap.edit.po +0 -0
- data/locale/en_GB/foreman_openscap.edit.po +0 -0
- data/locale/es/foreman_openscap.edit.po +0 -0
- data/locale/fr/foreman_openscap.edit.po +0 -0
- data/locale/gl/foreman_openscap.edit.po +0 -0
- data/locale/it/foreman_openscap.edit.po +0 -0
- data/locale/ja/foreman_openscap.edit.po +0 -0
- data/locale/ko/foreman_openscap.edit.po +0 -0
- data/locale/pt_BR/foreman_openscap.edit.po +0 -0
- data/locale/ru/foreman_openscap.edit.po +0 -0
- data/locale/sv_SE/foreman_openscap.edit.po +0 -0
- data/locale/zh_CN/foreman_openscap.edit.po +0 -0
- data/locale/zh_TW/foreman_openscap.edit.po +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cadd1a7264151b9ab0bd0cc4d21eb212f621094c9f62c68a5922c2c0fe7d20f
|
4
|
+
data.tar.gz: 87a58a9b949841cd923a9d91d7fc18bc2717194a6cd1a8c96f90f89cebf925ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f22820ffa670981fb8a2495ebfe19e9f67633e6549341972ac8caf23c5a826da6429ea2672f8a5d5b42b7a769662c87a068b3b7bbdda7d8d84f4d56043a1c37
|
7
|
+
data.tar.gz: 2134035b26dc747b698c69173d54eaeac67c17d81474997269872a6c62b0c8fe9cfe6a0c3c0734f13e5270b4e84b9f6da95f889b017dc366c1a276679e048d74
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mutations
|
2
|
+
module OvalPolicies
|
3
|
+
class Create < ::Mutations::BaseMutation
|
4
|
+
description 'Creates a new OVAL Policy'
|
5
|
+
graphql_name 'CreateOvalPolicyMutation'
|
6
|
+
|
7
|
+
resource_class ::ForemanOpenscap::OvalPolicy
|
8
|
+
|
9
|
+
argument :name, String
|
10
|
+
argument :description, String, required: false
|
11
|
+
argument :period, String
|
12
|
+
argument :weekday, String, required: false
|
13
|
+
argument :day_of_month, Integer, required: false
|
14
|
+
argument :cron_line, String, required: false
|
15
|
+
argument :oval_content_id, Integer, required: true
|
16
|
+
argument :hostgroup_ids, [Integer], required: false
|
17
|
+
|
18
|
+
field :oval_policy, Types::OvalPolicy, 'The new OVAL Policy.', null: true
|
19
|
+
field :check_collection, [Types::OvalCheck], 'A collection of checks to detect OVAL policy configuration error', null: false
|
20
|
+
|
21
|
+
def resolve(hostgroup_ids:, **params)
|
22
|
+
policy = ::ForemanOpenscap::OvalPolicy.new params
|
23
|
+
validate_object(policy)
|
24
|
+
authorize!(policy, :create)
|
25
|
+
check_collection = ::ForemanOpenscap::Oval::Configure.new.assign(policy, hostgroup_ids, ::Hostgroup)
|
26
|
+
{
|
27
|
+
:oval_policy => policy,
|
28
|
+
:check_collection => check_collection.checks
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -93,7 +93,7 @@ module PoliciesHelper
|
|
93
93
|
def tailoring_file_profile_selector(form, tailoring_file)
|
94
94
|
if tailoring_file
|
95
95
|
select_f form, :tailoring_file_profile_id, tailoring_file.scap_content_profiles, :id, :title,
|
96
|
-
{ :selected =>
|
96
|
+
{ :selected => @policy.tailoring_file_profile_id },
|
97
97
|
{ :label => _("XCCDF Profile in Tailoring File"),
|
98
98
|
:help_inline => _("This profile will be used to override the one from scap content") }
|
99
99
|
else
|
@@ -69,7 +69,7 @@ module ForemanOpenscap
|
|
69
69
|
|
70
70
|
def weekday_number
|
71
71
|
# 0 is sunday, 1 is monday in cron, while DAYS_INTO_WEEK has 0 as monday, 6 as sunday
|
72
|
-
(Date::DAYS_INTO_WEEK.with_indifferent_access[weekday]
|
72
|
+
(Date::DAYS_INTO_WEEK.with_indifferent_access[weekday]) % 7
|
73
73
|
end
|
74
74
|
end
|
75
75
|
end
|
@@ -16,21 +16,26 @@ module ForemanOpenscap
|
|
16
16
|
if model_class == ::Hostgroup
|
17
17
|
roles_method = :inherited_and_own_ansible_roles
|
18
18
|
ids_setter = :hostgroup_ids=
|
19
|
+
check_id = :hostgroups_without_proxy
|
19
20
|
elsif model_class == ::Host::Managed
|
20
21
|
roles_method = :all_ansible_roles
|
21
22
|
ids_setter = :host_ids=
|
23
|
+
check_id = :hosts_without_proxy
|
22
24
|
else
|
23
25
|
raise "Unexpected model_class, expected ::Hostgroup or ::Host::Managed, got: #{model_class}"
|
24
26
|
end
|
25
27
|
|
26
28
|
items_with_proxy, items_without_proxy = openscap_proxy_associated(ids, model_class)
|
27
29
|
|
28
|
-
oval_policy.send(ids_setter, items_with_proxy.pluck(:id))
|
29
30
|
|
30
|
-
|
31
|
+
if items_without_proxy.any?
|
32
|
+
return without_proxy_to_check items_without_proxy, check_id
|
33
|
+
end
|
34
|
+
|
35
|
+
oval_policy.send(ids_setter, items_with_proxy.pluck(:id))
|
31
36
|
|
32
37
|
unless oval_policy.save
|
33
|
-
return check_collection.add_check model_to_check(oval_policy)
|
38
|
+
return check_collection.add_check model_to_check(oval_policy, :oval_policy_errors)
|
34
39
|
end
|
35
40
|
|
36
41
|
check_collection.merge modify_items(items_with_proxy, oval_policy, ansible_role, roles_method)
|
@@ -47,31 +52,29 @@ module ForemanOpenscap
|
|
47
52
|
role_ids = item.ansible_role_ids + [ansible_role.id]
|
48
53
|
item.ansible_role_ids = role_ids unless item.send(roles_method).include? ansible_role
|
49
54
|
item.save if item.changed?
|
50
|
-
memo.add_check model_to_check(item)
|
55
|
+
memo.add_check model_to_check(item, item.is_a?(::Hostgroup) ? 'hostgroup' : 'host')
|
51
56
|
add_overrides ansible_role.ansible_variables, item, @config
|
52
57
|
memo
|
53
58
|
end
|
54
59
|
end
|
55
60
|
|
56
|
-
def without_proxy_to_check(items)
|
61
|
+
def without_proxy_to_check(items, check_id)
|
57
62
|
items.reduce(CheckCollection.new) do |memo, item|
|
58
63
|
memo.add_check(
|
59
64
|
SetupCheck.new(
|
60
65
|
:title => (_("Was %s configured successfully?") % item.class.name),
|
61
|
-
:fail_msg => (_("Assign openscap_proxy to %s before proceeding.") % item.name)
|
66
|
+
:fail_msg => (_("Assign openscap_proxy to %s before proceeding.") % item.name),
|
67
|
+
:id => check_id
|
62
68
|
).fail!
|
63
69
|
)
|
64
70
|
end
|
65
71
|
end
|
66
72
|
|
67
|
-
def
|
68
|
-
model.is_a?(::Hostgroup) ? 'hostgroup' : 'host'
|
69
|
-
end
|
70
|
-
|
71
|
-
def model_to_check(model)
|
73
|
+
def model_to_check(model, check_id)
|
72
74
|
check = SetupCheck.new(
|
73
|
-
:title => (_("Was %{model_name} %{name} configured successfully?") % { :model_name =>
|
74
|
-
:errors => model.errors.to_h
|
75
|
+
:title => (_("Was %{model_name} %{name} configured successfully?") % { :model_name => model.class.name, :name => model.name }),
|
76
|
+
:errors => model.errors.to_h,
|
77
|
+
:id => check_id
|
75
78
|
)
|
76
79
|
model.errors.any? ? check.fail! : check.pass!
|
77
80
|
end
|
@@ -227,6 +227,7 @@ module ForemanOpenscap
|
|
227
227
|
register_graphql_mutation_field :delete_oval_policy, ::Mutations::OvalPolicies::Delete
|
228
228
|
register_graphql_mutation_field :delete_oval_content, ::Mutations::OvalContents::Delete
|
229
229
|
register_graphql_mutation_field :update_oval_policy, ::Mutations::OvalPolicies::Update
|
230
|
+
register_graphql_mutation_field :create_oval_policy, ::Mutations::OvalPolicies::Create
|
230
231
|
|
231
232
|
register_facet ForemanOpenscap::Host::OvalFacet, :oval_facet do
|
232
233
|
configure_host do
|
@@ -49,6 +49,7 @@ const EditableInput = props => {
|
|
49
49
|
const onCancel = () => {
|
50
50
|
setInputValue(props.value);
|
51
51
|
setEditing(false);
|
52
|
+
setError('');
|
52
53
|
};
|
53
54
|
|
54
55
|
const onChange = value => {
|
@@ -58,20 +59,24 @@ const EditableInput = props => {
|
|
58
59
|
setInputValue(value);
|
59
60
|
};
|
60
61
|
|
62
|
+
const editBtn = (
|
63
|
+
<SplitItem>
|
64
|
+
<Button
|
65
|
+
className="inline-edit-icon"
|
66
|
+
aria-label={`edit ${props.attrName}`}
|
67
|
+
variant="plain"
|
68
|
+
onClick={() => setEditing(true)}
|
69
|
+
>
|
70
|
+
<PencilAltIcon />
|
71
|
+
</Button>
|
72
|
+
</SplitItem>
|
73
|
+
);
|
74
|
+
|
61
75
|
if (!editing) {
|
62
76
|
return (
|
63
77
|
<Split>
|
64
78
|
<SplitItem>{props.value || <i>{__('None provided')}</i>}</SplitItem>
|
65
|
-
|
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>
|
79
|
+
{props.allowed && editBtn}
|
75
80
|
</Split>
|
76
81
|
);
|
77
82
|
}
|
@@ -142,6 +147,7 @@ const EditableInput = props => {
|
|
142
147
|
};
|
143
148
|
|
144
149
|
EditableInput.propTypes = {
|
150
|
+
allowed: PropTypes.bool.isRequired,
|
145
151
|
value: PropTypes.string,
|
146
152
|
onConfirm: PropTypes.func.isRequired,
|
147
153
|
attrName: PropTypes.string.isRequired,
|
@@ -41,7 +41,12 @@ const IndexTable = ({
|
|
41
41
|
/>
|
42
42
|
</FlexItem>
|
43
43
|
</Flex>
|
44
|
-
<Table
|
44
|
+
<Table
|
45
|
+
aria-label={ariaTableLabel}
|
46
|
+
cells={columns}
|
47
|
+
{...rest}
|
48
|
+
variant="compact"
|
49
|
+
>
|
45
50
|
<TableHeader />
|
46
51
|
<TableBody />
|
47
52
|
</Table>
|
@@ -59,7 +64,7 @@ IndexTable.propTypes = {
|
|
59
64
|
};
|
60
65
|
|
61
66
|
IndexTable.defaultProps = {
|
62
|
-
toolbarBtns:
|
67
|
+
toolbarBtns: null,
|
63
68
|
};
|
64
69
|
|
65
70
|
export default IndexTable;
|
@@ -3,9 +3,19 @@ import { Link } from 'react-router-dom';
|
|
3
3
|
import { Button } from '@patternfly/react-core';
|
4
4
|
import PropTypes from 'prop-types';
|
5
5
|
|
6
|
-
const LinkButton = ({
|
6
|
+
const LinkButton = ({
|
7
|
+
path,
|
8
|
+
btnVariant,
|
9
|
+
btnText,
|
10
|
+
isDisabled,
|
11
|
+
btnAriaLabel,
|
12
|
+
}) => (
|
7
13
|
<Link to={path}>
|
8
|
-
<Button
|
14
|
+
<Button
|
15
|
+
variant={btnVariant}
|
16
|
+
isDisabled={isDisabled}
|
17
|
+
aria-label={btnAriaLabel}
|
18
|
+
>
|
9
19
|
{btnText}
|
10
20
|
</Button>
|
11
21
|
</Link>
|
@@ -16,11 +26,13 @@ LinkButton.propTypes = {
|
|
16
26
|
btnText: PropTypes.string.isRequired,
|
17
27
|
btnVariant: PropTypes.string,
|
18
28
|
isDisabled: PropTypes.bool,
|
29
|
+
btnAriaLabel: PropTypes.string,
|
19
30
|
};
|
20
31
|
|
21
32
|
LinkButton.defaultProps = {
|
22
33
|
btnVariant: 'primary',
|
23
34
|
isDisabled: false,
|
35
|
+
btnAriaLabel: null,
|
24
36
|
};
|
25
37
|
|
26
38
|
export default LinkButton;
|
@@ -10,7 +10,6 @@ import {
|
|
10
10
|
import EmptyState from './EmptyState';
|
11
11
|
|
12
12
|
const errorStateTitle = __('Error!');
|
13
|
-
const emptyStateBody = '';
|
14
13
|
|
15
14
|
const pluckData = (data, path) => {
|
16
15
|
const split = path.split('.');
|
@@ -28,6 +27,7 @@ const withLoading = Component => {
|
|
28
27
|
resultPath,
|
29
28
|
renameData,
|
30
29
|
emptyStateTitle,
|
30
|
+
emptyStateBody,
|
31
31
|
permissions,
|
32
32
|
primaryButton,
|
33
33
|
shouldRefetch,
|
@@ -87,6 +87,7 @@ const withLoading = Component => {
|
|
87
87
|
resultPath: PropTypes.string.isRequired,
|
88
88
|
renameData: PropTypes.func,
|
89
89
|
emptyStateTitle: PropTypes.string.isRequired,
|
90
|
+
emptyStateBody: PropTypes.string,
|
90
91
|
permissions: PropTypes.array,
|
91
92
|
primaryButton: PropTypes.node,
|
92
93
|
shouldRefetch: PropTypes.bool,
|
@@ -97,6 +98,7 @@ const withLoading = Component => {
|
|
97
98
|
permissions: [],
|
98
99
|
primaryButton: null,
|
99
100
|
shouldRefetch: false,
|
101
|
+
emptyStateBody: '',
|
100
102
|
};
|
101
103
|
|
102
104
|
return Subcomponent;
|
@@ -0,0 +1,22 @@
|
|
1
|
+
mutation CreateOvalPolicy($name: String!, $period: String!, $cronLine: String, $ovalContentId: Int!, $hostgroupIds: [Int!]) {
|
2
|
+
createOvalPolicy(input: {name: $name, period: $period, cronLine: $cronLine, ovalContentId: $ovalContentId, hostgroupIds: $hostgroupIds}) {
|
3
|
+
ovalPolicy {
|
4
|
+
name
|
5
|
+
id
|
6
|
+
period
|
7
|
+
cronLine
|
8
|
+
hostgroups {
|
9
|
+
nodes {
|
10
|
+
name
|
11
|
+
id
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
15
|
+
checkCollection {
|
16
|
+
id
|
17
|
+
errors
|
18
|
+
failMsg
|
19
|
+
result
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
@@ -1,9 +1,58 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
3
|
|
4
|
-
import {
|
4
|
+
import {
|
5
|
+
FormGroup,
|
6
|
+
TextInput,
|
7
|
+
TextArea,
|
8
|
+
FormSelect,
|
9
|
+
FormSelectOption,
|
10
|
+
} from '@patternfly/react-core';
|
5
11
|
import { ExclamationCircleIcon } from '@patternfly/react-icons';
|
6
12
|
|
13
|
+
export const SelectField = props => {
|
14
|
+
const { selectItems, field, form } = props;
|
15
|
+
const fieldProps = wrapFieldProps(field);
|
16
|
+
|
17
|
+
const valid = shouldValidate(form, field.name);
|
18
|
+
|
19
|
+
return (
|
20
|
+
<FormGroup
|
21
|
+
label={props.label}
|
22
|
+
isRequired={props.isRequired}
|
23
|
+
helperTextInvalid={form.errors[field.name]}
|
24
|
+
helperTextInvalidIcon={<ExclamationCircleIcon />}
|
25
|
+
validated={valid}
|
26
|
+
>
|
27
|
+
<FormSelect
|
28
|
+
{...fieldProps}
|
29
|
+
className="without_select2"
|
30
|
+
aria-label={fieldProps.name}
|
31
|
+
validated={valid}
|
32
|
+
isDisabled={form.isSubmitting}
|
33
|
+
>
|
34
|
+
<FormSelectOption key={0} value="" label={props.blankLabel} />
|
35
|
+
{selectItems.map((item, idx) => (
|
36
|
+
<FormSelectOption key={idx + 1} value={item.id} label={item.name} />
|
37
|
+
))}
|
38
|
+
</FormSelect>
|
39
|
+
</FormGroup>
|
40
|
+
);
|
41
|
+
};
|
42
|
+
|
43
|
+
SelectField.propTypes = {
|
44
|
+
selectItems: PropTypes.array,
|
45
|
+
label: PropTypes.string.isRequired,
|
46
|
+
isRequired: PropTypes.bool,
|
47
|
+
field: PropTypes.object.isRequired,
|
48
|
+
form: PropTypes.object.isRequired,
|
49
|
+
blankLabel: PropTypes.string.isRequired,
|
50
|
+
};
|
51
|
+
SelectField.defaultProps = {
|
52
|
+
selectItems: [],
|
53
|
+
isRequired: false,
|
54
|
+
};
|
55
|
+
|
7
56
|
const wrapFieldProps = fieldProps => {
|
8
57
|
const { onChange } = fieldProps;
|
9
58
|
// modify onChange args to correctly wire formik with pf4 input handlers
|
@@ -61,3 +110,4 @@ const fieldWithHandlers = Component => {
|
|
61
110
|
};
|
62
111
|
|
63
112
|
export const TextField = fieldWithHandlers(TextInput);
|
113
|
+
export const TextAreaField = fieldWithHandlers(TextArea);
|
@@ -4,8 +4,10 @@ const idSeparator = '-';
|
|
4
4
|
const versionSeparator = ':';
|
5
5
|
const defaultVersion = '01';
|
6
6
|
|
7
|
-
export const
|
8
|
-
|
7
|
+
export const decodeModelId = model => decodeId(model.id);
|
8
|
+
|
9
|
+
export const decodeId = globalId => {
|
10
|
+
const split = atob(globalId).split(idSeparator);
|
9
11
|
return parseInt(last(split), 10);
|
10
12
|
};
|
11
13
|
|
@@ -1,11 +1,12 @@
|
|
1
|
-
import {
|
1
|
+
import { decodeModelId } from './globalIdHelper';
|
2
2
|
|
3
3
|
const experimental = path => `/experimental${path}`;
|
4
4
|
|
5
5
|
const showPath = path => `${path}/:id`;
|
6
6
|
const newPath = path => `${path}/new`;
|
7
7
|
|
8
|
-
export const modelPath = (basePath, model) =>
|
8
|
+
export const modelPath = (basePath, model) =>
|
9
|
+
`${basePath}/${decodeModelId(model)}`;
|
9
10
|
|
10
11
|
// react-router uses path-to-regexp, should we use it as well in a future?
|
11
12
|
// https://github.com/pillarjs/path-to-regexp/tree/v1.7.0#compile-reverse-path-to-regexp
|
@@ -22,6 +23,7 @@ export const ovalContentsShowPath = showPath(ovalContentsPath);
|
|
22
23
|
export const ovalContentsNewPath = newPath(ovalContentsPath);
|
23
24
|
export const ovalPoliciesPath = experimental('/compliance/oval_policies');
|
24
25
|
export const ovalPoliciesShowPath = `${showPath(ovalPoliciesPath)}/:tab?`;
|
26
|
+
export const ovalPoliciesNewPath = newPath(ovalPoliciesPath);
|
25
27
|
export const hostsPath = '/hosts';
|
26
|
-
export const newJobPath = '/job_invocations
|
28
|
+
export const newJobPath = newPath('/job_invocations');
|
27
29
|
export const hostsShowPath = showPath(hostsPath);
|
@@ -56,7 +56,8 @@ const ovalContentNodes = [
|
|
56
56
|
thirdContent(),
|
57
57
|
fourthContent(),
|
58
58
|
];
|
59
|
-
|
59
|
+
|
60
|
+
export const ovalContents = {
|
60
61
|
totalCount: ovalContentNodes.length,
|
61
62
|
nodes: ovalContentNodes,
|
62
63
|
};
|
@@ -70,6 +71,10 @@ export const mocks = ovalContentMockFactory(
|
|
70
71
|
{ currentUser: admin }
|
71
72
|
);
|
72
73
|
|
74
|
+
export const unpagedMocks = ovalContentMockFactory({}, ovalContents, {
|
75
|
+
currentUser: admin,
|
76
|
+
});
|
77
|
+
|
73
78
|
export const paginatedMocks = ovalContentMockFactory(
|
74
79
|
{ first: 10, last: 5 },
|
75
80
|
{ totalCount: 7, nodes: [secondContent(), fourthContent()] },
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
|
+
import { Button } from '@patternfly/react-core';
|
4
|
+
|
3
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
4
6
|
|
5
7
|
import IndexTable from '../../../components/IndexTable';
|
@@ -7,7 +9,11 @@ import withLoading from '../../../components/withLoading';
|
|
7
9
|
import withDeleteModal from '../../../components/withDeleteModal';
|
8
10
|
|
9
11
|
import { linkCell } from '../../../helpers/tableHelper';
|
10
|
-
import {
|
12
|
+
import {
|
13
|
+
ovalPoliciesPath,
|
14
|
+
modelPath,
|
15
|
+
ovalPoliciesNewPath,
|
16
|
+
} from '../../../helpers/pathsHelper';
|
11
17
|
|
12
18
|
const OvalPoliciesTable = props => {
|
13
19
|
const columns = [{ title: __('Name') }, { title: __('OVAL Content') }];
|
@@ -33,6 +39,16 @@ const OvalPoliciesTable = props => {
|
|
33
39
|
return actions;
|
34
40
|
};
|
35
41
|
|
42
|
+
const createBtn = (
|
43
|
+
<Button
|
44
|
+
onClick={() => props.history.push(ovalPoliciesNewPath)}
|
45
|
+
variant="primary"
|
46
|
+
aria-label="create_oval_policy"
|
47
|
+
>
|
48
|
+
{__('Create OVAL Policy')}
|
49
|
+
</Button>
|
50
|
+
);
|
51
|
+
|
36
52
|
return (
|
37
53
|
<IndexTable
|
38
54
|
columns={columns}
|
@@ -42,6 +58,7 @@ const OvalPoliciesTable = props => {
|
|
42
58
|
totalCount={props.totalCount}
|
43
59
|
history={props.history}
|
44
60
|
ariaTableLabel={__('OVAL Policies Table')}
|
61
|
+
toolbarBtns={createBtn}
|
45
62
|
/>
|
46
63
|
);
|
47
64
|
};
|
@@ -0,0 +1,135 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { useLazyQuery } from '@apollo/client';
|
4
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
5
|
+
import {
|
6
|
+
Select,
|
7
|
+
SelectOption,
|
8
|
+
SelectVariant,
|
9
|
+
FormGroup,
|
10
|
+
} from '@patternfly/react-core';
|
11
|
+
import { ExclamationCircleIcon } from '@patternfly/react-icons';
|
12
|
+
import hostgroupsQuery from '../../../graphql/queries/hostgroups.gql';
|
13
|
+
|
14
|
+
const HostgroupSelect = ({
|
15
|
+
selected,
|
16
|
+
setSelected,
|
17
|
+
hgsError,
|
18
|
+
showError,
|
19
|
+
setShowError,
|
20
|
+
}) => {
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
22
|
+
|
23
|
+
const [typingTimeout, setTypingTimeout] = useState(null);
|
24
|
+
|
25
|
+
const [fetchHostgroups, { loading, data, error }] = useLazyQuery(
|
26
|
+
hostgroupsQuery
|
27
|
+
);
|
28
|
+
const results = data?.hostgroups?.nodes ? data.hostgroups.nodes : [];
|
29
|
+
|
30
|
+
const onSelect = (event, selection) => {
|
31
|
+
if (selected.find(item => item.name === selection)) {
|
32
|
+
setSelected(selected.filter(item => item.name !== selection));
|
33
|
+
} else {
|
34
|
+
const hg = results.find(item => item.name === selection);
|
35
|
+
setSelected([...selected, hg]);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
const onClear = () => {
|
40
|
+
if (showError) {
|
41
|
+
setShowError(false);
|
42
|
+
}
|
43
|
+
setSelected([]);
|
44
|
+
};
|
45
|
+
|
46
|
+
const onInputChange = value => {
|
47
|
+
if (showError) {
|
48
|
+
setShowError(false);
|
49
|
+
}
|
50
|
+
if (typingTimeout) {
|
51
|
+
clearTimeout(typingTimeout);
|
52
|
+
}
|
53
|
+
const variables = { search: `name ~ ${value}` };
|
54
|
+
setTypingTimeout(setTimeout(() => fetchHostgroups({ variables }), 500));
|
55
|
+
};
|
56
|
+
|
57
|
+
const shouldValidate = (err, shouldShowError) => {
|
58
|
+
if (shouldShowError) {
|
59
|
+
return err ? 'error' : 'success';
|
60
|
+
}
|
61
|
+
return 'noval';
|
62
|
+
};
|
63
|
+
|
64
|
+
const prepareOptions = fetchedResults => {
|
65
|
+
if (loading) {
|
66
|
+
return [
|
67
|
+
<SelectOption isDisabled key={0}>
|
68
|
+
{__('Loading...')}
|
69
|
+
</SelectOption>,
|
70
|
+
];
|
71
|
+
}
|
72
|
+
|
73
|
+
if (error) {
|
74
|
+
return [
|
75
|
+
<SelectOption isDisabled key={0}>
|
76
|
+
{sprintf('Failed to fetch hostgroups, cause: %s', error.message)}
|
77
|
+
</SelectOption>,
|
78
|
+
];
|
79
|
+
}
|
80
|
+
|
81
|
+
if (fetchedResults.length > 20) {
|
82
|
+
return [
|
83
|
+
<SelectOption isDisabled key={0}>
|
84
|
+
{sprintf(
|
85
|
+
'You have %s hostgroups to display. Please refine your search.',
|
86
|
+
fetchedResults.length
|
87
|
+
)}
|
88
|
+
</SelectOption>,
|
89
|
+
];
|
90
|
+
}
|
91
|
+
|
92
|
+
return fetchedResults.map((hg, idx) => (
|
93
|
+
<SelectOption key={hg.id} value={hg.name} />
|
94
|
+
));
|
95
|
+
};
|
96
|
+
|
97
|
+
return (
|
98
|
+
<FormGroup
|
99
|
+
label={__('Hostgroups')}
|
100
|
+
helperTextInvalidIcon={<ExclamationCircleIcon />}
|
101
|
+
helperTextInvalid={showError && hgsError}
|
102
|
+
validated={shouldValidate(hgsError, showError)}
|
103
|
+
>
|
104
|
+
<Select
|
105
|
+
variant={SelectVariant.typeaheadMulti}
|
106
|
+
typeAheadAriaLabel="Select a hostgroup"
|
107
|
+
placeholderText="Type a hostroup name..."
|
108
|
+
onToggle={() => setIsOpen(!isOpen)}
|
109
|
+
onSelect={onSelect}
|
110
|
+
onClear={onClear}
|
111
|
+
selections={selected.map(item => item.name)}
|
112
|
+
isOpen={isOpen}
|
113
|
+
onTypeaheadInputChanged={onInputChange}
|
114
|
+
validated={shouldValidate(hgsError, showError)}
|
115
|
+
>
|
116
|
+
{prepareOptions(results)}
|
117
|
+
</Select>
|
118
|
+
</FormGroup>
|
119
|
+
);
|
120
|
+
};
|
121
|
+
|
122
|
+
HostgroupSelect.propTypes = {
|
123
|
+
selected: PropTypes.array,
|
124
|
+
setSelected: PropTypes.func.isRequired,
|
125
|
+
hgsError: PropTypes.string,
|
126
|
+
showError: PropTypes.bool.isRequired,
|
127
|
+
setShowError: PropTypes.func.isRequired,
|
128
|
+
};
|
129
|
+
|
130
|
+
HostgroupSelect.defaultProps = {
|
131
|
+
selected: [],
|
132
|
+
hgsError: '',
|
133
|
+
};
|
134
|
+
|
135
|
+
export default HostgroupSelect;
|