Commit 874e557f authored by Sam Beckham's avatar Sam Beckham Committed by Kushal Pandya

Makes a new resolution alert component

- Adds the component
- Only renders it when there's a resolved on branch
- Displays a fallback if we don't have a default branch name
- Adds tests for the above
parent f494909e
<script>
import { GlLoadingIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { GlButton, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Api from 'ee/api';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import ResolutionAlert from './resolution_alert.vue';
import VulnerabilityStateDropdown from './vulnerability_state_dropdown.vue';
import { VULNERABILITY_STATES } from '../constants';
export default {
name: 'VulnerabilityManagementApp',
components: {
GlLoadingIcon,
GlButton,
GlLink,
GlLoadingIcon,
GlSprintf,
ResolutionAlert,
TimeAgoTooltip,
VulnerabilityStateDropdown,
LoadingButton,
},
props: {
......@@ -54,6 +55,9 @@ export default {
// Get the badge variant based on the vulnerability state, defaulting to 'expired'.
return VULNERABILITY_STATES[this.state]?.statusBoxStyle || 'expired';
},
showResolutionAlert() {
return this.vulnerability.resolved_on_default_branch && this.state !== 'resolved';
},
},
methods: {
......@@ -101,49 +105,57 @@ export default {
</script>
<template>
<div class="detail-page-header">
<div class="detail-page-header-body lh-4 align-items-center">
<gl-loading-icon v-if="isLoadingVulnerability" class="mr-2" />
<span
v-else
ref="badge"
:class="
`text-capitalize align-self-center issuable-status-box status-box status-box-${statusBoxStyle}`
"
>
{{ state }}
</span>
<div>
<resolution-alert
v-if="showResolutionAlert"
:default-branch-name="vulnerability.default_branch_name"
/>
<div class="detail-page-header">
<div class="detail-page-header-body lh-4 align-items-center">
<gl-loading-icon v-if="isLoadingVulnerability" class="mr-2" />
<span
v-else
ref="badge"
:class="
`text-capitalize align-self-center issuable-status-box status-box status-box-${statusBoxStyle}`
"
>
{{ state }}
</span>
<span v-if="pipeline" class="issuable-meta">
<gl-sprintf :message="__('Detected %{timeago} in pipeline %{pipelineLink}')">
<template #timeago>
<time-ago-tooltip :time="pipeline.created_at" />
</template>
<template v-if="pipeline.id" #pipelineLink>
<gl-link :href="pipeline.url" class="link" target="_blank">{{ pipeline.id }}</gl-link>
</template>
</gl-sprintf>
</span>
<span v-if="pipeline" class="issuable-meta">
<gl-sprintf :message="__('Detected %{timeago} in pipeline %{pipelineLink}')">
<template #timeago>
<time-ago-tooltip :time="pipeline.created_at" />
</template>
<template v-if="pipeline.id" #pipelineLink>
<gl-link :href="pipeline.url" class="link" target="_blank">{{ pipeline.id }}</gl-link>
</template>
</gl-sprintf>
</span>
<time-ago-tooltip v-else class="issuable-meta" :time="vulnerability.created_at" />
</div>
<time-ago-tooltip v-else class="issuable-meta" :time="vulnerability.created_at" />
</div>
<div class="detail-page-header-actions align-items-center">
<label class="mb-0 mx-2">{{ __('Status') }}</label>
<gl-loading-icon v-if="isLoadingVulnerability" class="d-inline" />
<vulnerability-state-dropdown
v-else
:initial-state="state"
@change="onVulnerabilityStateChange"
/>
<loading-button
ref="create-issue-btn"
class="ml-2"
:loading="isCreatingIssue"
:label="s__('VulnerabilityManagement|Create issue')"
container-class="btn btn-success btn-inverted"
@click="createIssue"
/>
<div class="detail-page-header-actions align-items-center">
<label class="mb-0 mx-2">{{ __('Status') }}</label>
<gl-loading-icon v-if="isLoadingVulnerability" class="d-inline" />
<vulnerability-state-dropdown
v-else
:initial-state="state"
@change="onVulnerabilityStateChange"
/>
<gl-button
ref="create-issue-btn"
class="ml-2"
variant="success"
category="secondary"
:loading="isCreatingIssue"
@click="createIssue"
>
{{ s__('VulnerabilityManagement|Create issue') }}
</gl-button>
</div>
</div>
</div>
</template>
<script>
import { GlAlert } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
export default {
name: 'ResolutionAlert',
components: {
GlAlert,
},
props: {
defaultBranchName: {
type: String,
required: false,
default: '',
},
},
data: () => ({
isVisible: true,
}),
computed: {
resolutionTitle() {
return this.defaultBranchName
? sprintf(__(`Vulnerability resolved in %{branch}`), { branch: this.defaultBranchName })
: __('Vulnerability resolved in the default branch');
},
},
methods: {
dismiss() {
// This isn't the best way to handle the dismissal, but it is a borig solution.
// The next iteration of this is tracked in the below issue.
// https://gitlab.com/gitlab-org/gitlab/-/issues/212195
this.isVisible = false;
},
},
};
</script>
<template>
<gl-alert v-if="isVisible" :title="resolutionTitle" @dismiss="dismiss">
{{
__(
'The vulnerability is no longer detected. Verify the vulnerability has been remediated before changing its status.',
)
}}
</gl-alert>
</template>
......@@ -6,6 +6,7 @@ import * as urlUtility from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import App from 'ee/vulnerabilities/components/app.vue';
import waitForPromises from 'helpers/wait_for_promises';
import ResolutionAlert from 'ee/vulnerabilities/components/resolution_alert.vue';
import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
......@@ -16,10 +17,11 @@ jest.mock('~/flash');
describe('Vulnerability management app', () => {
let wrapper;
const vulnerability = {
const defaultVulnerability = {
id: 1,
created_at: new Date().toISOString(),
report_type: 'sast',
state: 'detected',
};
const dataset = {
......@@ -34,12 +36,16 @@ describe('Vulnerability management app', () => {
const findCreateIssueButton = () => wrapper.find({ ref: 'create-issue-btn' });
const findBadge = () => wrapper.find({ ref: 'badge' });
const findResolutionAlert = () => wrapper.find(ResolutionAlert);
const createWrapper = (state = 'detected') => {
const createWrapper = (vulnerability = {}) => {
wrapper = shallowMount(App, {
propsData: {
vulnerability: Object.assign({ state }, vulnerability),
...dataset,
vulnerability: {
...defaultVulnerability,
...vulnerability,
},
},
});
};
......@@ -102,9 +108,12 @@ describe('Vulnerability management app', () => {
expect(JSON.parse(postRequest.data)).toMatchObject({
vulnerability_feedback: {
feedback_type: 'issue',
category: vulnerability.report_type,
category: defaultVulnerability.report_type,
project_fingerprint: dataset.projectFingerprint,
vulnerability_data: { ...vulnerability, category: vulnerability.report_type },
vulnerability_data: {
...defaultVulnerability,
category: defaultVulnerability.report_type,
},
},
});
expect(spy).toHaveBeenCalledWith(issueUrl);
......@@ -126,12 +135,50 @@ describe('Vulnerability management app', () => {
describe('state badge', () => {
test.each(vulnerabilityStateEntries)(
'the vulnerability state badge has the correct style for the %s state',
(stateString, stateObject) => {
createWrapper(stateString);
(state, stateObject) => {
createWrapper({ state });
expect(findBadge().classes()).toContain(`status-box-${stateObject.statusBoxStyle}`);
expect(findBadge().text()).toBe(stateString);
expect(findBadge().text()).toBe(state);
},
);
});
describe('when the vulnerability is no-longer detected on the default branch', () => {
const branchName = 'master';
beforeEach(() => {
createWrapper({
resolved_on_default_branch: true,
default_branch_name: branchName,
});
});
it('should show the resolution alert component', () => {
const alert = findResolutionAlert();
expect(alert.exists()).toBe(true);
});
it('should pass down the default branch name', () => {
const alert = findResolutionAlert();
expect(alert.props().defaultBranchName).toEqual(branchName);
});
describe('when the vulnerability is already resolved', () => {
beforeEach(() => {
createWrapper({
resolved_on_default_branch: true,
state: 'resolved',
});
});
it('should not show the resolution alert component', () => {
const alert = findResolutionAlert();
expect(alert.exists()).toBe(false);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import ResolutionAlert from 'ee/vulnerabilities/components/resolution_alert.vue';
describe('Vulnerability list component', () => {
let wrapper;
const DEFAULT_BRANCH_NAME = 'not-always-master';
const createWrapper = (options = {}) => {
wrapper = shallowMount(ResolutionAlert, options);
};
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => wrapper.destroy());
describe('with a default branch name passed to it', () => {
beforeEach(() => {
createWrapper({
propsData: { defaultBranchName: DEFAULT_BRANCH_NAME },
});
});
it('should render the default branch name in the alert title', () => {
const alert = findAlert();
expect(alert.attributes().title).toMatch(DEFAULT_BRANCH_NAME);
});
it('should call the dismiss method when dismissed', () => {
expect(wrapper.vm.isVisible).toBe(true);
wrapper.vm.dismiss();
expect(wrapper.vm.isVisible).toBe(false);
});
});
describe('with no default branch name', () => {
beforeEach(() => {
createWrapper();
});
it('should render the fallback in the alert title', () => {
const alert = findAlert();
expect(alert.attributes().title).toMatch('in the default branch');
});
});
});
......@@ -20023,6 +20023,9 @@ msgstr ""
msgid "The vulnerability is no longer detected. Verify the vulnerability has been fixed or removed before changing its status."
msgstr ""
msgid "The vulnerability is no longer detected. Verify the vulnerability has been remediated before changing its status."
msgstr ""
msgid "There are no GPG keys associated with this account."
msgstr ""
......@@ -22432,6 +22435,12 @@ msgstr ""
msgid "Vulnerability remediated. Review before resolving."
msgstr ""
msgid "Vulnerability resolved in %{branch}"
msgstr ""
msgid "Vulnerability resolved in the default branch"
msgstr ""
msgid "Vulnerability-Check"
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