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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/app/graphql/mutations/oval_policies/create.rb +33 -0
  3. data/app/helpers/policies_helper.rb +1 -1
  4. data/app/models/concerns/foreman_openscap/oval_facet_hostgroup_extensions.rb +1 -0
  5. data/app/models/concerns/foreman_openscap/policy_common.rb +1 -1
  6. data/app/services/foreman_openscap/oval/configure.rb +16 -13
  7. data/app/services/foreman_openscap/oval/setup_check.rb +1 -1
  8. data/lib/foreman_openscap/engine.rb +1 -0
  9. data/lib/foreman_openscap/version.rb +1 -1
  10. data/webpack/components/EditableInput.js +16 -10
  11. data/webpack/components/IndexTable/index.js +7 -2
  12. data/webpack/components/LinkButton.js +14 -2
  13. data/webpack/components/withLoading.js +3 -1
  14. data/webpack/graphql/mutations/createOvalPolicy.gql +22 -0
  15. data/webpack/graphql/queries/ovalPolicy.gql +3 -0
  16. data/webpack/helpers/formFieldsHelper.js +51 -1
  17. data/webpack/helpers/globalIdHelper.js +4 -2
  18. data/webpack/helpers/pathsHelper.js +5 -3
  19. data/webpack/helpers/toastsHelper.js +3 -0
  20. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +6 -1
  21. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +18 -1
  22. data/webpack/routes/OvalPolicies/OvalPoliciesNew/HostgroupSelect.js +135 -0
  23. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyForm.js +119 -0
  24. data/webpack/routes/OvalPolicies/OvalPoliciesNew/NewOvalPolicyFormHelpers.js +107 -0
  25. data/webpack/routes/OvalPolicies/OvalPoliciesNew/OvalPoliciesNew.js +32 -0
  26. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.fixtures.js +147 -0
  27. data/webpack/routes/OvalPolicies/OvalPoliciesNew/__tests__/OvalPoliciesNew.test.js +172 -0
  28. data/webpack/routes/OvalPolicies/OvalPoliciesNew/index.js +11 -0
  29. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +2 -2
  30. data/webpack/routes/OvalPolicies/OvalPoliciesShow/DetailsTab.js +2 -0
  31. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +4 -3
  32. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesEdit.test.js +27 -0
  33. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +11 -1
  34. data/webpack/routes/routes.js +7 -0
  35. data/webpack/testHelper.js +22 -0
  36. metadata +39 -42
  37. data/locale/de/foreman_openscap.edit.po +0 -0
  38. data/locale/en_GB/foreman_openscap.edit.po +0 -0
  39. data/locale/es/foreman_openscap.edit.po +0 -0
  40. data/locale/fr/foreman_openscap.edit.po +0 -0
  41. data/locale/gl/foreman_openscap.edit.po +0 -0
  42. data/locale/it/foreman_openscap.edit.po +0 -0
  43. data/locale/ja/foreman_openscap.edit.po +0 -0
  44. data/locale/ko/foreman_openscap.edit.po +0 -0
  45. data/locale/pt_BR/foreman_openscap.edit.po +0 -0
  46. data/locale/ru/foreman_openscap.edit.po +0 -0
  47. data/locale/sv_SE/foreman_openscap.edit.po +0 -0
  48. data/locale/zh_CN/foreman_openscap.edit.po +0 -0
  49. 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: 0deaa4503a6ab004120595983e5b6fac947691d57e7b8da5d38a0aed3316f1a6
4
- data.tar.gz: 7447310d905705fbf71ca93f1cca7b2314e7d13827e90ce9c3a8b321cfcba411
3
+ metadata.gz: 0cadd1a7264151b9ab0bd0cc4d21eb212f621094c9f62c68a5922c2c0fe7d20f
4
+ data.tar.gz: 87a58a9b949841cd923a9d91d7fc18bc2717194a6cd1a8c96f90f89cebf925ae
5
5
  SHA512:
6
- metadata.gz: 192e4e96375311fbf3225aa5e715eed99797338a8ba2be9d5b19cdf3f49dcae9ff5a78cb0136df175744360dbcc5ffc2ce453058ac31839a71f0373e79fa22cc
7
- data.tar.gz: 2b231f618e80bc0bd187417328afae0c2b7f4c7a10d0954dc26ff554fd4ad34c67073b252cdc6f86ae6194d9a9d7c53c5c08bb00ba7348d884e334baac82f1fe
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 => tailoring_file.scap_content_profiles.first.id },
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
@@ -11,6 +11,7 @@ module ForemanOpenscap
11
11
  :on => :id,
12
12
  :rename => :oval_policy_id,
13
13
  :complete_value => false,
14
+ :only_explicit => true,
14
15
  :ext_method => :find_by_oval_policy_id,
15
16
  :operators => ['= ']
16
17
  end
@@ -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] + 1) % 7
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
- check_collection = without_proxy_to_check items_without_proxy
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 model_to_s(model)
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 => model_to_s(model), :name => 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
@@ -1,7 +1,7 @@
1
1
  module ForemanOpenscap
2
2
  module Oval
3
3
  class SetupCheck
4
- attr_reader :result, :id, :errors
4
+ attr_reader :result, :id, :title, :errors
5
5
 
6
6
  def initialize(hash)
7
7
  @id = hash[:id]
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ForemanOpenscap
2
- VERSION = "5.1.0".freeze
2
+ VERSION = "5.1.1".freeze
3
3
  end
@@ -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
- <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>
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 aria-label={ariaTableLabel} cells={columns} {...rest}>
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 = ({ path, btnVariant, btnText, isDisabled }) => (
6
+ const LinkButton = ({
7
+ path,
8
+ btnVariant,
9
+ btnText,
10
+ isDisabled,
11
+ btnAriaLabel,
12
+ }) => (
7
13
  <Link to={path}>
8
- <Button variant={btnVariant} isDisabled={isDisabled}>
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
+ }
@@ -9,6 +9,9 @@ query($id: String!) {
9
9
  weekday
10
10
  dayOfMonth
11
11
  description
12
+ meta {
13
+ canEdit
14
+ }
12
15
  hostgroups {
13
16
  nodes {
14
17
  id
@@ -1,9 +1,58 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
 
4
- import { FormGroup, TextInput } from '@patternfly/react-core';
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 decodeId = model => {
8
- const split = atob(model.id).split(idSeparator);
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 { decodeId } from './globalIdHelper';
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) => `${basePath}/${decodeId(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/new';
28
+ export const newJobPath = newPath('/job_invocations');
27
29
  export const hostsShowPath = showPath(hostsPath);
@@ -0,0 +1,3 @@
1
+ import { addToast } from 'foremanReact/redux/actions/toasts';
2
+
3
+ export const showToast = dispatch => toast => dispatch(addToast(toast));
@@ -56,7 +56,8 @@ const ovalContentNodes = [
56
56
  thirdContent(),
57
57
  fourthContent(),
58
58
  ];
59
- const ovalContents = {
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 { ovalPoliciesPath, modelPath } from '../../../helpers/pathsHelper';
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;