diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/vulnerability_management/editor_component.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/vulnerability_management/editor_component.vue index 1ea8ae66564678c316fecc1e6ee3fd6f475b9171..810faa07cdda342eda06c4ba3fd91195cf2ddff7 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/vulnerability_management/editor_component.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/vulnerability_management/editor_component.vue @@ -1,5 +1,6 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { s__, sprintf, n__ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { POLICY_TYPE_COMPONENT_OPTIONS } from 'ee/security_orchestration/components/constants'; import { extractPolicyContent } from 'ee/security_orchestration/components/utils'; @@ -15,6 +16,7 @@ import { EDITOR_MODE_YAML, PARSING_ERROR_MESSAGE, SECURITY_POLICY_ACTIONS, + MAX_ALLOWED_RULES_LENGTH, } from '../constants'; import EditorLayout from '../editor_layout.vue'; import DimDisableContainer from '../dim_disable_container.vue'; @@ -34,6 +36,9 @@ export default { RULES_LABEL, ADD_RULE_LABEL, ACTIONS_LABEL, + exceedingRulesMessage: s__( + 'SecurityOrchestration|You can add a maximum of %{rulesCount} %{rules}.', + ), }, components: { GlButton, @@ -42,6 +47,7 @@ export default { ActionSection, DimDisableContainer, }, + directives: { GlTooltip: GlTooltipDirective }, mixins: [glFeatureFlagsMixin()], inject: ['namespacePath'], props: { @@ -92,6 +98,18 @@ export default { parsingError, }; }, + computed: { + canAddRule() { + return this.policy.rules?.length < MAX_ALLOWED_RULES_LENGTH; + }, + addRuleTitle() { + const rules = n__('rule', 'rules', this.policy.rules?.length); + return sprintf(this.$options.i18n.exceedingRulesMessage, { + rulesCount: MAX_ALLOWED_RULES_LENGTH, + rules, + }); + }, + }, methods: { changeEditorMode(mode) { this.mode = mode; @@ -184,9 +202,22 @@ export default { /> <div class="security-policies-bg-subtle gl-mb-5 gl-rounded-base gl-p-5"> - <gl-button variant="link" data-testid="add-rule" @click="addRule"> - {{ $options.i18n.ADD_RULE_LABEL }} - </gl-button> + <span + v-gl-tooltip="{ + disabled: canAddRule, + title: addRuleTitle, + }" + data-testid="add-rule-wrapper" + > + <gl-button + variant="link" + data-testid="add-rule" + :disabled="!canAddRule" + @click="addRule" + > + {{ $options.i18n.ADD_RULE_LABEL }} + </gl-button> + </span> </div> </dim-disable-container> </template> diff --git a/ee/app/validators/json_schemas/security_orchestration_policy.json b/ee/app/validators/json_schemas/security_orchestration_policy.json index f9af0db8a48c7fffe4f2199f6594157ece448366..506d5589530c0efca5cc80d039cc46bd0619ff60 100644 --- a/ee/app/validators/json_schemas/security_orchestration_policy.json +++ b/ee/app/validators/json_schemas/security_orchestration_policy.json @@ -67,6 +67,7 @@ "rules": { "description": "Specifies conditions when this policy should be applied.", "type": "array", + "maxItems": 5, "additionalItems": false, "items": { "type": "object", diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/vulnerability_management/editor_component_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/vulnerability_management/editor_component_spec.js index 8c562a17f8dadda9ae4d46d02f9269e492f10024..d3dc8753bcc7b1eee765dd2401a6d187f4f5e72f 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/vulnerability_management/editor_component_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/vulnerability_management/editor_component_spec.js @@ -1,6 +1,7 @@ import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/security_orchestration/constants'; import EditorComponent from 'ee/security_orchestration/components/policy_editor/vulnerability_management/editor_component.vue'; import EditorLayout from 'ee/security_orchestration/components/policy_editor/editor_layout.vue'; @@ -25,6 +26,9 @@ describe('EditorComponent', () => { const factory = ({ propsData = {}, provide = {} } = {}) => { wrapper = shallowMountExtended(EditorComponent, { + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, propsData: { assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT, isCreating: false, @@ -39,11 +43,11 @@ describe('EditorComponent', () => { }); }; - const factoryWithExistingPolicy = () => { + const factoryWithExistingPolicy = ({ policy = {} } = {}) => { return factory({ propsData: { assignedPolicyProject: ASSIGNED_POLICY_PROJECT, - existingPolicy: mockVulnerabilityManagementObject, + existingPolicy: { ...mockVulnerabilityManagementObject, ...policy }, isEditing: true, }, }); @@ -53,6 +57,8 @@ describe('EditorComponent', () => { const findRuleSection = () => wrapper.findComponent(RuleSection); const findAllRuleSections = () => wrapper.findAllComponents(RuleSection); const findAddRuleButton = () => wrapper.findByTestId('add-rule'); + const findTooltip = () => + getBinding(wrapper.findByTestId('add-rule-wrapper').element, 'gl-tooltip'); const findActionSection = () => wrapper.findComponent(ActionSection); beforeEach(() => { @@ -105,6 +111,20 @@ describe('EditorComponent', () => { it('shows correct label for add rule button', () => { expect(findAddRuleButton().text()).toBe('Add new rule'); + expect(findAddRuleButton().props('disabled')).toBe(false); + expect(findTooltip().value.disabled).toBe(true); + }); + + it('disables add button when the limit of 5 rules has been reached', () => { + const limit = 5; + const { id, ...rule } = mockVulnerabilityManagementObject.rules[0]; + factoryWithExistingPolicy({ policy: { rules: [rule, rule, rule, rule, rule] } }); + expect(findAllRuleSections()).toHaveLength(limit); + expect(findAddRuleButton().props('disabled')).toBe(true); + expect(findTooltip().value).toMatchObject({ + disabled: false, + title: 'You can add a maximum of 5 rules.', + }); }); it('removes rule when "remove" event is emitted', async () => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a94b5e9cba4d0737e224e29b8a38f6284e8386be..972466893370ac9147d7d212230739a00579c434 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -50546,6 +50546,9 @@ msgstr "" msgid "SecurityOrchestration|You already have the maximum %{maximumAllowed} %{policyType} %{instance}." msgstr "" +msgid "SecurityOrchestration|You can add a maximum of %{rulesCount} %{rules}." +msgstr "" + msgid "SecurityOrchestration|You can select this option only once." msgstr ""