foreman_openscap 5.1.0 → 5.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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;
|