diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js index 8269a7790a54ea6f8226000336569745040bbc00..7f2c41d9aaf94c1818856a6699a67eeff52a7747 100644 --- a/app/assets/javascripts/graphql_shared/constants.js +++ b/app/assets/javascripts/graphql_shared/constants.js @@ -29,3 +29,4 @@ export const TYPENAME_WORK_ITEM = 'WorkItem'; export const TYPENAME_ORGANIZATION = 'Organization'; export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply'; export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace'; +export const TYPE_COMPLIANCE_FRAMEWORK = 'ComplianceManagement::Framework'; diff --git a/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue b/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue index e51f38294636af603eada0f9b1bfe11921d2cb70..5a2ea77186ab70c8dec1b116110e4dcff8ec6dee 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/group_projects_dropdown.vue @@ -2,7 +2,10 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import { debounce } from 'lodash'; import produce from 'immer'; -import { n__, __ } from '~/locale'; +import { __ } from '~/locale'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; +import { renderMultiSelectText } from 'ee/security_orchestration/components/policy_editor/utils'; import getGroupProjects from 'ee/security_orchestration/graphql/queries/get_group_projects.query.graphql'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; @@ -51,6 +54,18 @@ export default { required: false, default: () => [], }, + /** + * selected ids passed as short format + * [21,34,45] as number + * needs to be converted to full graphql id + * if false, selectedProjectsIds needs to be + * an array of full graphQl ids + */ + useShortIdFormat: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -60,18 +75,34 @@ export default { }; }, computed: { - dropdownPlaceholder() { - if (this.selectedProjectsIds.length === this.projects.length && !this.loading) { - return __('All projects selected'); + formattedSelectedProjectsIds() { + if (this.useShortIdFormat) { + return ( + this.selectedProjectsIds?.map((id) => convertToGraphQLId(TYPENAME_PROJECT, id)) || [] + ); } - if (this.selectedProjectsIds.length) { - return n__('%d project selected', '%d projects selected', this.selectedProjectsIds.length); - } - return __('Select projects'); + + return this.selectedProjectsIds || []; + }, + existingFormattedSelectedProjectsIds() { + return this.formattedSelectedProjectsIds.filter((id) => this.projectsIds.includes(id)); + }, + dropdownPlaceholder() { + return renderMultiSelectText( + this.formattedSelectedProjectsIds, + this.projectItems, + __('projects'), + ); }, loading() { return this.$apollo.queries.projects.loading; }, + projectItems() { + return this.projects?.reduce((acc, { id, name }) => { + acc[id] = name; + return acc; + }, {}); + }, projectListBoxItems() { return this.projects.map(({ id, name }) => ({ text: name, value: id })); }, @@ -110,7 +141,9 @@ export default { this.searchTerm = searchTerm.trim(); }, selectProjects(ids) { - this.$emit('select', ids); + const payload = this.useShortIdFormat ? ids.map((id) => getIdFromGraphQLId(id)) : ids; + + this.$emit('select', payload); }, }, }; @@ -128,7 +161,7 @@ export default { :infinite-scroll="projectsPageInfo.hasNextPage" :infinite-scroll-loading="loading" :searching="loading" - :selected="selectedProjectsIds" + :selected="existingFormattedSelectedProjectsIds" :placement="placement" :items="projectListBoxItems" :reset-button-label="$options.i18n.clearAllLabel" diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js index 4dc6e7b9a92e5e43b567a251f531de5acefabb1e..8fbabae1f0fb1c3095bc0615d85968cea747b1d9 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/constants.js @@ -176,3 +176,9 @@ export const HUMANIZED_BRANCH_TYPE_TEXT_DICT = { [GROUP_DEFAULT_BRANCHES.value]: s__('SecurityOrchestration|any default branch'), [PROJECT_DEFAULT_BRANCH.value]: s__('SecurityOrchestration|the default branch'), }; + +export const MULTIPLE_SELECTED_LABEL = s__( + 'PolicyRuleMultiSelect|%{firstLabel} +%{numberOfAdditionalLabels} more', +); +export const SELECTED_ITEMS_LABEL = s__('PolicyRuleMultiSelect|Select %{itemTypeName}'); +export const ALL_SELECTED_LABEL = s__('PolicyRuleMultiSelect|All %{itemTypeName}'); diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue index 4be5b830ca7a09ecda1a6c9a62345c82ee2fd0bf..7f6a08025f60b91ef7d6b8ad3576562b9c15716c 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue @@ -15,7 +15,7 @@ import { __, s__, sprintf } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue'; import DimDisableContainer from 'ee/security_orchestration/components/policy_editor/dim_disable_container.vue'; -import PolicyScope from 'ee/security_orchestration/components/policy_editor/scope/policy_scope.vue'; +import ScopeSection from 'ee/security_orchestration/components/policy_editor/scope/scope_section.vue'; import { NAMESPACE_TYPES } from '../../constants'; import { POLICY_TYPE_COMPONENT_OPTIONS } from '../constants'; import { @@ -46,7 +46,7 @@ export default { ], components: { DimDisableContainer, - PolicyScope, + ScopeSection, GlAlert, GlButton, GlFormGroup, @@ -257,7 +257,10 @@ export default { <div class="gl-bg-gray-10 gl-rounded-base gl-p-6"></div> </template> - <policy-scope /> + <scope-section + :policy-scope="policy.policy_scope" + @changed="setPolicyProperty('policy_scope', $event)" + /> </dim-disable-container> <slot name="actions-first"></slot> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/rule_multi_select.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/rule_multi_select.vue index 53c3557a1d9b265f592d1b76bf2efa13d69df3e7..5a709f64a1ec7be8770e1254b92417168a9e39fe 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/rule_multi_select.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/rule_multi_select.vue @@ -2,9 +2,7 @@ import { GlCollapsibleListbox, GlTruncate } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; - -const NO_ITEM_SELECTED = 0; -const ONE_ITEM_SELECTED = 1; +import { renderMultiSelectText } from './utils'; export default { components: { @@ -49,29 +47,7 @@ export default { return this.includeSelectAll ? this.$options.i18n.selectAllLabel : ''; }, text() { - switch (this.selected.length) { - case this.itemsKeys.length: - return sprintf( - this.$options.i18n.allSelectedLabel, - { itemTypeName: this.itemTypeName }, - false, - ); - case NO_ITEM_SELECTED: - return sprintf( - this.$options.i18n.selectedItemsLabel, - { - itemTypeName: this.itemTypeName, - }, - false, - ); - case ONE_ITEM_SELECTED: - return this.items[this.selected[0]]; - default: - return sprintf(this.$options.i18n.multipleSelectedLabel, { - firstLabel: this.items[this.selected[0]], - numberOfAdditionalLabels: this.selected.length - 1, - }); - } + return renderMultiSelectText(this.selected, this.items, this.itemTypeName); }, itemsKeys() { return Object.keys(this.items); diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml.js index a72513bb55e4a2d88d267d5aba869417eaadb3fb..408a31d08ee52acd9aafdc63925374d4c787411b 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml.js @@ -2,6 +2,7 @@ import { safeLoad } from 'js-yaml'; import { isValidPolicy, hasInvalidCron } from '../../utils'; import { BRANCH_TYPE_KEY, + PRIMARY_POLICY_KEYS, RULE_MODE_SCANNERS, VALID_SCAN_EXECUTION_BRANCH_TYPE_OPTIONS, } from '../../constants'; @@ -65,7 +66,15 @@ export const fromYaml = ({ manifest, validateRuleMode = false }) => { ]; const actionsKeys = ['scan', 'site_profile', 'scanner_profile', 'variables', 'tags']; - return isValidPolicy({ policy, rulesKeys, actionsKeys }) && + /** + * Can be removed after ff is enabled + */ + const primaryKeys = PRIMARY_POLICY_KEYS; + if (gon?.features?.securityPoliciesPolicyScope) { + primaryKeys.push('policy_scope'); + } + + return isValidPolicy({ policy, primaryKeys, rulesKeys, actionsKeys }) && !hasInvalidCron(policy) && !hasInvalidBranchType(policy.rules) && hasRuleModeSupportedScanners(policy) diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js index 9783fc1756e0f1c8ec4d5aca82a2a5eb293614a4..8542b698f4d9409373134d74428f06869bf24a43 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/lib/from_yaml.js @@ -32,6 +32,7 @@ export const fromYaml = ({ manifest, validateRuleMode = false }) => { ...(hasApprovalSettings || isEqual(policy.approval_settings, PERMITTED_INVALID_SETTINGS) // Temporary workaround to allow the rule builder to load with wrongly persisted settings ? [`approval_settings`] : []), + ...(gon?.features?.securityPoliciesPolicyScope ? ['policy_scope'] : []), ]; const rulesKeys = [ 'type', diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue index 0815d86309cd8359b2053d84870e9927b081c40b..cc280974751c550e903f175a3d99d3df6c278c29 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue @@ -1,8 +1,11 @@ <script> import { debounce } from 'lodash'; import { GlButton, GlCollapsibleListbox, GlLabel } from '@gitlab/ui'; -import { n__, s__, __ } from '~/locale'; +import { s__, __ } from '~/locale'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { TYPE_COMPLIANCE_FRAMEWORK } from '~/graphql_shared/constants'; import getComplianceFrameworkQuery from 'ee/graphql_shared/queries/get_compliance_framework.query.graphql'; +import { renderMultiSelectText } from 'ee/security_orchestration/components/policy_editor/utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import ComplianceFrameworkFormModal from 'ee/groups/settings/compliance_frameworks/components/form_modal.vue'; @@ -10,7 +13,7 @@ export default { i18n: { complianceFrameworkCreateButton: s__('SecurityOrchestration|Create new framework label'), complianceFrameworkHeader: s__('SecurityOrchestration|Select frameworks'), - complianceFrameworkPlaceholder: s__('SecurityOrchestration|Choose framework labels'), + complianceFrameworkTypeName: s__('SecurityOrchestration|compliance frameworks'), errorMessage: s__('SecurityOrchestration|At least one framework label should be selected'), noFrameworksText: s__('SecurityOrchestration|No compliance frameworks'), selectAllLabel: __('Select all'), @@ -61,6 +64,18 @@ export default { required: false, default: false, }, + /** + * selected ids passed as short format + * [21,34,45] as number + * needs to be converted to full graphql id + * if false, selectedFrameworkIds needs to be + * an array of full graphQl ids + */ + useShortIdFormat: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -69,22 +84,34 @@ export default { }; }, computed: { - dropdownPlaceholder() { - if ( - this.selectedFrameworkIds.length === this.complianceFrameworks?.length && - this.complianceFrameworks?.length > 0 - ) { - return __('All frameworks selected'); - } - if (this.selectedFrameworkIds.length) { - return n__( - '%d compliance framework selected', - '%d compliance frameworks selected', - this.selectedFrameworkIds.length, + formattedSelectedFrameworkIds() { + if (this.useShortIdFormat) { + return ( + this.selectedFrameworkIds?.map((id) => + convertToGraphQLId(TYPE_COMPLIANCE_FRAMEWORK, id), + ) || [] ); } - return this.$options.i18n.complianceFrameworkPlaceholder; + return this.selectedFrameworkIds || []; + }, + existingFormattedSelectedFrameworkIds() { + return this.formattedSelectedFrameworkIds.filter((id) => + this.complianceFrameworkIds.includes(id), + ); + }, + complianceFrameworkItems() { + return this.complianceFrameworks?.reduce((acc, { id, name }) => { + acc[id] = name; + return acc; + }, {}); + }, + dropdownPlaceholder() { + return renderMultiSelectText( + this.formattedSelectedFrameworkIds, + this.complianceFrameworkItems, + this.$options.i18n.complianceFrameworkTypeName, + ); }, listBoxItems() { return ( @@ -130,10 +157,11 @@ export default { * Only works with ListBox multiple mode * Without multiple prop select method emits single id * and includes method won't work - * @param ids selected ids + * @param ids selected ids in full graphql format */ selectFrameworks(ids) { - this.$emit('select', ids); + const payload = this.useShortIdFormat ? ids.map((id) => getIdFromGraphQLId(id)) : ids; + this.$emit('select', payload); }, onComplianceFrameworkCreated() { this.$refs.formModal.hide(); @@ -158,7 +186,7 @@ export default { :show-select-all-button-label="$options.i18n.selectAllLabel" :toggle-text="dropdownPlaceholder" :title="dropdownPlaceholder" - :selected="selectedFrameworkIds" + :selected="existingFormattedSelectedFrameworkIds" @reset="selectFrameworks([])" @search="debouncedSearch" @select="selectFrameworks" diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/constants.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/constants.js index 2b7e9efa57ec629b51bb1fe0f0abf06dd8d973e6..32c366b4ea08c6735bdd9474cc1f6d9a0b371cc9 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/constants.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/constants.js @@ -20,6 +20,10 @@ export const PROJECT_SCOPE_TYPE_LISTBOX_ITEMS = mapToListBoxItems(PROJECT_SCOPE_ export const WITHOUT_EXCEPTIONS = 'without_exceptions'; export const EXCEPT_PROJECTS = 'except_projects'; +export const INCLUDING = 'including'; +export const EXCLUDING = 'excluding'; +export const COMPLIANCE_FRAMEWORKS_KEY = 'compliance_frameworks'; +export const PROJECTS_KEY = 'projects'; export const EXCEPTION_TYPE_TEXTS = { [WITHOUT_EXCEPTIONS]: s__('SecurityOrchestration|without exceptions'), diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/policy_scope.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue similarity index 63% rename from ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/policy_scope.vue rename to ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue index 5e86b636779b90c4fdcf7d3ae95c3123d7b44af5..d76a244744bdafd8017f4f6f62ee7196a8422161 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/policy_scope.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scope/scope_section.vue @@ -14,6 +14,10 @@ import { SPECIFIC_PROJECTS, EXCEPT_PROJECTS, ALL_PROJECTS_IN_GROUP, + INCLUDING, + EXCLUDING, + COMPLIANCE_FRAMEWORKS_KEY, + PROJECTS_KEY, } from './constants'; export default { @@ -31,7 +35,7 @@ export default { 'SecurityOrchestration|Failed to load compliance frameworks', ), }, - name: 'PolicyScope', + name: 'ScopeSection', components: { GlAlert, GlCollapsibleListbox, @@ -41,17 +45,59 @@ export default { GroupProjectsDropdown, }, inject: ['namespacePath', 'rootNamespacePath'], + props: { + policyScope: { + type: Object, + required: true, + default: () => ({}), + }, + }, data() { + let selectedProjectScopeType = PROJECTS_WITH_FRAMEWORK; + let selectedExceptionType = WITHOUT_EXCEPTIONS; + let projectsPayloadKey = EXCLUDING; + + const { projects = [] } = this.policyScope || {}; + + if (projects?.excluding) { + selectedProjectScopeType = ALL_PROJECTS_IN_GROUP; + selectedExceptionType = EXCEPT_PROJECTS; + } + + if (projects?.including) { + selectedProjectScopeType = SPECIFIC_PROJECTS; + projectsPayloadKey = INCLUDING; + } + return { - selectedProjectScopeType: PROJECTS_WITH_FRAMEWORK, - selectedExceptionType: WITHOUT_EXCEPTIONS, - selectedProjectIds: [], - selectedFrameworkIds: [], + selectedProjectScopeType, + selectedExceptionType, + projectsPayloadKey, showAlert: false, errorDescription: '', }; }, computed: { + projectIds() { + /** + * Protection from manual yam input as objects + * @type {*|*[]} + */ + const projects = Array.isArray(this.policyScope?.projects?.[this.projectsPayloadKey]) + ? this.policyScope?.projects?.[this.projectsPayloadKey] + : []; + return projects?.map(({ id }) => id) || []; + }, + complianceFrameworksIds() { + /** + * Protection from manual yam input as objects + * @type {*|*[]} + */ + const frameworks = Array.isArray(this.policyScope?.compliance_frameworks) + ? this.policyScope?.compliance_frameworks + : []; + return frameworks?.map(({ id }) => id) || []; + }, selectedProjectScopeText() { return PROJECT_SCOPE_TYPE_TEXTS[this.selectedProjectScopeType]; }, @@ -67,6 +113,11 @@ export default { this.selectedProjectScopeType === SPECIFIC_PROJECTS ); }, + payloadKey() { + return this.selectedProjectScopeType === PROJECTS_WITH_FRAMEWORK + ? COMPLIANCE_FRAMEWORKS_KEY + : PROJECTS_KEY; + }, policyScopeCopy() { return this.selectedProjectScopeType === PROJECTS_WITH_FRAMEWORK ? this.$options.i18n.policyScopeFrameworkCopy @@ -74,17 +125,37 @@ export default { }, }, methods: { + resetPolicyScope() { + const internalPayload = + this.payloadKey === COMPLIANCE_FRAMEWORKS_KEY ? [] : { [this.projectsPayloadKey]: [] }; + const payload = { + [this.payloadKey]: internalPayload, + }; + + this.$emit('changed', payload); + }, selectProjectScopeType(scopeType) { this.selectedProjectScopeType = scopeType; + this.projectsPayloadKey = + this.selectedProjectScopeType === ALL_PROJECTS_IN_GROUP ? EXCLUDING : INCLUDING; + this.resetPolicyScope(); }, selectExceptionType(type) { this.selectedExceptionType = type; + this.resetPolicyScope(); }, setSelectedProjectIds(ids) { - this.selectedProjectIds = ids; + const projects = ids.map((id) => ({ id })); + const payload = { projects: { [this.projectsPayloadKey]: projects } }; + + this.triggerChanged(payload); }, setSelectedFrameworkIds(ids) { - this.selectedFrameworkIds = ids; + const payload = ids.map((id) => ({ id })); + this.triggerChanged({ compliance_frameworks: payload }); + }, + triggerChanged(value) { + this.$emit('changed', { ...this.policyScope, ...value }); }, setShowAlert(errorDescription) { this.showAlert = true; @@ -104,6 +175,7 @@ export default { <gl-sprintf :message="policyScopeCopy"> <template #projectScopeType> <gl-collapsible-listbox + data-testid="project-scope-type" :items="$options.PROJECT_SCOPE_TYPE_LISTBOX_ITEMS" :selected="selectedProjectScopeType" :toggle-text="selectedProjectScopeText" @@ -114,7 +186,7 @@ export default { <template #frameworkSelector> <div class="gl-display-inline-flex gl-align-items-center gl-flex-wrap gl-gap-3"> <compliance-framework-dropdown - :selected-framework-ids="selectedFrameworkIds" + :selected-framework-ids="complianceFrameworksIds" :full-path="rootNamespacePath" @framework-query-error=" setShowAlert($options.i18n.complianceFrameworkErrorDescription) @@ -129,6 +201,7 @@ export default { <template #exceptionType> <gl-collapsible-listbox v-if="showExceptionTypeDropdown" + data-testid="exception-type" :items="$options.EXCEPTION_TYPE_LISTBOX_ITEMS" :toggle-text="selectedExceptionTypeText" :selected="selectedExceptionType" @@ -140,7 +213,7 @@ export default { <group-projects-dropdown v-if="showGroupProjectsDropdown" :group-full-path="namespacePath" - :selected-projects-ids="selectedProjectIds" + :selected-projects-ids="projectIds" @projects-query-error="setShowAlert($options.i18n.groupProjectErrorDescription)" @select="setSelectedProjectIds" /> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js index 4866970dbf0ac9f7e562e66d426600b77fa6ce1b..17c045df89e48e61d4d9eeff985645627da513bd 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js @@ -1,4 +1,6 @@ +import { intersection } from 'lodash'; import { isValidCron } from 'cron-validator'; +import { sprintf } from '~/locale'; import createPolicyProject from 'ee/security_orchestration/graphql/mutations/create_policy_project.mutation.graphql'; import createScanExecutionPolicy from 'ee/security_orchestration/graphql/mutations/create_scan_execution_policy.mutation.graphql'; import { gqClient } from 'ee/security_orchestration/utils'; @@ -11,6 +13,9 @@ import { PRIMARY_POLICY_KEYS, RULE_MODE_SCANNERS, SECURITY_POLICY_ACTIONS, + ALL_SELECTED_LABEL, + SELECTED_ITEMS_LABEL, + MULTIPLE_SELECTED_LABEL, } from './constants'; /** @@ -225,3 +230,55 @@ export const hasInvalidCron = (policy) => { }; export const enforceIntValue = (value) => parseInt(value || '0', 10); + +const NO_ITEM_SELECTED = 0; +const ONE_ITEM_SELECTED = 1; + +/** + * + * @param selected items + * @param items items used to render list + * @param itemTypeName + * @returns {*} + */ +export const renderMultiSelectText = (selected, items, itemTypeName) => { + const itemsKeys = Object.keys(items); + + const defaultPlaceholder = sprintf( + SELECTED_ITEMS_LABEL, + { + itemTypeName, + }, + false, + ); + + /** + * Another edge case + * number of selected items and items are equal + * but none of them match + * without this check it would fall through to + * ALL_SELECTED_LABEL + * @type {string[]} + */ + const commonItems = intersection(itemsKeys, selected); + /** + * Edge case for loading states when initial items are empty + */ + if (itemsKeys.length === 0 || commonItems.length === 0) { + return defaultPlaceholder; + } + + switch (commonItems.length) { + case itemsKeys.length: + return sprintf(ALL_SELECTED_LABEL, { itemTypeName }, false); + case NO_ITEM_SELECTED: + return defaultPlaceholder; + case ONE_ITEM_SELECTED: + return items[commonItems[0]] || defaultPlaceholder; + default: + return sprintf(MULTIPLE_SELECTED_LABEL, { + firstLabel: items[commonItems[0]], + numberOfAdditionalLabels: commonItems.length - 1, + }); + } +}; diff --git a/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js b/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js index dbefa4dc7920142e66c52229ec0f9340200c2276..7d19e0ae68958d423cbcab34227432d116f9997e 100644 --- a/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js +++ b/ee/spec/frontend/security_orchestration/components/group_projects_dropdown_spec.js @@ -2,6 +2,8 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMount } from '@vue/test-utils'; import { GlCollapsibleListbox } from '@gitlab/ui'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import getGroupProjects from 'ee/security_orchestration/graphql/queries/get_group_projects.query.graphql'; @@ -12,8 +14,8 @@ describe('GroupProjectsDropdown', () => { let requestHandlers; const defaultNodes = [ - { id: 1, name: '1', fullPath: 'project-1-full-path' }, - { id: 2, name: '2', fullPath: 'project-2-full-path' }, + { id: convertToGraphQLId(TYPENAME_PROJECT, 1), name: '1', fullPath: 'project-1-full-path' }, + { id: convertToGraphQLId(TYPENAME_PROJECT, 2), name: '2', fullPath: 'project-2-full-path' }, ]; const defaultNodesIds = defaultNodes.map(({ id }) => id); @@ -84,14 +86,16 @@ describe('GroupProjectsDropdown', () => { const [{ id }] = defaultNodes; await waitForPromises(); - findDropdown().vm.$emit('select', id); - expect(wrapper.emitted('select')).toEqual([[id]]); + findDropdown().vm.$emit('select', [id]); + expect(wrapper.emitted('select')).toEqual([[[getIdFromGraphQLId(id)]]]); }); it('should select all projects', async () => { await waitForPromises(); selectAllProjects(); - expect(wrapper.emitted('select')).toEqual([[defaultNodesIds]]); + expect(wrapper.emitted('select')).toEqual([ + [defaultNodesIds.map((id) => getIdFromGraphQLId(id))], + ]); }); it('renders default text when loading', () => { @@ -114,7 +118,7 @@ describe('GroupProjectsDropdown', () => { it('renders all projects selected text when', async () => { await waitForPromises(); - expect(findDropdown().props('toggleText')).toBe('All projects selected'); + expect(findDropdown().props('toggleText')).toBe('All projects'); }); it('should reset all projects', async () => { @@ -125,6 +129,32 @@ describe('GroupProjectsDropdown', () => { }); }); + describe('selected projects that does not exist', () => { + it('renders default placeholder when selected projects do not exist', async () => { + createComponent({ + propsData: { + selectedProjectsIds: ['one', 'two'], + }, + }); + + await waitForPromises(); + expect(findDropdown().props('toggleText')).toBe('Select projects'); + }); + + it('filters selected projects that does not exist', async () => { + createComponent({ + propsData: { + selectedProjectsIds: ['one', 'two'], + }, + }); + + await waitForPromises(); + findDropdown().vm.$emit('select', [defaultNodesIds[0]]); + + expect(wrapper.emitted('select')).toEqual([[[getIdFromGraphQLId(defaultNodesIds[0])]]]); + }); + }); + describe('when there is more than a page of projects', () => { describe('when bottom reached on scrolling', () => { it('makes a query to fetch more projects', async () => { @@ -160,4 +190,32 @@ describe('GroupProjectsDropdown', () => { }); }); }); + + describe('full id format', () => { + it('should emit full format of id', async () => { + createComponent({ + propsData: { + useShortIdFormat: false, + }, + }); + + await waitForPromises(); + selectAllProjects(); + + expect(wrapper.emitted('select')).toEqual([[defaultNodesIds]]); + }); + + it('should render selected ids in full format', async () => { + createComponent({ + propsData: { + selectedProjectsIds: defaultNodesIds, + useShortIdFormat: false, + }, + }); + + await waitForPromises(); + + expect(findDropdown().props('selected')).toEqual(defaultNodesIds); + }); + }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/editor_layout_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/editor_layout_spec.js index afd5481b2ce6c824bfb499aebb0a612ab74df937..ac1775b7306bfcfd03bb059d86ebd6d3ec4a2c35 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/editor_layout_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/editor_layout_spec.js @@ -17,7 +17,7 @@ import { import { NAMESPACE_TYPES } from 'ee/security_orchestration/constants'; import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue'; import EditorLayout from 'ee/security_orchestration/components/policy_editor/editor_layout.vue'; -import PolicyScope from 'ee/security_orchestration/components/policy_editor/scope/policy_scope.vue'; +import ScopeSection from 'ee/security_orchestration/components/policy_editor/scope/scope_section.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mockDastScanExecutionObject } from '../../mocks/mock_scan_execution_policy_data'; import { mockDefaultBranchesScanResultObject } from '../../mocks/mock_scan_result_policy_data'; @@ -65,7 +65,7 @@ describe('EditorLayout component', () => { wrapper.findByTestId('scan-result-policy-run-time-info'); const findScanResultPolicyRunTimeTooltip = () => findScanResultPolicyRunTimeInfo().findComponent(GlIcon); - const findPolicyScope = () => wrapper.findComponent(PolicyScope); + const findScopeSection = () => wrapper.findComponent(ScopeSection); describe('default behavior', () => { beforeEach(() => { @@ -314,8 +314,22 @@ describe('EditorLayout component', () => { }, }); - expect(findPolicyScope().exists()).toBe(expectedResult); + expect(findScopeSection().exists()).toBe(expectedResult); }, ); + + it('should set policy properties', () => { + const payload = { policy_scope: { compliance_frameworks: [{ id: 'test' }] } }; + + factory({ + provide: { + namespaceType: NAMESPACE_TYPES.GROUP, + glFeatures: { securityPoliciesPolicyScope: true }, + }, + }); + + findScopeSection().vm.$emit('changed', payload); + expect(wrapper.emitted('set-policy-property')).toEqual([['policy_scope', payload]]); + }); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml_spec.js index 928d552bdf1ac0f1605507b22f6bbf3507d9b415..e3488b0d7fbdbed9b327e2af540237b2d4634ef2 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_execution/lib/from_yaml_spec.js @@ -14,18 +14,23 @@ import { mockInvalidYamlCadenceValue, mockBranchExceptionsScanExecutionObject, mockBranchExceptionsExecutionManifest, + mockPolicyScopeExecutionManifest, + mockPolicyScopeScanExecutionObject, } from 'ee_jest/security_orchestration/mocks/mock_scan_execution_policy_data'; describe('fromYaml', () => { it.each` - title | input | output - ${'returns the policy object for a supported manifest'} | ${{ manifest: mockDastScanExecutionManifest }} | ${mockDastScanExecutionObject} - ${'returns the error object for a policy with an unsupported attribute'} | ${{ manifest: unsupportedManifest, validateRuleMode: true }} | ${{ error: true }} - ${'returns the policy object for a policy with an unsupported attribute when validation is skipped'} | ${{ manifest: unsupportedManifest }} | ${unsupportedManifestObject} - ${'returns error object for a policy with invalid cadence cron string and validation mode'} | ${{ manifest: mockInvalidCadenceScanExecutionObject, validateRuleMode: true }} | ${{ error: true }} - ${'returns error object for a policy with invalid cadence cron string'} | ${{ manifest: mockInvalidYamlCadenceValue }} | ${{ error: true, key: 'yaml-parsing' }} - ${'returns the policy object for branch exceptions'} | ${{ manifest: mockBranchExceptionsExecutionManifest, validateRuleMode: true }} | ${mockBranchExceptionsScanExecutionObject} - `('$title', ({ input, output }) => { + title | input | output | features + ${'returns the policy object for a supported manifest'} | ${{ manifest: mockDastScanExecutionManifest }} | ${mockDastScanExecutionObject} | ${{}} + ${'returns the error object for a policy with an unsupported attribute'} | ${{ manifest: unsupportedManifest, validateRuleMode: true }} | ${{ error: true }} | ${{}} + ${'returns the policy object for a policy with an unsupported attribute when validation is skipped'} | ${{ manifest: unsupportedManifest }} | ${unsupportedManifestObject} | ${{}} + ${'returns error object for a policy with invalid cadence cron string and validation mode'} | ${{ manifest: mockInvalidCadenceScanExecutionObject, validateRuleMode: true }} | ${{ error: true }} | ${{}} + ${'returns error object for a policy with invalid cadence cron string'} | ${{ manifest: mockInvalidYamlCadenceValue }} | ${{ error: true, key: 'yaml-parsing' }} | ${{}} + ${'returns the policy object for branch exceptions'} | ${{ manifest: mockBranchExceptionsExecutionManifest, validateRuleMode: true }} | ${mockBranchExceptionsScanExecutionObject} | ${{}} + ${'returns the policy object for project scope with disabled ff'} | ${{ manifest: mockPolicyScopeExecutionManifest, validateRuleMode: true }} | ${{ error: true }} | ${{ securityPoliciesPolicyScope: false }} + ${'returns the policy object for project scope with enabled ff'} | ${{ manifest: mockPolicyScopeExecutionManifest, validateRuleMode: true }} | ${mockPolicyScopeScanExecutionObject} | ${{ securityPoliciesPolicyScope: true }} + `('$title', ({ input, output, features }) => { + window.gon = { features }; expect(fromYaml(input)).toStrictEqual(output); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/from_yaml_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/from_yaml_spec.js index 6c8bf701b5cb22a06fc37c5a4ed0d5887a9766d2..0f16a671ba0fdd706e0b4e2283c30ea8e9a2024b 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/from_yaml_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result/lib/from_yaml_spec.js @@ -10,6 +10,8 @@ import { mockApprovalSettingsScanResultObject, mockApprovalSettingsPermittedInvalidScanResultManifest, mockApprovalSettingsPermittedInvalidScanResultObject, + mockPolicyScopeScanResultManifest, + mockPolicyScopeScanResultObject, } from 'ee_jest/security_orchestration/mocks/mock_scan_result_policy_data'; import { unsupportedManifest, @@ -97,9 +99,12 @@ describe('createPolicyObject', () => { title | features | input | output ${'returns the policy object for a manifest with `approval_settings` with the `scanResultPoliciesBlockUnprotectingBranches` feature flag on'} | ${{ scanResultPoliciesBlockUnprotectingBranches: true }} | ${mockApprovalSettingsScanResultManifest} | ${{ policy: mockApprovalSettingsScanResultObject, hasParsingError: false }} ${'returns the policy object for a manifest with `approval_settings` containing permitted invalid settings and the `scanResultPoliciesBlockUnprotectingBranches` feature flag on'} | ${{ scanResultPoliciesBlockUnprotectingBranches: true }} | ${mockApprovalSettingsPermittedInvalidScanResultManifest} | ${{ policy: mockApprovalSettingsPermittedInvalidScanResultObject, hasParsingError: false }} + ${'returns the policy object for a manifest with `policy_scope` feature flag on'} | ${{ securityPoliciesPolicyScope: true }} | ${mockPolicyScopeScanResultManifest} | ${{ policy: mockPolicyScopeScanResultObject, hasParsingError: false }} ${'returns the error object for a manifest with `approval_settings` containing permitted invalid settings and the `scanResultPoliciesBlockUnprotectingBranches` feature flag off'} | ${{}} | ${mockApprovalSettingsPermittedInvalidScanResultManifest} | ${{ policy: mockApprovalSettingsPermittedInvalidScanResultObject, hasParsingError: false }} ${'returns the policy object for a manifest with `approval_settings` with the `scanResultAnyMergeRequest` feature flag on'} | ${{ scanResultAnyMergeRequest: true }} | ${mockApprovalSettingsScanResultManifest} | ${{ policy: mockApprovalSettingsScanResultObject, hasParsingError: false }} ${'returns the error object for a manifest with `approval_settings` with all feature flags off'} | ${{}} | ${mockApprovalSettingsScanResultManifest} | ${{ policy: { error: true }, hasParsingError: true }} + ${'returns the error object for a manifest with `approval_settings` with all feature flags off'} | ${{}} | ${mockApprovalSettingsScanResultManifest} | ${{ policy: { error: true }, hasParsingError: true }} + ${'returns the error object for a manifest with `policy_scope` feature flag off'} | ${{}} | ${mockPolicyScopeScanResultManifest} | ${{ policy: { error: true }, hasParsingError: true }} `('$title', ({ features, input, output }) => { window.gon = { features }; expect(createPolicyObject(input)).toStrictEqual(output); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown_spec.js index 7a0450bd32fbf12218548fa2ea2ac14e3390b7c7..15b22e5a5f7db2998fff93b78e58564e72a87237 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown_spec.js @@ -3,6 +3,8 @@ import VueApollo from 'vue-apollo'; import { GlButton, GlCollapsibleListbox, GlModal } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { TYPE_COMPLIANCE_FRAMEWORK } from '~/graphql_shared/constants'; import ComplianceFrameworkDropdown from 'ee/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue'; import ComplianceFrameworkFormModal from 'ee/groups/settings/compliance_frameworks/components/form_modal.vue'; import CreateForm from 'ee/groups/settings/compliance_frameworks/components/create_form.vue'; @@ -22,7 +24,7 @@ describe('ComplianceFrameworkDropdown', () => { const defaultNodes = [ { - id: 1, + id: convertToGraphQLId(TYPE_COMPLIANCE_FRAMEWORK, 1), name: 'A1', default: true, description: 'description 1', @@ -30,7 +32,7 @@ describe('ComplianceFrameworkDropdown', () => { pipelineConfigurationFullPath: 'path 1', }, { - id: 2, + id: convertToGraphQLId(TYPE_COMPLIANCE_FRAMEWORK, 2), name: 'B2', default: false, description: 'description 2', @@ -38,7 +40,7 @@ describe('ComplianceFrameworkDropdown', () => { pipelineConfigurationFullPath: 'path 2', }, { - id: 3, + id: convertToGraphQLId(TYPE_COMPLIANCE_FRAMEWORK, 3), name: 'a3', default: true, description: 'description 3', @@ -131,18 +133,20 @@ describe('ComplianceFrameworkDropdown', () => { const [{ id }] = defaultNodes; await waitForPromises(); - findDropdown().vm.$emit('select', id); - expect(wrapper.emitted('select')).toEqual([[id]]); + findDropdown().vm.$emit('select', [id]); + expect(wrapper.emitted('select')).toEqual([[[getIdFromGraphQLId(id)]]]); }); it('should select all frameworks', async () => { await waitForPromises(); selectAll(); - expect(wrapper.emitted('select')).toEqual([[defaultNodesIds]]); + expect(wrapper.emitted('select')).toEqual([ + [defaultNodesIds.map((id) => getIdFromGraphQLId(id))], + ]); }); it('renders default text when loading', () => { - expect(findDropdown().props('toggleText')).toBe('Choose framework labels'); + expect(findDropdown().props('toggleText')).toBe('Select compliance frameworks'); }); it('should search frameworks despite case', async () => { @@ -192,7 +196,7 @@ describe('ComplianceFrameworkDropdown', () => { handlers: mockApolloHandlers([]), }); await waitForPromises(); - expect(findDropdown().props('toggleText')).toBe('Choose framework labels'); + expect(findDropdown().props('toggleText')).toBe('Select compliance frameworks'); }); }); @@ -212,7 +216,7 @@ describe('ComplianceFrameworkDropdown', () => { it('renders all frameworks selected text', async () => { await waitForPromises(); - expect(findDropdown().props('toggleText')).toBe('All frameworks selected'); + expect(findDropdown().props('toggleText')).toBe('All compliance frameworks'); }); it('should reset all frameworks', async () => { @@ -223,6 +227,32 @@ describe('ComplianceFrameworkDropdown', () => { }); }); + describe('selected frameworks that does not exist', () => { + it('renders default placeholder when selected frameworks do not exist', async () => { + createComponent({ + propsData: { + selectedFrameworkIds: ['one', 'two'], + }, + }); + + await waitForPromises(); + expect(findDropdown().props('toggleText')).toBe('Select compliance frameworks'); + }); + + it('filters selected frameworks that does not exist', async () => { + createComponent({ + propsData: { + selectedFrameworkIds: ['one', 'two'], + }, + }); + + await waitForPromises(); + findDropdown().vm.$emit('select', [defaultNodesIds[0]]); + + expect(wrapper.emitted('select')).toEqual([[[getIdFromGraphQLId(defaultNodesIds[0])]]]); + }); + }); + describe('one selected project', () => { it('should render text for selected framework', async () => { createComponent({ @@ -232,7 +262,7 @@ describe('ComplianceFrameworkDropdown', () => { }); await waitForPromises(); - expect(findDropdown().props('toggleText')).toBe('1 compliance framework selected'); + expect(findDropdown().props('toggleText')).toBe(defaultNodes[0].name); }); }); @@ -265,4 +295,32 @@ describe('ComplianceFrameworkDropdown', () => { expect(findDropdown().props('category')).toBe(category); }); }); + + describe('full id format', () => { + it('should emit full format of id', async () => { + createComponent({ + propsData: { + useShortIdFormat: false, + }, + }); + + await waitForPromises(); + selectAll(); + + expect(wrapper.emitted('select')).toEqual([[defaultNodesIds]]); + }); + + it('should render selected ids in full format', async () => { + createComponent({ + propsData: { + selectedFrameworkIds: defaultNodesIds, + useShortIdFormat: false, + }, + }); + + await waitForPromises(); + + expect(findDropdown().props('selected')).toEqual(defaultNodesIds); + }); + }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c64c9305c8b1643e2f4edbc772852a34f48e64e5 --- /dev/null +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scope/scope_section_spec.js @@ -0,0 +1,223 @@ +import { GlAlert, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ScopeSection from 'ee/security_orchestration/components/policy_editor/scope/scope_section.vue'; +import ComplianceFrameworkDropdown from 'ee/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue'; +import GroupProjectsDropdown from 'ee/security_orchestration/components/group_projects_dropdown.vue'; +import { + PROJECTS_WITH_FRAMEWORK, + ALL_PROJECTS_IN_GROUP, + SPECIFIC_PROJECTS, + EXCEPT_PROJECTS, +} from 'ee/security_orchestration/components/policy_editor/scope/constants'; + +describe('PolicyScope', () => { + let wrapper; + + const createComponent = ({ propsData } = {}) => { + wrapper = shallowMountExtended(ScopeSection, { + propsData: { + policyScope: {}, + ...propsData, + }, + provide: { + namespacePath: 'gitlab-org', + rootNamespacePath: 'gitlab-org', + }, + stubs: { + GlSprintf, + }, + }); + }; + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findComplianceFrameworkDropdown = () => wrapper.findComponent(ComplianceFrameworkDropdown); + const findGroupProjectsDropdown = () => wrapper.findComponent(GroupProjectsDropdown); + const findProjectScopeTypeDropdown = () => wrapper.findByTestId('project-scope-type'); + const findExceptionTypeDropdown = () => wrapper.findByTestId('exception-type'); + + beforeEach(() => { + createComponent(); + }); + + it('should render framework dropdown in initial state', () => { + expect(findProjectScopeTypeDropdown().props('selected')).toBe(PROJECTS_WITH_FRAMEWORK); + expect(findComplianceFrameworkDropdown().exists()).toBe(true); + + expect(findExceptionTypeDropdown().exists()).toBe(false); + expect(findGroupProjectsDropdown().exists()).toBe(false); + expect(findGlAlert().exists()).toBe(false); + }); + + it('should change scope and reset it', async () => { + await findProjectScopeTypeDropdown().vm.$emit('select', ALL_PROJECTS_IN_GROUP); + + expect(findComplianceFrameworkDropdown().exists()).toBe(false); + + expect(findExceptionTypeDropdown().exists()).toBe(true); + expect(findGroupProjectsDropdown().exists()).toBe(false); + expect(wrapper.emitted('changed')).toEqual([ + [ + { + projects: { + excluding: [], + }, + }, + ], + ]); + + await findProjectScopeTypeDropdown().vm.$emit('select', SPECIFIC_PROJECTS); + + expect(findExceptionTypeDropdown().exists()).toBe(false); + expect(findGroupProjectsDropdown().exists()).toBe(true); + expect(wrapper.emitted('changed')).toEqual([ + [ + { + projects: { + excluding: [], + }, + }, + ], + [ + { + projects: { + including: [], + }, + }, + ], + ]); + }); + + it('should select excluding projects', async () => { + await findProjectScopeTypeDropdown().vm.$emit('select', ALL_PROJECTS_IN_GROUP); + + expect(findGroupProjectsDropdown().exists()).toBe(false); + + await findExceptionTypeDropdown().vm.$emit('select', EXCEPT_PROJECTS); + + expect(findGroupProjectsDropdown().exists()).toBe(true); + + findGroupProjectsDropdown().vm.$emit('select', ['id1', 'id2']); + + expect(wrapper.emitted('changed')).toEqual([ + [ + { + projects: { + excluding: [], + }, + }, + ], + [ + { + projects: { + excluding: [], + }, + }, + ], + [{ projects: { excluding: [{ id: 'id1' }, { id: 'id2' }] } }], + ]); + }); + + it('should select including projects', async () => { + await findProjectScopeTypeDropdown().vm.$emit('select', SPECIFIC_PROJECTS); + + expect(findGroupProjectsDropdown().exists()).toBe(true); + + findGroupProjectsDropdown().vm.$emit('select', ['id1', 'id2']); + + expect(wrapper.emitted('changed')).toEqual([ + [ + { + projects: { + including: [], + }, + }, + ], + [{ projects: { including: [{ id: 'id1' }, { id: 'id2' }] } }], + ]); + }); + + it('should select compliance frameworks', () => { + findComplianceFrameworkDropdown().vm.$emit('select', ['id1', 'id2']); + + expect(wrapper.emitted('changed')).toEqual([ + [{ compliance_frameworks: [{ id: 'id1' }, { id: 'id2' }] }], + ]); + }); + + describe('existing policy scope', () => { + it('should render existing compliance frameworks', () => { + createComponent({ + propsData: { + policyScope: { + compliance_frameworks: [{ id: 'id1' }, { id: 'id2' }], + }, + }, + }); + + expect(findComplianceFrameworkDropdown().exists()).toBe(true); + expect(findComplianceFrameworkDropdown().props('selectedFrameworkIds')).toEqual([ + 'id1', + 'id2', + ]); + + expect(findExceptionTypeDropdown().exists()).toBe(false); + expect(findGroupProjectsDropdown().exists()).toBe(false); + }); + + it('should render existing excluding projects', () => { + createComponent({ + propsData: { + policyScope: { + projects: { + excluding: [{ id: 'id1' }, { id: 'id2' }], + }, + }, + }, + }); + + expect(findComplianceFrameworkDropdown().exists()).toBe(false); + + expect(findExceptionTypeDropdown().props('selected')).toBe(EXCEPT_PROJECTS); + expect(findExceptionTypeDropdown().exists()).toBe(true); + expect(findGroupProjectsDropdown().exists()).toBe(true); + expect(findGroupProjectsDropdown().props('selectedProjectsIds')).toEqual(['id1', 'id2']); + }); + + it('should render existing including projects', () => { + createComponent({ + propsData: { + policyScope: { + projects: { + including: [{ id: 'id1' }, { id: 'id2' }], + }, + }, + }, + }); + + expect(findComplianceFrameworkDropdown().exists()).toBe(false); + expect(findExceptionTypeDropdown().exists()).toBe(false); + expect(findGroupProjectsDropdown().exists()).toBe(true); + expect(findGroupProjectsDropdown().props('selectedProjectsIds')).toEqual(['id1', 'id2']); + }); + + it('should render alert message for projects dropdown', async () => { + createComponent({ + propsData: { + policyScope: { + projects: { + including: ['id1', 'id2'], + }, + }, + }, + }); + + await findGroupProjectsDropdown().vm.$emit('projects-query-error'); + expect(findGlAlert().exists()).toBe(true); + }); + + it('should render alert message for compliance framework dropdown', async () => { + await findComplianceFrameworkDropdown().vm.$emit('framework-query-error'); + expect(findGlAlert().exists()).toBe(true); + }); + }); +}); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js index 571148fc7b5d562e38eb36540116679317627f55..4c9a4dd03ea37ef9dbd099ebdf36118893034810 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js @@ -6,6 +6,7 @@ import { hasInvalidCron, slugify, slugifyToArray, + renderMultiSelectText, } from 'ee/security_orchestration/components/policy_editor/utils'; import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/security_orchestration/constants'; import createPolicyProject from 'ee/security_orchestration/graphql/mutations/create_policy_project.mutation.graphql'; @@ -207,3 +208,18 @@ describe('slugifyToArray', () => { ); }); }); + +describe('renderMultiSelectText', () => { + it.each` + selected | items | expectedText + ${[]} | ${{}} | ${'Select projects'} + ${['project1']} | ${{ project1: 'project 1', project2: 'project 2' }} | ${'project 1'} + ${['project1', 'project2']} | ${{ project1: 'project 1', project2: 'project 2' }} | ${'All projects'} + ${['project1', 'project2']} | ${{ project1: 'project 1', project2: 'project 2', project3: 'project 3' }} | ${'project 1 +1 more'} + ${[]} | ${{ project1: 'project 1', project2: 'project 2', project3: 'project 3' }} | ${'Select projects'} + ${['project4', 'project5']} | ${{ project1: 'project 1', project2: 'project 2', project3: 'project 3' }} | ${'Select projects'} + ${['project4', 'project5']} | ${{ project2: 'project 2', project3: 'project 3' }} | ${'Select projects'} + `('should render correct selection text', ({ selected, items, expectedText }) => { + expect(renderMultiSelectText(selected, items, 'projects')).toBe(expectedText); + }); +}); diff --git a/ee/spec/frontend/security_orchestration/mocks/mock_scan_execution_policy_data.js b/ee/spec/frontend/security_orchestration/mocks/mock_scan_execution_policy_data.js index 7d7060b0700bbb042e6cb4a30d8d5bbf7ee882eb..9b3d614899d383a030c3a7306c3d6cbeb7d9bfbb 100644 --- a/ee/spec/frontend/security_orchestration/mocks/mock_scan_execution_policy_data.js +++ b/ee/spec/frontend/security_orchestration/mocks/mock_scan_execution_policy_data.js @@ -211,3 +211,38 @@ export const mockInvalidCadenceScanExecutionObject = { }, ], }; + +export const mockPolicyScopeExecutionManifest = `type: scan_execution_policy +name: Project scope +description: This policy enforces policy scope +enabled: false +rules: + - type: pipeline + branches: + - main +actions: + - scan: container_scanning +policy_scope: + compliance_frameworks: [] +`; + +export const mockPolicyScopeScanExecutionObject = { + type: 'scan_execution_policy', + name: 'Project scope', + enabled: false, + description: 'This policy enforces policy scope', + rules: [ + { + type: 'pipeline', + branches: ['main'], + }, + ], + actions: [ + { + scan: 'container_scanning', + }, + ], + policy_scope: { + compliance_frameworks: [], + }, +}; diff --git a/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js b/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js index 3d0aac9aea837c847e0359d292da9759ca930556..9a50d55a94f3a5531cd693b810a253d06d28a24d 100644 --- a/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js +++ b/ee/spec/frontend/security_orchestration/mocks/mock_scan_result_policy_data.js @@ -181,6 +181,29 @@ approval_settings: enabled: true `; +export const mockPolicyScopeScanResultManifest = `type: scan_result_policy +name: policy scope +description: This policy enforces policy scope +enabled: true +rules: [] +actions: [] +policy_scope: + compliance_frameworks: + - id: 26 +`; + +export const mockPolicyScopeScanResultObject = { + type: 'scan_result_policy', + name: 'policy scope', + description: 'This policy enforces policy scope', + enabled: true, + rules: [], + actions: [], + policy_scope: { + compliance_frameworks: [{ id: 26 }], + }, +}; + export const mockApprovalSettingsScanResultObject = { type: 'scan_result_policy', name: 'critical vulnerability CS approvals', diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 48896fb3e1d99a8068f9f53dcc5f414cec2fc6c4..3b8701619a7c919bec6a1435275d1000291a7ffc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -237,11 +237,6 @@ msgid_plural "%d completed issues" msgstr[0] "" msgstr[1] "" -msgid "%d compliance framework selected" -msgid_plural "%d compliance frameworks selected" -msgstr[0] "" -msgstr[1] "" - msgid "%d contribution" msgid_plural "%d contributions" msgstr[0] "" @@ -4799,9 +4794,6 @@ msgstr "" msgid "All environments" msgstr "" -msgid "All frameworks selected" -msgstr "" - msgid "All groups" msgstr "" @@ -43133,9 +43125,6 @@ msgstr "" msgid "SecurityOrchestration|Choose approver type" msgstr "" -msgid "SecurityOrchestration|Choose framework labels" -msgstr "" - msgid "SecurityOrchestration|Choose specific role" msgstr "" @@ -43606,6 +43595,9 @@ msgstr "" msgid "SecurityOrchestration|by the agent named %{agents} %{cadence}%{branchExceptionsString}" msgstr "" +msgid "SecurityOrchestration|compliance frameworks" +msgstr "" + msgid "SecurityOrchestration|except projects" msgstr ""