Commit 7e03e71c authored by Jeremy Jackson's avatar Jeremy Jackson Committed by Miguel Rincon

Add a new "enable feature" prompt in MR widget

This is the implementation of an experiment that we might run on a
couple of features. Because of this, it’s been implemented as a new
component that can be used for multiple features.
parent 8dd996c2
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
} }
} }
.ci-status-icon-notification,
.ci-status-icon-preparing, .ci-status-icon-preparing,
.ci-status-icon-created, .ci-status-icon-created,
.ci-status-icon-skipped, .ci-status-icon-skipped,
......
<script>
import { GlButton } from '@gitlab/ui';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
name: 'MrWidgetEnableFeaturePrompt',
components: {
CiIcon,
GitlabExperiment,
GlButton,
},
props: {
feature: {
type: String,
required: true,
},
},
data() {
const dismissalKey = [this.$options.name, this.feature, 'dismissed'].join('.');
return {
dismissalKey,
status: {
group: 'notification',
icon: 'status-neutral',
},
dismissed: localStorage.getItem(dismissalKey),
};
},
methods: {
dismiss() {
localStorage.setItem(this.dismissalKey, (this.dismissed = true));
},
},
};
</script>
<template>
<gitlab-experiment v-if="!dismissed" :name="feature">
<template #control></template>
<template #candidate>
<div class="mr-widget-body media">
<ci-icon class="gl-mr-3" :status="status" :size="24" />
<div class="media-body gl-text-gray-400">
<slot></slot>
</div>
<gl-button
category="tertiary"
size="small"
icon="close"
:aria-label="s__('mrWidget|Dismiss')"
data-track-action="dismissed"
:data-track-experiment="feature"
@click="dismiss"
/>
</div>
</template>
</gitlab-experiment>
</template>
<script> <script>
import { GlSafeHtmlDirective } from '@gitlab/ui'; import { GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import MrWidgetLicenses from 'ee/vue_shared/license_compliance/mr_widget_license_report.vue'; import MrWidgetLicenses from 'ee/vue_shared/license_compliance/mr_widget_license_report.vue';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin'; import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import MrWidgetEnableFeaturePrompt from './components/states/mr_widget_enable_feature_prompt.vue';
import MrWidgetJiraAssociationMissing from './components/states/mr_widget_jira_association_missing.vue'; import MrWidgetJiraAssociationMissing from './components/states/mr_widget_jira_association_missing.vue';
import MrWidgetPolicyViolation from './components/states/mr_widget_policy_violation.vue'; import MrWidgetPolicyViolation from './components/states/mr_widget_policy_violation.vue';
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue'; import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
export default { export default {
components: { components: {
GlSprintf,
GlLink,
MrWidgetLicenses, MrWidgetLicenses,
MrWidgetGeoSecondaryNode, MrWidgetGeoSecondaryNode,
MrWidgetPolicyViolation, MrWidgetPolicyViolation,
MrWidgetEnableFeaturePrompt,
MrWidgetJiraAssociationMissing, MrWidgetJiraAssociationMissing,
StatusChecksReportsApp: () => StatusChecksReportsApp: () =>
import('ee/reports/status_checks_report/status_checks_reports_app.vue'), import('ee/reports/status_checks_report/status_checks_reports_app.vue'),
...@@ -389,6 +393,31 @@ export default { ...@@ -389,6 +393,31 @@ export default {
:mr-iid="mr.iid" :mr-iid="mr.iid"
class="js-security-widget" class="js-security-widget"
/> />
<mr-widget-enable-feature-prompt
v-else-if="mr.canReadVulnerabilities"
feature="security_reports_mr_widget_prompt"
>
{{ s__('mrWidget|SAST and Secret Detection is not enabled.') }}
<gl-sprintf
:message="
s__(
'mrWidget|%{linkStart}Set up now%{linkEnd} to analyze your source code for known security vulnerabilities.',
)
"
>
<template #link="{ content }">
<gl-link
href="../security/configuration"
target="_blank"
rel="noopener noreferrer"
data-track-action="followed"
data-track-experiment="security_reports_mr_widget_prompt"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</mr-widget-enable-feature-prompt>
<mr-widget-licenses <mr-widget-licenses
v-if="shouldRenderLicenseReport" v-if="shouldRenderLicenseReport"
:api-url="mr.licenseScanning.managed_licenses_path" :api-url="mr.licenseScanning.managed_licenses_path"
......
import { mount } from '@vue/test-utils';
import MrWidgetEnableFeaturePrompt from 'ee/vue_merge_request_widget/components/states/mr_widget_enable_feature_prompt.vue';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
const FEATURE = 'my_feature_name';
const LOCAL_STORAGE_KEY = `MrWidgetEnableFeaturePrompt.${FEATURE}.dismissed`;
describe('MrWidgetEnableFeaturePrompt', () => {
let wrapper;
const createComponent = () => {
wrapper = mount(MrWidgetEnableFeaturePrompt, {
propsData: { feature: FEATURE },
slots: {
default: 'this is my content',
},
});
};
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findDismissButton = () => wrapper.find('[data-track-action="dismissed"]');
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('when the experiment is not enabled', () => {
beforeAll(() => {
assignGitlabExperiment(FEATURE, 'control');
});
it('renders nothing', () => {
expect(wrapper.text()).toBe('');
});
});
describe('when the experiment is enabled', () => {
beforeAll(() => {
localStorage.removeItem(LOCAL_STORAGE_KEY);
assignGitlabExperiment(FEATURE, 'candidate');
});
it('shows a neutral icon', () => {
expect(findCiIcon().props('status').group).toBe('notification');
expect(findCiIcon().props('status').icon).toBe('status-neutral');
expect(findCiIcon().props('size')).toBe(24);
});
it('renders the provided slots', () => {
expect(wrapper.text()).toBe('this is my content');
});
it('can be dismissed', async () => {
const button = findDismissButton();
button.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(localStorage.getItem(LOCAL_STORAGE_KEY)).toBe('true');
expect(wrapper.text()).toBe('');
});
});
});
...@@ -24,6 +24,7 @@ import { ...@@ -24,6 +24,7 @@ import {
coverageFuzzingDiffSuccessMock, coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock, apiFuzzingDiffSuccessMock,
} from 'ee_jest/vue_shared/security_reports/mock_data'; } from 'ee_jest/vue_shared/security_reports/mock_data';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
...@@ -212,6 +213,22 @@ describe('ee merge request widget options', () => { ...@@ -212,6 +213,22 @@ describe('ee merge request widget options', () => {
}); });
}); });
}); });
describe('when not enabled', () => {
it("doesn't show anything SAST related", () => {
createComponent({ propsData: { mrData: mockData } });
expect(wrapper.text()).not.toContain('SAST');
});
describe('security_reports_mr_widget_prompt experiment', () => {
assignGitlabExperiment('security_reports_mr_widget_prompt', 'candidate');
it('prompts to enable the feature', () => {
createComponent({ propsData: { mrData: mockData } });
expect(wrapper.text()).toContain('SAST and Secret Detection is not enabled.');
});
});
});
}); });
describe('Dependency Scanning', () => { describe('Dependency Scanning', () => {
......
...@@ -40148,6 +40148,9 @@ msgstr "" ...@@ -40148,6 +40148,9 @@ msgstr ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
msgid "mrWidget|%{linkStart}Set up now%{linkEnd} to analyze your source code for known security vulnerabilities."
msgstr ""
msgid "mrWidget|%{mergeError}." msgid "mrWidget|%{mergeError}."
msgstr "" msgstr ""
...@@ -40240,6 +40243,9 @@ msgstr "" ...@@ -40240,6 +40243,9 @@ msgstr ""
msgid "mrWidget|Did not close" msgid "mrWidget|Did not close"
msgstr "" msgstr ""
msgid "mrWidget|Dismiss"
msgstr ""
msgid "mrWidget|Email patches" msgid "mrWidget|Email patches"
msgstr "" msgstr ""
...@@ -40356,6 +40362,9 @@ msgstr "" ...@@ -40356,6 +40362,9 @@ msgstr ""
msgid "mrWidget|Revoke approval" msgid "mrWidget|Revoke approval"
msgstr "" msgstr ""
msgid "mrWidget|SAST and Secret Detection is not enabled."
msgstr ""
msgid "mrWidget|Set by %{merge_author} to be added to the merge train when the pipeline succeeds" msgid "mrWidget|Set by %{merge_author} to be added to the merge train when the pipeline succeeds"
msgstr "" 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