diff --git a/ee/app/assets/javascripts/security_orchestration/components/policies/experiment_features_banner.vue b/ee/app/assets/javascripts/security_orchestration/components/policies/experiment_features_banner.vue new file mode 100644 index 0000000000000000000000000000000000000000..7bd033b3fea1bf918e9fdfb98dfacbfea01f02cd --- /dev/null +++ b/ee/app/assets/javascripts/security_orchestration/components/policies/experiment_features_banner.vue @@ -0,0 +1,71 @@ +<script> +import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui'; +import shieldCheckIllustrationUrl from '@gitlab/svgs/dist/illustrations/secure-sm.svg?url'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { s__ } from '~/locale'; + +export default { + EXPERIMENTAL_FEATURES_PATH: helpPagePath( + 'user/application_security/policies/scan-execution-policies', + { + anchor: 'experimental-features', + }, + ), + SHARE_FEEDBACK_ATTRIBUTES: { target: '_blank' }, + SHARE_FEEDBACK_URL: 'https://gitlab.com/gitlab-org/gitlab/-/issues/434425', + BANNER_STORAGE_KEY: 'security_policies_experimental_features', + SVG_PATH: shieldCheckIllustrationUrl, + name: 'ExperimentFeaturesBanner', + components: { + GlBanner, + GlLink, + GlSprintf, + LocalStorageSync, + }, + i18n: { + buttonBannerText: s__('SecurityOrchestration|Share feedback'), + bannerTitle: s__( + 'SecurityOrchestration|Introducing Policy Scoping and Pipeline Execution Policy Action experimental features', + ), + bannerDescription: s__( + 'SecurityOrchestration|Granularly scope your policies to selected projects. Enforce custom CI with pipeline execution action for scan execution policies. %{linkStart}How do I implement these features?%{linkEnd}', + ), + }, + data() { + return { + feedbackBannerDismissed: false, + }; + }, + methods: { + dismissBanner() { + this.feedbackBannerDismissed = true; + }, + }, +}; +</script> + +<template> + <local-storage-sync v-model="feedbackBannerDismissed" :storage-key="$options.BANNER_STORAGE_KEY"> + <gl-banner + v-if="!feedbackBannerDismissed" + :button-attributes="$options.SHARE_FEEDBACK_ATTRIBUTES" + :button-link="$options.SHARE_FEEDBACK_URL" + :button-text="$options.i18n.buttonBannerText" + :title="$options.i18n.bannerTitle" + :svg-path="$options.SVG_PATH" + @close="dismissBanner" + > + <p> + <gl-sprintf :message="$options.i18n.bannerDescription"> + <template #link="{ content }"> + <gl-link :href="$options.EXPERIMENTAL_FEATURES_PATH" target="_blank"> + <p class="gl-mb-0">{{ content }}</p> + </gl-link> + </template> + </gl-sprintf> + </p> + </gl-banner> + </local-storage-sync> +</template> +> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policies/list_header.vue b/ee/app/assets/javascripts/security_orchestration/components/policies/list_header.vue index 9fda427ae92578e1620e42dc03ff8eb2b2f827ca..8c1c67bff26863ef964c8646bbae2e0c03a5701a 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policies/list_header.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policies/list_header.vue @@ -2,17 +2,21 @@ import { GlAlert, GlButton, GlIcon, GlSprintf } from '@gitlab/ui'; import { joinPaths } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { NEW_POLICY_BUTTON_TEXT } from '../constants'; +import ExperimentFeaturesBanner from './experiment_features_banner.vue'; import ProjectModal from './project_modal.vue'; export default { components: { + ExperimentFeaturesBanner, GlAlert, GlButton, GlIcon, GlSprintf, ProjectModal, }, + mixins: [glFeatureFlagMixin()], inject: [ 'assignedPolicyProject', 'disableSecurityPolicyProject', @@ -39,6 +43,11 @@ export default { }; }, computed: { + feedbackBannerEnabled() { + return ( + this.glFeatures.securityPoliciesPolicyScope || this.glFeatures.compliancePipelineInPolicies + ); + }, hasAssignedPolicyProject() { return Boolean(this.assignedPolicyProject?.id); }, @@ -83,48 +92,58 @@ export default { > {{ alertText }} </gl-alert> - <header class="gl-my-6 gl-display-flex gl-align-items-flex-start"> - <div class="gl-flex-grow-1 gl-my-0"> - <h2 class="gl-mt-0"> - {{ $options.i18n.title }} - </h2> - <p data-testid="policies-subheader"> - <gl-sprintf :message="$options.i18n.subtitle"> - <template #link="{ content }"> - <gl-button class="gl-pb-1!" variant="link" :href="documentationPath" target="_blank"> - {{ content }} - </gl-button> - </template> - </gl-sprintf> - </p> + <header class="gl-my-6 gl-display-flex gl-flex-direction-column"> + <div class="gl-display-flex gl-align-items-flex-start"> + <div class="gl-flex-grow-1 gl-my-0"> + <h2 class="gl-mt-0"> + {{ $options.i18n.title }} + </h2> + <p data-testid="policies-subheader"> + <gl-sprintf :message="$options.i18n.subtitle"> + <template #link="{ content }"> + <gl-button + class="gl-pb-1!" + variant="link" + :href="documentationPath" + target="_blank" + > + {{ content }} + </gl-button> + </template> + </gl-sprintf> + </p> + </div> + <gl-button + v-if="!disableSecurityPolicyProject" + data-testid="edit-project-policy-button" + class="gl-mr-4" + :loading="projectIsBeingLinked" + @click="showNewPolicyModal" + > + {{ $options.i18n.editPolicyProjectButtonText }} + </gl-button> + <gl-button + v-else-if="hasAssignedPolicyProject" + data-testid="view-project-policy-button" + class="gl-mr-3" + target="_blank" + :href="securityPolicyProjectPath" + > + <gl-icon name="external-link" /> + {{ $options.i18n.viewPolicyProjectButtonText }} + </gl-button> + <gl-button + v-if="!disableScanPolicyUpdate" + data-testid="new-policy-button" + variant="confirm" + :href="newPolicyPath" + > + {{ $options.i18n.newPolicyButtonText }} + </gl-button> </div> - <gl-button - v-if="!disableSecurityPolicyProject" - data-testid="edit-project-policy-button" - class="gl-mr-4" - :loading="projectIsBeingLinked" - @click="showNewPolicyModal" - > - {{ $options.i18n.editPolicyProjectButtonText }} - </gl-button> - <gl-button - v-else-if="hasAssignedPolicyProject" - data-testid="view-project-policy-button" - class="gl-mr-3" - target="_blank" - :href="securityPolicyProjectPath" - > - <gl-icon name="external-link" /> - {{ $options.i18n.viewPolicyProjectButtonText }} - </gl-button> - <gl-button - v-if="!disableScanPolicyUpdate" - data-testid="new-policy-button" - variant="confirm" - :href="newPolicyPath" - > - {{ $options.i18n.newPolicyButtonText }} - </gl-button> + + <experiment-features-banner v-if="feedbackBannerEnabled" /> + <project-modal :visible="modalVisible" @close="modalVisible = false" diff --git a/ee/spec/frontend/security_orchestration/components/policies/experiment_features_banner_spec.js b/ee/spec/frontend/security_orchestration/components/policies/experiment_features_banner_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7c2f1bc947b8b27f6882d74cbe25f93c71115b2c --- /dev/null +++ b/ee/spec/frontend/security_orchestration/components/policies/experiment_features_banner_spec.js @@ -0,0 +1,46 @@ +import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui'; +import ExperimentFeaturesBanner from 'ee/security_orchestration/components/policies/experiment_features_banner.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('ExperimentFeaturesBanner', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ExperimentFeaturesBanner, { + stubs: { + GlBanner, + GlSprintf, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + const findBanner = () => wrapper.findComponent(GlBanner); + const findLink = () => wrapper.findComponent(GlLink); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findPrimaryBannerButton = () => wrapper.findByTestId('gl-banner-primary-button'); + + it('renders banner with links', () => { + expect(findLocalStorageSync().exists()).toBe(true); + expect(findBanner().exists()).toBe(true); + expect(findBanner().text()).toContain( + 'Introducing Policy Scoping and Pipeline Execution Policy Action experimental features', + ); + expect(findPrimaryBannerButton().attributes('href')).toBe( + 'https://gitlab.com/gitlab-org/gitlab/-/issues/434425', + ); + expect(findLink().attributes('href')).toBe( + '/help/user/application_security/policies/scan-execution-policies#experimental-features', + ); + }); + + it('dismisses the banner', async () => { + await findBanner().vm.$emit('close'); + + expect(findBanner().exists()).toBe(false); + }); +}); diff --git a/ee/spec/frontend/security_orchestration/components/policies/list_header_spec.js b/ee/spec/frontend/security_orchestration/components/policies/list_header_spec.js index f8cc0b4bc9e4b1c4f43d92fb27d9550a70585e9e..32bdd8af1ae8a518b788143c52aff68f3485eda5 100644 --- a/ee/spec/frontend/security_orchestration/components/policies/list_header_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policies/list_header_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui'; import { nextTick } from 'vue'; +import ExperimentFeaturesBanner from 'ee/security_orchestration/components/policies/experiment_features_banner.vue'; import ListHeader from 'ee/security_orchestration/components/policies/list_header.vue'; import ProjectModal from 'ee/security_orchestration/components/policies/project_modal.vue'; import { NEW_POLICY_BUTTON_TEXT } from 'ee/security_orchestration/components/constants'; @@ -20,6 +21,7 @@ describe('List Header Component', () => { const findViewPolicyProjectButton = () => wrapper.findByTestId('view-project-policy-button'); const findNewPolicyButton = () => wrapper.findByTestId('new-policy-button'); const findSubheader = () => wrapper.findByTestId('policies-subheader'); + const findExperimentFeaturesBanner = () => wrapper.findComponent(ExperimentFeaturesBanner); const linkSecurityPoliciesProject = async () => { findScanNewPolicyModal().vm.$emit('project-updated', { @@ -55,6 +57,7 @@ describe('List Header Component', () => { expect(findNewPolicyButton().exists()).toBe(true); expect(findNewPolicyButton().text()).toBe(NEW_POLICY_BUTTON_TEXT); expect(findNewPolicyButton().attributes('href')).toBe(newPolicyPath); + expect(findExperimentFeaturesBanner().exists()).toBe(false); }); it.each` @@ -154,4 +157,28 @@ describe('List Header Component', () => { }); }); }); + + describe('experiments promotion banner', () => { + it.each` + securityPoliciesPolicyScope | compliancePipelineInPolicies | expectedResult + ${true} | ${true} | ${true} + ${true} | ${false} | ${true} + ${false} | ${true} | ${true} + ${false} | ${false} | ${false} + `( + 'renders experiments promotion banner', + ({ securityPoliciesPolicyScope, compliancePipelineInPolicies, expectedResult }) => { + createWrapper({ + provide: { + glFeatures: { + securityPoliciesPolicyScope, + compliancePipelineInPolicies, + }, + }, + }); + + expect(findExperimentFeaturesBanner().exists()).toBe(expectedResult); + }, + ); + }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 020c54ca136ecde15779fd0204beeccba567825b..a9ab99f76dfb65a849c1d0a5c125ea9609d77bb8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -44613,6 +44613,9 @@ msgstr "" msgid "SecurityOrchestration|For large groups, there may be a significant delay in applying policy changes to pre-existing merge requests. Policy changes typically apply almost immediately for newly created merge requests." msgstr "" +msgid "SecurityOrchestration|Granularly scope your policies to selected projects. Enforce custom CI with pipeline execution action for scan execution policies. %{linkStart}How do I implement these features?%{linkEnd}" +msgstr "" + msgid "SecurityOrchestration|Groups" msgstr "" @@ -44634,6 +44637,9 @@ msgstr "" msgid "SecurityOrchestration|Inherited from %{namespace}" msgstr "" +msgid "SecurityOrchestration|Introducing Policy Scoping and Pipeline Execution Policy Action experimental features" +msgstr "" + msgid "SecurityOrchestration|Invalid Compliance Framework ID(s)" msgstr "" @@ -44846,6 +44852,9 @@ msgstr "" msgid "SecurityOrchestration|Severity is %{severity}." msgstr "" +msgid "SecurityOrchestration|Share feedback" +msgstr "" + msgid "SecurityOrchestration|Show all included projects" msgstr ""