Commit 3c70b090 authored by Jannik Lehmann's avatar Jannik Lehmann Committed by Kushal Pandya

Add Sbom Survey Banner

parent 06d96858
---
name: sbom_survey
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76446
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348181
milestone: '14.6'
type: development
group: group::secure
default_enabled: false
......@@ -2,6 +2,7 @@
import { GlEmptyState, GlIcon, GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { __ } from '~/locale';
import SbomBanner from 'ee/sbom_banner/components/app.vue';
import { DEPENDENCY_LIST_TYPES } from '../store/constants';
import { REPORT_STATUS } from '../store/modules/list/constants';
import DependenciesActions from './dependencies_actions.vue';
......@@ -18,6 +19,7 @@ export default {
GlLoadingIcon,
GlSprintf,
GlLink,
SbomBanner,
DependencyListIncompleteAlert,
DependencyListJobFailedAlert,
PaginatedDependenciesTable,
......@@ -27,6 +29,10 @@ export default {
type: String,
required: true,
},
sbomSurveySvgPath: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
......@@ -133,6 +139,7 @@ export default {
</gl-empty-state>
<section v-else>
<sbom-banner :sbom-survey-svg-path="sbomSurveySvgPath" />
<dependency-list-incomplete-alert
v-if="isIncomplete && !isIncompleteAlertDismissed"
@dismiss="dismissIncompleteListAlert"
......
......@@ -4,16 +4,23 @@ import createStore from './store';
export default () => {
const el = document.querySelector('#js-dependencies-app');
const { endpoint, emptyStateSvgPath, documentationPath, supportDocumentationPath } = el.dataset;
const {
endpoint,
emptyStateSvgPath,
documentationPath,
supportDocumentationPath,
sbomSurveySvgPath,
} = el.dataset;
const store = createStore();
return new Vue({
el,
store,
components: {
DependenciesApp,
},
store,
render(createElement) {
return createElement(DependenciesApp, {
props: {
......@@ -21,6 +28,7 @@ export default () => {
emptyStateSvgPath,
documentationPath,
supportDocumentationPath,
sbomSurveySvgPath,
},
});
},
......
......@@ -11,6 +11,7 @@ import {
} from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import LicenseManagement from 'ee/vue_shared/license_compliance/license_management.vue';
import SbomBanner from 'ee/sbom_banner/components/app.vue';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import { getLocationHash } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......@@ -31,6 +32,7 @@ export default {
GlTabs,
GlBadge,
GlAlert,
SbomBanner,
LicenseManagement,
},
mixins: [glFeatureFlagsMixin()],
......@@ -39,6 +41,10 @@ export default {
type: String,
required: true,
},
sbomSurveySvgPath: {
type: String,
required: true,
},
documentationPath: {
type: String,
required: true,
......@@ -119,7 +125,7 @@ export default {
)
}}
</gl-alert>
<sbom-banner :sbom-survey-svg-path="sbomSurveySvgPath" />
<header class="my-3">
<h2 class="h4 mb-1 gl-display-flex gl-align-items-center">
{{ s__('Licenses|License Compliance') }}
......
......@@ -18,6 +18,7 @@ export default () => {
approvalsDocumentationPath,
lockedApprovalsRuleName,
softwareLicenses,
sbomSurveySvgPath,
} = el.dataset;
const storeSettings = {
......@@ -47,6 +48,7 @@ export default () => {
render(createElement) {
return createElement(LicenseComplianceApp, {
props: {
sbomSurveySvgPath,
emptyStateSvgPath,
documentationPath,
},
......
<script>
import {
SBOM_BANNER_LOCAL_STORAGE_KEY,
SBOM_BANNER_CURRENT_ID,
SBOM_SURVEY_LINK,
SBOM_SURVEY_DAYS_TO_ASK_LATER,
SBOM_SURVEY_TITLE,
SBOM_SURVEY_BUTTON_TEXT,
SBOM_SURVEY_DESCRIPTION,
SBOM_SURVEY_TOAST_MESSAGE,
} from 'ee/vue_shared/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SurveyBanner from 'ee/vue_shared/survey_banner/survey_banner.vue';
export default {
name: 'SbomBanner',
components: {
SurveyBanner,
},
mixins: [glFeatureFlagsMixin()],
props: {
sbomSurveySvgPath: {
type: String,
required: true,
},
},
computed: {
shouldShowSbomSurvey() {
return this.glFeatures.sbomSurvey;
},
},
storageKey: SBOM_BANNER_LOCAL_STORAGE_KEY,
bannerId: SBOM_BANNER_CURRENT_ID,
surveyLink: SBOM_SURVEY_LINK,
daysToAskLater: SBOM_SURVEY_DAYS_TO_ASK_LATER,
title: SBOM_SURVEY_TITLE,
buttonText: SBOM_SURVEY_BUTTON_TEXT,
description: SBOM_SURVEY_DESCRIPTION,
toastMessage: SBOM_SURVEY_TOAST_MESSAGE,
};
</script>
<template>
<survey-banner
v-if="shouldShowSbomSurvey"
:svg-path="sbomSurveySvgPath"
:survey-link="$options.surveyLink"
:days-to-ask-later="$options.daysToAskLater"
:title="$options.title"
:button-text="$options.buttonText"
:description="$options.description"
:toast-message="$options.toastMessage"
:storage-key="$options.storageKey"
:banner-id="$options.bannerId"
class="gl-mt-5"
/>
</template>
......@@ -51,7 +51,7 @@ export default {
</script>
<template>
<security-dashboard-layout>
<security-dashboard-layout :show-sbom-survey="false">
<template v-if="shouldShowEmptyState" #empty-state>
<report-not-configured />
</template>
......
<script>
import { s__ } from '~/locale';
import SbomBanner from 'ee/sbom_banner/components/app.vue';
import SurveyRequestBanner from './survey_request_banner.vue';
export default {
components: { SurveyRequestBanner },
components: { SurveyRequestBanner, SbomBanner },
i18n: {
title: s__('SecurityReports|Security Dashboard'),
},
inject: ['sbomSurveySvgPath'],
props: {
// this prop is needed since the sbom survey banner should not be shown
// on the instance security dashboard
showSbomSurvey: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
<template>
<div>
<slot name="loading"></slot>
<!-- TODO: this component needs to be refactored to use the shared survey-banner component, tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/348190 -->
<survey-request-banner v-if="!$slots.loading" class="gl-mt-5" />
<sbom-banner
v-if="!$slots.loading && showSbomSurvey"
:sbom-survey-svg-path="sbomSurveySvgPath"
/>
<template v-if="$slots.default">
<h2 data-testid="title">{{ $options.i18n.title }}</h2>
<div class="security-charts gl-display-flex gl-flex-wrap">
......
......@@ -121,6 +121,7 @@ export default {
<template>
<div>
<template v-if="!isDashboardConfigured">
<!-- TODO: this component needs to be refactored to use the shared survey-banner component, tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/348190 -->
<survey-request-banner v-if="shouldShowSurvey" class="gl-mt-5" />
<report-not-configured-group v-if="isGroup" />
<report-not-configured-instance v-else-if="isInstance" />
......@@ -134,6 +135,7 @@ export default {
/>
<vulnerability-report-layout>
<template v-if="!isPipeline" #header>
<!-- TODO: this component needs to be refactored to use the shared survey-banner component, tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/348190 -->
<survey-request-banner class="gl-mt-5" />
<header class="gl-mt-6 gl-mb-3 gl-display-flex gl-align-items-center">
<h2 class="gl-flex-grow-1 gl-my-0">
......
......@@ -58,6 +58,7 @@ export default {
<template>
<div>
<!-- TODO: this component needs to be refactored to use the shared survey-banner component, tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/348190 -->
<survey-request-banner class="gl-mt-5" />
<vulnerability-report-header />
......
......@@ -36,6 +36,7 @@ export default (el, dashboardType) => {
securityConfigurationPath: el.dataset.securityConfigurationPath,
surveyRequestSvgPath: el.dataset.surveyRequestSvgPath,
securityDashboardHelpPath: el.dataset.securityDashboardHelpPath,
sbomSurveySvgPath: el.dataset.sbomSurveySvgPath,
};
let component;
......
import { __ } from '~/locale';
import { s__, __ } from '~/locale';
export const noneEpic = {
id: 0,
......@@ -9,3 +9,18 @@ export const placeholderEpic = {
id: -1,
title: __('Select epic'),
};
export const SBOM_BANNER_LOCAL_STORAGE_KEY = 'sbom_survey_request';
// NOTE: This string needs to parse to an invalid date. Do not put any characters in between the
// word 'survey' and the number, or else it will parse to a valid date.
export const SBOM_BANNER_CURRENT_ID = 'sbom1';
export const SBOM_SURVEY_LINK = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_es038rUv1VFqmXk';
export const SBOM_SURVEY_DAYS_TO_ASK_LATER = 7;
export const SBOM_SURVEY_TITLE = s__('SecurityReports|Security Dashboard');
export const SBOM_SURVEY_BUTTON_TEXT = s__('SecurityReports|Take survey');
export const SBOM_SURVEY_DESCRIPTION = s__(
`SecurityReports|The Composition Analysis group is planning significant updates to how we make available the list of software and container dependency information in your projects. Therefore, we ask that you assist us by taking a short -no longer than 5 minute- survey to help align our direction with your needs.`,
);
export const SBOM_SURVEY_TOAST_MESSAGE = s__(
'SecurityReports|Your feedback is important to us! We will ask again in 7 days.',
);
<script>
import { GlButton, GlBanner, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import showToast from '~/vue_shared/plugins/global_toast';
export default {
components: { GlButton, GlBanner, GlSprintf, LocalStorageSync },
props: {
surveyLink: {
type: String,
required: true,
},
daysToAskLater: {
type: Number,
required: true,
},
title: {
type: String,
required: true,
},
buttonText: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
toastMessage: {
type: String,
required: true,
},
storageKey: {
type: String,
required: true,
},
bannerId: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
data: () => ({
surveyShowDate: null,
}),
computed: {
shouldShowSurvey() {
const { surveyShowDate } = this;
const date = new Date(surveyShowDate);
// Survey is not enabled or user dismissed the survey by clicking the close icon.
if (surveyShowDate === this.$props.bannerId) {
return false;
}
// Date is invalid, we should show the survey.
else if (Number.isNaN(date.getDate())) {
return true;
}
return date <= Date.now();
},
},
methods: {
hideSurvey() {
this.surveyShowDate = this.$props.bannerId;
},
askLater() {
const date = new Date();
date.setDate(date.getDate() + this.daysToAskLater);
this.surveyShowDate = date.toISOString();
showToast(this.$props.toastMessage);
},
},
i18n: {
askAgainLater: __('Ask again later'),
},
};
</script>
<template>
<local-storage-sync v-model="surveyShowDate" :storage-key="storageKey">
<gl-banner
v-if="shouldShowSurvey"
:title="title"
:button-text="buttonText"
:svg-path="svgPath"
:button-link="surveyLink"
@close="hideSurvey"
>
<p>
<gl-sprintf :message="description">
<template #bold="{ content }">
<span class="gl-font-weight-bold">{{ content }}</span>
</template>
</gl-sprintf>
</p>
<template #actions>
<gl-button variant="link" class="gl-ml-5" data-testid="ask-later-button" @click="askLater">
{{ $options.i18n.askAgainLater }}
</gl-button>
</template>
</gl-banner>
</local-storage-sync>
</template>
......@@ -6,6 +6,7 @@ class Groups::Security::DashboardController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:sbom_survey, @user, default_enabled: :yaml)
end
def show
......
......@@ -4,6 +4,10 @@ module Projects
class DependenciesController < Projects::ApplicationController
include SecurityAndCompliancePermissions
before_action do
push_frontend_feature_flag(:sbom_survey, @user, default_enabled: :yaml)
end
before_action :authorize_read_dependency_list!
feature_category :dependency_scanning
......
......@@ -4,6 +4,10 @@ module Projects
class LicensesController < Projects::ApplicationController
include SecurityAndCompliancePermissions
before_action do
push_frontend_feature_flag(:sbom_survey, @user, default_enabled: :yaml)
end
before_action :authorize_read_licenses!, only: [:index]
before_action :authorize_admin_software_license_policy!, only: [:create, :update]
......@@ -103,6 +107,7 @@ module Projects
write_license_policies_endpoint: write_license_policies_endpoint,
documentation_path: help_page_path('user/compliance/license_compliance/index'),
empty_state_svg_path: helpers.image_path('illustrations/Dependency-list-empty-state.svg'),
sbom_survey_svg_path: helpers.image_path('illustrations/monitoring/tracing.svg'),
software_licenses: SoftwareLicense.unclassified_licenses_for(project).pluck_names,
project_id: @project.id,
project_path: expose_path(api_v4_projects_path(id: @project.id)),
......
......@@ -11,6 +11,7 @@ module Projects
before_action only: [:index] do
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:sbom_survey, @user, default_enabled: :yaml)
end
feature_category :vulnerability_management
......
......@@ -175,6 +175,7 @@ module EE
operational_empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
operational_help_path: help_page_path('user/application_security/policies/index'),
survey_request_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
sbom_survey_svg_path: image_path('illustrations/monitoring/tracing.svg'),
security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
project_full_path: project.full_path,
......@@ -189,6 +190,7 @@ module EE
vulnerabilities_export_endpoint: api_v4_security_projects_vulnerability_exports_path(id: project.id),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
survey_request_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
sbom_survey_svg_path: image_path('illustrations/monitoring/tracing.svg'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'),
......
......@@ -25,6 +25,7 @@ module Groups::SecurityFeaturesHelper
operational_empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
operational_help_path: help_page_path('user/application_security/policies/index'),
survey_request_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
sbom_survey_svg_path: image_path('illustrations/monitoring/tracing.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
vulnerabilities_export_endpoint: expose_path(api_v4_security_groups_vulnerability_exports_path(id: group.id)),
scanners: VulnerabilityScanners::ListService.new(group).execute.to_json,
......
......@@ -28,6 +28,7 @@ module SecurityHelper
def security_dashboard_unavailable_view_data
{
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
sbom_survey_svg_path: image_path('illustrations/monitoring/tracing.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
is_unavailable: "true"
}
......
......@@ -4,4 +4,5 @@
#js-dependencies-app{ data: { endpoint: project_dependencies_path(@project, format: :json),
documentation_path: help_page_path('user/application_security/dependency_list/index'),
support_documentation_path: help_page_path('user/application_security/dependency_scanning/index', anchor: 'supported-languages-and-package-managers'),
empty_state_svg_path: image_path('illustrations/Dependency-list-empty-state.svg') } }
empty_state_svg_path: image_path('illustrations/Dependency-list-empty-state.svg'),
sbom_survey_svg_path: image_path('illustrations/monitoring/tracing.svg')} }
......@@ -10,6 +10,8 @@ import { DEPENDENCY_LIST_TYPES } from 'ee/dependencies/store/constants';
import { REPORT_STATUS } from 'ee/dependencies/store/modules/list/constants';
import { TEST_HOST } from 'helpers/test_constants';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SbomBanner from 'ee/sbom_banner/components/app.vue';
describe('DependenciesApp component', () => {
let store;
......@@ -19,6 +21,7 @@ describe('DependenciesApp component', () => {
const basicAppProps = {
endpoint: '/foo',
emptyStateSvgPath: '/bar.svg',
sbomSurveySvgPath: '/foo.svg',
documentationPath: TEST_HOST,
supportDocumentationPath: `${TEST_HOST}/dependency_scanning#supported-languages`,
};
......@@ -29,12 +32,20 @@ describe('DependenciesApp component', () => {
const stubs = Object.keys(DependenciesApp.components).filter((name) => name !== 'GlSprintf');
wrapper = mount(DependenciesApp, {
store,
propsData: { ...props },
stubs,
...options,
});
window.gon = {
features: {
sbomSurvey: true,
},
};
wrapper = extendedWrapper(
mount(DependenciesApp, {
store,
propsData: { ...props },
stubs,
...options,
}),
);
};
const setStateJobNotRun = () => {
......@@ -96,6 +107,7 @@ describe('DependenciesApp component', () => {
const findJobFailedAlert = () => wrapper.find(DependencyListJobFailedAlert);
const findIncompleteListAlert = () => wrapper.find(DependencyListIncompleteAlert);
const findDependenciesTables = () => wrapper.findAll(PaginatedDependenciesTable);
const findSbomBanner = () => wrapper.findComponent(SbomBanner);
const findHeader = () => wrapper.find('section > header');
const findHeaderHelpLink = () => findHeader().find(GlLink);
......@@ -139,6 +151,7 @@ describe('DependenciesApp component', () => {
};
afterEach(() => {
window.gon = {};
wrapper.destroy();
});
......@@ -205,6 +218,12 @@ describe('DependenciesApp component', () => {
expectComponentWithProps(DependenciesActions, { namespace: allNamespace });
});
it('renders the SbomBannercomponent with the right props', () => {
const sbomBanner = findSbomBanner();
expect(sbomBanner.exists()).toBe(true);
expect(sbomBanner.props().sbomSurveySvgPath).toEqual(wrapper.props().sbomSurveySvgPath);
});
describe('given the user has public permissions', () => {
beforeEach(() => {
store.state[allNamespace].reportInfo.generatedAt = '';
......
......@@ -19,6 +19,7 @@ import {
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubTransition } from 'helpers/stub_transition';
import { TEST_HOST } from 'helpers/test_constants';
import SbomBanner from 'ee/sbom_banner/components/app.vue';
Vue.use(Vuex);
......@@ -29,6 +30,7 @@ const managedLicenses = [approvedLicense, blacklistedLicense];
const licenses = [{}, {}];
const emptyStateSvgPath = '/';
const documentationPath = '/';
const sbomSurveySvgPath = '/';
const noop = () => {};
......@@ -74,6 +76,7 @@ const createComponent = ({ state, props, options }) => {
propsData: {
emptyStateSvgPath,
documentationPath,
sbomSurveySvgPath,
readLicensePoliciesEndpoint,
...props,
},
......@@ -84,9 +87,19 @@ const createComponent = ({ state, props, options }) => {
};
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findSbomBanner = () => wrapper.findComponent(SbomBanner);
describe('Project Licenses', () => {
beforeEach(() => {
window.gon = {
features: {
sbomSurvey: true,
},
};
});
afterEach(() => {
window.gon = {};
wrapper.destroy();
wrapper = null;
});
......@@ -174,6 +187,12 @@ describe('Project Licenses', () => {
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
it('renders the SbomBannercomponent with the right props', () => {
const sbomBanner = findSbomBanner();
expect(sbomBanner.exists()).toBe(true);
expect(sbomBanner.props().sbomSurveySvgPath).toEqual(wrapper.props().sbomSurveySvgPath);
});
it('renders a "Detected in project" tab and a "Policies" tab', () => {
expect(wrapper.find(GlTabs).exists()).toBe(true);
expect(wrapper.find(GlTab).exists()).toBe(true);
......
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
SBOM_BANNER_LOCAL_STORAGE_KEY,
SBOM_BANNER_CURRENT_ID,
SBOM_SURVEY_LINK,
SBOM_SURVEY_DAYS_TO_ASK_LATER,
SBOM_SURVEY_TITLE,
SBOM_SURVEY_BUTTON_TEXT,
SBOM_SURVEY_DESCRIPTION,
SBOM_SURVEY_TOAST_MESSAGE,
} from 'ee/vue_shared/constants';
import sbomBanner from 'ee/sbom_banner/components/app.vue';
import sharedSurveyBanner from 'ee/vue_shared/survey_banner/survey_banner.vue';
describe('Sbom Banner Component', () => {
let wrapper;
const findSharedSurveyBanner = () => wrapper.findComponent(sharedSurveyBanner);
const createComponent = (sbomSurvey = { sbomSurvey: true }) => {
wrapper = extendedWrapper(
mount(sbomBanner, {
propsData: {
sbomSurveySvgPath: 'foo.svg',
},
provide: { glFeatures: { ...sbomSurvey } },
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('given a true sbom_survey flag', () => {
beforeEach(() => {
createComponent();
});
it('renders the SBOM Banner component with the right props', () => {
const surveyBanner = findSharedSurveyBanner();
expect(surveyBanner.exists()).toBe(true);
expect(surveyBanner.props()).toMatchObject({
bannerId: SBOM_BANNER_CURRENT_ID,
storageKey: SBOM_BANNER_LOCAL_STORAGE_KEY,
daysToAskLater: SBOM_SURVEY_DAYS_TO_ASK_LATER,
surveyLink: SBOM_SURVEY_LINK,
svgPath: wrapper.props().sbomSurveySvgPath,
title: SBOM_SURVEY_TITLE,
toastMessage: SBOM_SURVEY_TOAST_MESSAGE,
});
expect(surveyBanner.props('buttonText')).toContain(SBOM_SURVEY_BUTTON_TEXT);
expect(surveyBanner.props('description')).toContain(SBOM_SURVEY_DESCRIPTION);
});
});
describe('given a false sbom_survey flag', () => {
beforeEach(() => {
createComponent({ sbomSurvey: false });
});
it('does not render the SBOM Banner component', () => {
const surveyBanner = findSharedSurveyBanner();
expect(surveyBanner.exists()).toBe(false);
});
});
});
......@@ -23,6 +23,8 @@ describe('Group Security Dashboard component', () => {
let wrapper;
const groupFullPath = `${TEST_HOST}/group/5`;
// To be consumed by SecurityDashboardLayout
const sbomSurveySvgPath = '/';
const findSecurityChartsLayoutComponent = () => wrapper.find(SecurityDashboardLayout);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
......@@ -41,7 +43,7 @@ describe('Group Security Dashboard component', () => {
},
},
},
provide: { groupFullPath },
provide: { groupFullPath, sbomSurveySvgPath },
stubs: {
SecurityDashboardLayout,
},
......
......@@ -44,6 +44,10 @@ describe('Instance Security Dashboard component', () => {
stubs: {
SecurityDashboardLayout,
},
provide: {
// to be consumed by SecurityDashboardLayout
sbomSurveySvgPath: '/',
},
});
};
......@@ -62,6 +66,7 @@ describe('Instance Security Dashboard component', () => {
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(securityChartsLayout.props().showSbomSurvey).toBe(false);
expect(reportNotConfigured.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(true);
expect(vulnerabilitiesOverTimeChart.exists()).toBe(false);
......@@ -78,6 +83,7 @@ describe('Instance Security Dashboard component', () => {
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(securityChartsLayout.props().showSbomSurvey).toBe(false);
expect(reportNotConfigured.exists()).toBe(true);
expect(loadingIcon.exists()).toBe(false);
expect(vulnerabilitiesOverTimeChart.exists()).toBe(false);
......@@ -96,6 +102,7 @@ describe('Instance Security Dashboard component', () => {
const vulnerabilitySeverities = findVulnerabilitySeverities();
expect(securityChartsLayout.exists()).toBe(true);
expect(securityChartsLayout.props().showSbomSurvey).toBe(false);
expect(reportNotConfigured.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(false);
expect(vulnerabilitiesOverTimeChart.props()).toEqual({ query: vulnerabilityHistoryQuery });
......
......@@ -46,6 +46,10 @@ describe('Project Security Dashboard component', () => {
helpPath,
...propsData,
},
provide: {
// To be consumed by SecurityDashboardLayout
sbomSurveySvgPath: '/',
},
stubs: {
SecurityDashboardLayout,
},
......
......@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import SecurityDashboardLayout from 'ee/security_dashboard/components/shared/security_dashboard_layout.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/shared/survey_request_banner.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SbomBanner from 'ee/sbom_banner/components/app.vue';
describe('Security Dashboard Layout component', () => {
let wrapper;
......@@ -14,21 +15,33 @@ describe('Security Dashboard Layout component', () => {
const findDummyComponent = () => wrapper.findComponent(DummyComponent);
const findTitle = () => wrapper.findByTestId('title');
const findSurveyBanner = () => wrapper.findComponent(SurveyRequestBanner);
const findSbomBanner = () => wrapper.findComponent(SbomBanner);
const createWrapper = (slots) => {
wrapper = extendedWrapper(shallowMount(SecurityDashboardLayout, { slots }));
const createWrapper = (slots, props = { showSbomSurvey: true }) => {
wrapper = extendedWrapper(
shallowMount(SecurityDashboardLayout, {
provide: {
sbomSurveySvgPath: '/',
},
propsData: {
...props,
},
slots,
}),
);
};
afterEach(() => {
wrapper.destroy();
beforeEach(() => {
window.gon = {
features: {
sbomSurvey: true,
},
};
});
it('should render the default slot and survey banner', () => {
createWrapper({ default: DummyComponent });
expect(findDummyComponent().exists()).toBe(true);
expect(findTitle().exists()).toBe(true);
expect(findSurveyBanner().exists()).toBe(true);
afterEach(() => {
wrapper.destroy();
window.gon = {};
});
it('should render the empty-state slot and survey banner', () => {
......@@ -46,4 +59,25 @@ describe('Security Dashboard Layout component', () => {
expect(findTitle().exists()).toBe(false);
expect(findSurveyBanner().exists()).toBe(false);
});
describe('given a false showSbowmSurvey prop', () => {
beforeEach(() => {
createWrapper({}, { showSbomSurvey: false });
});
it('does not render the SBOM Banner component', () => {
const sbomBanner = findSbomBanner();
expect(sbomBanner.exists()).toBe(false);
});
});
describe('given a true showSbowmSurvey prop', () => {
beforeEach(() => {
createWrapper({}, { showSbomSurvey: true });
});
it('does not render the SBOM Banner component', () => {
const sbomBanner = findSbomBanner();
expect(sbomBanner.exists()).toBe(true);
expect(sbomBanner.props().sbomSurveySvgPath).toBe(wrapper.vm.sbomSurveySvgPath);
});
});
});
import { GlBanner, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SharedSurveyBanner from 'ee/vue_shared/survey_banner/survey_banner.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import toast from '~/vue_shared/plugins/global_toast';
const TEST_LOCAL_STORAGE_KEY = 'testLocalStorageKey';
const TEST_BANNER_ID = 'testBannerId';
jest.mock('~/vue_shared/plugins/global_toast');
describe('Shared Survey Banner component', () => {
let wrapper;
const findGlBanner = () => wrapper.findComponent(GlBanner);
const findAskLaterButton = () => wrapper.findByTestId('ask-later-button');
const getOffsetDateString = (days) => {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString();
};
const createWrapper = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(SharedSurveyBanner, {
propsData: {
surveyLink: 'foo.bar',
daysToAskLater: 7,
title: 'testTitle',
buttonText: 'buttonText',
description: 'description',
toastMessage: 'toastMessage',
storageKey: TEST_LOCAL_STORAGE_KEY,
bannerId: TEST_BANNER_ID,
svgPath: '/foo.svg',
...props,
},
stubs: { GlBanner, GlButton, LocalStorageSync },
}),
);
};
beforeEach(() => {
gon.features = {};
});
afterEach(() => {
wrapper.destroy();
localStorage.removeItem(TEST_LOCAL_STORAGE_KEY);
});
beforeEach(() => {
createWrapper();
});
it('shows the banner with the correct components and props', () => {
const { title, buttonText, description, svgPath } = wrapper.props();
expect(findGlBanner().html()).toContain(description);
expect(findAskLaterButton().exists()).toBe(true);
expect(findGlBanner().props()).toMatchObject({
title,
buttonText,
svgPath,
});
});
it.each`
showOrHide | phrase | localStorageValue | isShown
${'hides'} | ${'a future date'} | ${getOffsetDateString(1)} | ${false}
${'shows'} | ${'a past date'} | ${getOffsetDateString(-1)} | ${true}
${'hides'} | ${'the current survey ID'} | ${TEST_BANNER_ID} | ${false}
${'shows'} | ${'a different survey ID'} | ${'SOME OTHER ID'} | ${true}
`(
'$showOrHide the banner if the localStorage value is $phrase',
async ({ localStorageValue, isShown }) => {
localStorage.setItem(TEST_LOCAL_STORAGE_KEY, localStorageValue);
createWrapper();
await wrapper.vm.$nextTick();
expect(findGlBanner().exists()).toBe(isShown);
},
);
describe('closing the banner', () => {
beforeEach(() => {
createWrapper();
});
it('hides the banner and will set it to reshow later if the "Ask again later" button is clicked', async () => {
expect(findGlBanner().exists()).toBe(true);
findAskLaterButton().vm.$emit('click');
await wrapper.vm.$nextTick();
const date = new Date(localStorage.getItem(TEST_LOCAL_STORAGE_KEY));
expect(findGlBanner().exists()).toBe(false);
expect(toast).toHaveBeenCalledTimes(1);
expect(date > new Date()).toBe(true);
});
it('hides the banner and sets it to never show again if the close button is clicked', async () => {
expect(findGlBanner().exists()).toBe(true);
findGlBanner().vm.$emit('close');
await wrapper.vm.$nextTick();
expect(findGlBanner().exists()).toBe(false);
expect(localStorage.getItem(TEST_LOCAL_STORAGE_KEY)).toBe(TEST_BANNER_ID);
});
});
});
......@@ -10,6 +10,7 @@ const TEST_DATASET = {
svgPath: '/test/no_changes_state.svg',
dashboardDocumentation: '/test/dashboard_page',
emptyStateSvgPath: '/test/empty_state.svg',
sbomSurveySvgPath: '/',
};
describe('Security Dashboard', () => {
......
......@@ -72,6 +72,7 @@ RSpec.describe Groups::SecurityFeaturesHelper do
group_full_path: group.full_path,
no_vulnerabilities_svg_path: helper.image_path('illustrations/issues.svg'),
empty_state_svg_path: helper.image_path('illustrations/security-dashboard-empty-state.svg'),
sbom_survey_svg_path: helper.image_path('illustrations/monitoring/tracing.svg'),
operational_empty_state_svg_path: helper.image_path('illustrations/security-dashboard_empty.svg'),
operational_help_path: help_page_path('user/application_security/policies/index'),
survey_request_svg_path: helper.image_path('illustrations/security-dashboard_empty.svg'),
......
......@@ -193,6 +193,7 @@ RSpec.describe ProjectsHelper do
operational_empty_state_svg_path: kind_of(String),
operational_help_path: kind_of(String),
survey_request_svg_path: start_with('/assets/illustrations/security-dashboard_empty'),
sbom_survey_svg_path: start_with('/assets/illustrations/monitoring/tracing'),
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
project_full_path: project.full_path,
no_vulnerabilities_svg_path: start_with('/assets/illustrations/issues-'),
......@@ -217,6 +218,7 @@ RSpec.describe ProjectsHelper do
operational_empty_state_svg_path: kind_of(String),
operational_help_path: kind_of(String),
survey_request_svg_path: start_with('/assets/illustrations/security-dashboard_empty'),
sbom_survey_svg_path: start_with('/assets/illustrations/monitoring/tracing'),
dashboard_documentation: '/help/user/application_security/security_dashboard/index',
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
......
......@@ -31549,6 +31549,9 @@ msgstr ""
msgid "SecurityReports|Take survey"
msgstr ""
msgid "SecurityReports|The Composition Analysis group is planning significant updates to how we make available the list of software and container dependency information in your projects. Therefore, we ask that you assist us by taking a short -no longer than 5 minute- survey to help align our direction with your needs."
msgstr ""
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
......@@ -31624,6 +31627,9 @@ msgstr ""
msgid "SecurityReports|You must sign in as an authorized user to see this report"
msgstr ""
msgid "SecurityReports|Your feedback is important to us! We will ask again in 7 days."
msgstr ""
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
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