Commit 9dee2023 authored by Zamir Martins's avatar Zamir Martins Committed by Jose Ivan Vargas

Add severity_levels in UI dialogs related to project approval rules

parent 86abff01
......@@ -2,7 +2,7 @@
import { GlFormGroup, GlFormInput, GlDropdown, GlTruncate, GlDropdownItem } from '@gitlab/ui';
import { groupBy, isEqual, isNumber, omit } from 'lodash';
import { mapState, mapActions } from 'vuex';
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import { REPORT_TYPES, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue';
import { sprintf } from '~/locale';
import {
......@@ -71,6 +71,7 @@ export default {
containsHiddenGroups: false,
serverValidationErrors: [],
scanners: [],
severityLevels: [],
...this.getInitialData(),
};
},
......@@ -98,11 +99,11 @@ export default {
invalidName() {
if (this.isMultiSubmission) {
if (this.serverValidationErrors.includes('name has already been taken')) {
return this.$options.APPROVAL_DIALOG_I18N.validations.ruleNameTaken;
return APPROVAL_DIALOG_I18N.validations.ruleNameTaken;
}
if (!this.name) {
return this.$options.APPROVAL_DIALOG_I18N.validations.ruleNameMissing;
return APPROVAL_DIALOG_I18N.validations.ruleNameMissing;
}
}
......@@ -110,15 +111,15 @@ export default {
},
invalidApprovalsRequired() {
if (!isNumber(this.approvalsRequired)) {
return this.$options.APPROVAL_DIALOG_I18N.validations.approvalsRequiredNotNumber;
return APPROVAL_DIALOG_I18N.validations.approvalsRequiredNotNumber;
}
if (this.approvalsRequired < 0) {
return this.$options.APPROVAL_DIALOG_I18N.validations.approvalsRequiredNegativeNumber;
return APPROVAL_DIALOG_I18N.validations.approvalsRequiredNegativeNumber;
}
if (this.approvalsRequired < this.minApprovalsRequired) {
return sprintf(this.$options.APPROVAL_DIALOG_I18N.validations.approvalsRequiredMinimum, {
return sprintf(APPROVAL_DIALOG_I18N.validations.approvalsRequiredMinimum, {
number: this.minApprovalsRequired,
});
}
......@@ -127,7 +128,7 @@ export default {
},
invalidApprovers() {
if (this.isMultiSubmission && this.approvers.length <= 0) {
return this.$options.APPROVAL_DIALOG_I18N.validations.approversRequired;
return APPROVAL_DIALOG_I18N.validations.approversRequired;
}
return '';
......@@ -137,25 +138,33 @@ export default {
!this.isMrEdit &&
!this.branches.every((branch) => isEqual(branch, ANY_BRANCH) || isNumber(branch?.id))
) {
return this.$options.APPROVAL_DIALOG_I18N.validations.branchesRequired;
return APPROVAL_DIALOG_I18N.validations.branchesRequired;
}
return '';
},
invalidScanners() {
if (this.scanners.length <= 0) {
return this.$options.APPROVAL_DIALOG_I18N.validations.scannersRequired;
return APPROVAL_DIALOG_I18N.validations.scannersRequired;
}
return '';
},
invalidVulnerabilitiesAllowedError() {
if (!isNumber(this.vulnerabilitiesAllowed)) {
return this.$options.APPROVAL_DIALOG_I18N.validations.approvalsRequiredNotNumber;
return APPROVAL_DIALOG_I18N.validations.approvalsRequiredNotNumber;
}
if (this.vulnerabilitiesAllowed < 0) {
return this.$options.APPROVAL_DIALOG_I18N.validations.vulnerabilitiesAllowedMinimum;
return APPROVAL_DIALOG_I18N.validations.vulnerabilitiesAllowedMinimum;
}
return '';
},
invalidSeverityLevels() {
if (this.severityLevels.length === 0) {
return APPROVAL_DIALOG_I18N.validations.severityLevelsRequired;
}
return '';
},
isValid() {
......@@ -165,7 +174,8 @@ export default {
this.isValidApprovalsRequired &&
this.isValidApprovers &&
this.areValidScanners &&
this.isValidVulnerabilitiesAllowed
this.isValidVulnerabilitiesAllowed &&
this.areValidSeverityLevels
);
},
isValidName() {
......@@ -190,6 +200,9 @@ export default {
!this.invalidVulnerabilitiesAllowedError
);
},
areValidSeverityLevels() {
return !this.showValidation || !this.isVulnerabilityCheck || !this.invalidSeverityLevels;
},
isMultiSubmission() {
return this.settings.allowMultiRule && !this.isFallbackSubmission;
},
......@@ -228,6 +241,7 @@ export default {
removeHiddenGroups: this.removeHiddenGroups,
protectedBranchIds: this.branches.map((x) => x.id),
scanners: this.scanners,
severityLevels: this.severityLevels,
};
},
isEditing() {
......@@ -242,15 +256,33 @@ export default {
scannersText() {
switch (this.scanners.length) {
case Object.values(this.$options.REPORT_TYPES).length:
return this.$options.APPROVAL_DIALOG_I18N.form.allScannersSelectedLabel;
return APPROVAL_DIALOG_I18N.form.allScannersSelectedLabel;
case 0:
return this.$options.APPROVAL_DIALOG_I18N.form.scannersSelectLabel;
return APPROVAL_DIALOG_I18N.form.scannersSelectLabel;
case 1:
return this.$options.REPORT_TYPES[this.scanners[0]];
default:
return sprintf(this.$options.APPROVAL_DIALOG_I18N.form.multipleSelectedScannersLabel, {
scanner: this.$options.REPORT_TYPES[this.scanners[0]],
additionalScanners: this.scanners.length - 1,
return sprintf(APPROVAL_DIALOG_I18N.form.multipleSelectedLabel, {
firstLabel: this.$options.REPORT_TYPES[this.scanners[0]],
numberOfAdditionalLabels: this.scanners.length - 1,
});
}
},
areAllSeverityLevelsSelected() {
return this.severityLevels.length === Object.values(this.$options.SEVERITY_LEVELS).length;
},
severityLevelsText() {
switch (this.severityLevels.length) {
case Object.keys(this.$options.SEVERITY_LEVELS).length:
return APPROVAL_DIALOG_I18N.form.allSeverityLevelsSelectedLabel;
case 0:
return APPROVAL_DIALOG_I18N.form.severityLevelsSelectLabel;
case 1:
return this.$options.SEVERITY_LEVELS[this.severityLevels[0]];
default:
return sprintf(APPROVAL_DIALOG_I18N.form.multipleSelectedLabel, {
firstLabel: this.$options.SEVERITY_LEVELS[this.severityLevels[0]],
numberOfAdditionalLabels: this.severityLevels.length - 1,
});
}
},
......@@ -371,6 +403,7 @@ export default {
branches,
scanners: this.initRule.scanners || [],
vulnerabilitiesAllowed: this.initRule.vulnerabilitiesAllowed || 0,
severityLevels: this.initRule.severityLevels || [],
};
},
setAllSelectedScanners() {
......@@ -387,9 +420,26 @@ export default {
this.scanners.splice(pos, 1);
}
},
setAllSelectedSeverityLevels() {
this.severityLevels = this.areAllSeverityLevelsSelected
? []
: Object.keys(this.$options.SEVERITY_LEVELS);
},
isSeveritySelected(severity) {
return this.severityLevels.includes(severity);
},
setSeverity(severity) {
const pos = this.severityLevels.indexOf(severity);
if (pos === -1) {
this.severityLevels.push(severity);
} else {
this.severityLevels.splice(pos, 1);
}
},
},
APPROVAL_DIALOG_I18N,
REPORT_TYPES: omit(REPORT_TYPES, EXCLUDED_REPORT_TYPE),
SEVERITY_LEVELS,
};
</script>
......@@ -441,7 +491,7 @@ export default {
:is-checked="areAllScannersSelected"
@click.native.capture.stop="setAllSelectedScanners"
>
<gl-truncate :text="$options.APPROVAL_DIALOG_I18N.form.selectAllScannersLabel" />
<gl-truncate :text="$options.APPROVAL_DIALOG_I18N.form.selectAllLabel" />
</gl-dropdown-item>
<gl-dropdown-item
v-for="(value, key) in $options.REPORT_TYPES"
......@@ -454,6 +504,34 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group
v-if="isVulnerabilityCheck"
:label="$options.APPROVAL_DIALOG_I18N.form.severityLevelsLabel"
:description="$options.APPROVAL_DIALOG_I18N.form.severityLevelsDescription"
:state="areValidSeverityLevels"
:invalid-feedback="invalidSeverityLevels"
data-testid="severity-levels-group"
>
<gl-dropdown :text="severityLevelsText">
<gl-dropdown-item
key="all"
is-check-item
:is-checked="areAllSeverityLevelsSelected"
@click.native.capture.stop="setAllSelectedSeverityLevels"
>
<gl-truncate :text="$options.APPROVAL_DIALOG_I18N.form.selectAllLabel" />
</gl-dropdown-item>
<gl-dropdown-item
v-for="(value, key) in $options.SEVERITY_LEVELS"
:key="key"
is-check-item
:is-checked="isSeveritySelected(key)"
@click.native.capture.stop="setSeverity(key)"
>
<gl-truncate :text="value" />
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group
:label="$options.APPROVAL_DIALOG_I18N.form.approvalsRequiredLabel"
:state="isValidApprovalsRequired"
......
......@@ -90,13 +90,19 @@ export const APPROVAL_DIALOG_I18N = {
scannersDescription: s__(
'ApprovalRule|Apply this approval rule to consider only the selected security scanners.',
),
selectAllScannersLabel: s__('ApprovalRule|Select All'),
selectAllLabel: s__('ApprovalRule|Select All'),
allScannersSelectedLabel: s__('ApprovalRule|All scanners'),
multipleSelectedScannersLabel: s__('ApprovalRule|%{scanner} +%{additionalScanners} more'),
multipleSelectedLabel: s__('ApprovalRule|%{firstLabel} +%{numberOfAdditionalLabels} more'),
vulnerabilitiesAllowedLabel: s__('ApprovalRule|Vulnerabilities allowed'),
vulnerabilitiesAllowedDescription: s__(
'ApprovalRule|Number of vulnerabilities allowed before approval rule is triggered.',
),
severityLevelsLabel: s__('ApprovalRule|Severity levels'),
severityLevelsDescription: s__(
'ApprovalRule|Apply this approval rule to consider only the selected severity levels.',
),
severityLevelsSelectLabel: s__('ApprovalRule|Select severity levels'),
allSeverityLevelsSelectedLabel: s__('ApprovalRule|All severity levels'),
},
validations: {
approvalsRequiredNegativeNumber: __('Please enter a non-negative number'),
......@@ -112,5 +118,6 @@ export const APPROVAL_DIALOG_I18N = {
vulnerabilitiesAllowedMinimum: s__(
'ApprovalRule|Please enter a number equal or greater than zero',
),
severityLevelsRequired: s__('ApprovalRule|Please select at least one severity level'),
},
};
......@@ -33,6 +33,7 @@ export const mapApprovalRuleRequest = (req) => ({
protected_branch_ids: req.protectedBranchIds,
scanners: req.scanners,
vulnerabilities_allowed: req.vulnerabilitiesAllowed,
severity_levels: req.severityLevels,
});
export const mapApprovalFallbackRuleRequest = (req) => ({
......@@ -54,6 +55,7 @@ export const mapApprovalRuleResponse = (res) => ({
overridden: res.overridden,
scanners: res.scanners,
vulnerabilitiesAllowed: res.vulnerabilities_allowed,
severityLevels: res.severity_levels,
});
export const mapApprovalSettingsResponse = (res) => ({
......
......@@ -41,6 +41,7 @@ const TEST_RULE_VULNERABILITY_CHECK = {
name: VULNERABILITY_CHECK_NAME,
scanners: ['sast', 'dast'],
vulnerabilitiesAllowed: 0,
severityLevels: ['high'],
};
const TEST_APPROVERS = [{ id: 7, type: TYPE_USER }];
const TEST_APPROVALS_REQUIRED = 3;
......@@ -99,6 +100,7 @@ describe('EE Approvals RuleForm', () => {
const findBranchesValidation = () => wrapper.findByTestId('branches-group');
const findScannersGroup = () => wrapper.findByTestId('scanners-group');
const findVulnerabilityFormGroup = () => wrapper.findByTestId('vulnerability-amount-group');
const findSeverityLevelsGroup = () => wrapper.findByTestId('severity-levels-group');
const inputsAreValid = (inputs) => inputs.every((x) => x.props('state'));
......@@ -203,6 +205,7 @@ describe('EE Approvals RuleForm', () => {
groupRecords,
removeHiddenGroups: false,
scanners: [],
severityLevels: [],
protectedBranchIds: branches.map((x) => x.id),
};
......@@ -282,6 +285,7 @@ describe('EE Approvals RuleForm', () => {
groupRecords,
removeHiddenGroups: false,
scanners: [],
severityLevels: [],
protectedBranchIds: branches.map((x) => x.id),
};
......@@ -362,6 +366,7 @@ describe('EE Approvals RuleForm', () => {
groupRecords,
removeHiddenGroups: false,
scanners: [],
severityLevels: [],
protectedBranchIds: [],
};
......@@ -539,11 +544,11 @@ describe('EE Approvals RuleForm', () => {
describe('with approval suggestions', () => {
describe.each`
defaultRuleName | expectedDisabledAttribute | expectedDisplayedScanners | expectedDisplayVulnerabilityAllowed
${VULNERABILITY_CHECK_NAME} | ${true} | ${true} | ${true}
${'License-Check'} | ${true} | ${false} | ${false}
${'Coverage-Check'} | ${true} | ${false} | ${false}
${'Foo Bar Baz'} | ${false} | ${false} | ${false}
defaultRuleName | expectedDisabledAttribute | expectedDisplayedScanners | expectedDisplayVulnerabilityAllowed | expectedDisplayedSeverityLevels
${VULNERABILITY_CHECK_NAME} | ${true} | ${true} | ${true} | ${true}
${'License-Check'} | ${true} | ${false} | ${false} | ${false}
${'Coverage-Check'} | ${true} | ${false} | ${false} | ${false}
${'Foo Bar Baz'} | ${false} | ${false} | ${false} | ${false}
`(
'with defaultRuleName set to $defaultRuleName',
({
......@@ -551,6 +556,7 @@ describe('EE Approvals RuleForm', () => {
expectedDisabledAttribute,
expectedDisplayedScanners,
expectedDisplayVulnerabilityAllowed,
expectedDisplayedSeverityLevels,
}) => {
beforeEach(() => {
createComponent({
......@@ -575,6 +581,11 @@ describe('EE Approvals RuleForm', () => {
} the number of vulnerabilities form group`, () => {
expect(findVulnerabilityFormGroup().exists()).toBe(expectedDisplayVulnerabilityAllowed);
});
it(`it ${
expectedDisplayedSeverityLevels ? 'shows' : 'does not show'
} severity levels dropdown`, () => {
expect(findSeverityLevelsGroup().exists()).toBe(expectedDisplayedSeverityLevels);
});
},
);
});
......@@ -672,7 +683,41 @@ describe('EE Approvals RuleForm', () => {
it('contains the supported report types and select all option', () => {
const supportedReportsPlusAll =
Object.keys(REPORT_TYPES).length - [EXCLUDED_REPORT_TYPE].length + 1;
expect(wrapper.findAllComponents(GlTruncate)).toHaveLength(supportedReportsPlusAll);
expect(findScannersGroup().findAllComponents(GlTruncate)).toHaveLength(
supportedReportsPlusAll,
);
});
});
describe('and without any severity levels selected', () => {
beforeEach(() => {
createComponent({
initRule: {
...TEST_RULE_VULNERABILITY_CHECK,
severityLevels: [],
},
});
findForm().trigger('submit');
});
it('does not dispatch the action on submit', () => {
expect(actions.postRule).not.toHaveBeenCalled();
});
});
describe('with one severity level selected', () => {
beforeEach(() => {
createComponent({
initRule: TEST_RULE_VULNERABILITY_CHECK,
});
findForm().trigger('submit');
});
it('dispatches the action on submit', () => {
expect(actions.postRule).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ severityLevels: ['high'] }),
);
});
});
});
......
......@@ -4166,7 +4166,7 @@ msgid_plural "ApprovalRuleSummary|%{count} approvals required from %{membersCoun
msgstr[0] ""
msgstr[1] ""
msgid "ApprovalRule|%{scanner} +%{additionalScanners} more"
msgid "ApprovalRule|%{firstLabel} +%{numberOfAdditionalLabels} more"
msgstr ""
msgid "ApprovalRule|Add approvers"
......@@ -4175,9 +4175,15 @@ msgstr ""
msgid "ApprovalRule|All scanners"
msgstr ""
msgid "ApprovalRule|All severity levels"
msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected security scanners."
msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected severity levels."
msgstr ""
msgid "ApprovalRule|Approval rules"
msgstr ""
......@@ -4205,6 +4211,9 @@ msgstr ""
msgid "ApprovalRule|Please select at least one security scanner"
msgstr ""
msgid "ApprovalRule|Please select at least one severity level"
msgstr ""
msgid "ApprovalRule|Rule name"
msgstr ""
......@@ -4217,6 +4226,12 @@ msgstr ""
msgid "ApprovalRule|Select scanners"
msgstr ""
msgid "ApprovalRule|Select severity levels"
msgstr ""
msgid "ApprovalRule|Severity levels"
msgstr ""
msgid "ApprovalRule|Target branch"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment