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.
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;