Commit a10dcd98 authored by Sam Beckham's avatar Sam Beckham Committed by Filipa Lacerda

Adds dismissalFeedback to the secure modal

This starts us down the path of unifying the data on the modal and
allows us to start showing dismissal data in the same way across the two
different uses of the security reports modal
parent 036a8567
...@@ -127,7 +127,8 @@ export default { ...@@ -127,7 +127,8 @@ export default {
state.modal.vulnerability, state.modal.vulnerability,
'hasMergeRequest', 'hasMergeRequest',
Boolean( Boolean(
vulnerability.merge_request_feedback && vulnerability.merge_request.merge_request_iid, vulnerability.merge_request_feedback &&
vulnerability.merge_request_feedback.merge_request_iid,
), ),
); );
Vue.set(state.modal.vulnerability, 'isDismissed', Boolean(vulnerability.dismissal_feedback)); Vue.set(state.modal.vulnerability, 'isDismissed', Boolean(vulnerability.dismissal_feedback));
......
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
export default {
components: {
EventItem,
},
props: {
feedback: {
type: Object,
required: true,
},
project: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
eventText() {
const { project, feedback } = this;
const { pipeline } = feedback;
const pipelineLink =
pipeline && pipeline.path && pipeline.id
? `<a href="${pipeline.path}">#${pipeline.id}</a>`
: null;
const projectLink =
project && project.url && project.value
? `<a href="${_.escape(project.url)}">${_.escape(project.value)}</a>`
: null;
if (pipelineLink && projectLink) {
return sprintf(
__('Dismissed on pipeline %{pipelineLink} at %{projectLink}'),
{ pipelineLink, projectLink },
false,
);
} else if (pipelineLink && !projectLink) {
return sprintf(__('Dismissed on pipeline %{pipelineLink}'), { pipelineLink }, false);
} else if (!pipelineLink && projectLink) {
return sprintf(__('Dismissed at %{projectLink}'), { projectLink }, false);
}
return __('Dismissed');
},
},
};
</script>
<template>
<event-item
:author="feedback.author"
:created-at="feedback.created_at"
icon-name="cancel"
icon-style="ci-status-icon-pending"
>
<div v-html="eventText"></div>
</event-item>
</template>
<script> <script>
import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
name: 'EventItem', name: 'EventItem',
components: { components: {
Icon, Icon,
TimeAgoTooltip,
}, },
props: { props: {
type: { author: {
type: String, type: Object,
required: true,
},
authorName: {
type: String,
required: true, required: true,
}, },
authorUsername: { createdAt: {
type: String,
required: true,
},
projectName: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
projectLink: { iconName: {
type: String, type: String,
required: false, required: false,
default: '', default: 'plus',
},
actionLinkText: {
type: String,
required: true,
}, },
actionLinkUrl: { iconStyle: {
type: String, type: String,
required: true, required: false,
}, default: 'ci-status-icon-success',
},
typeMap: {
issue: {
name: s__('Reports|issue'),
icon: 'issue-created',
},
mergeRequest: {
name: s__('Reports|merge request'),
icon: 'merge-request',
},
},
computed: {
typeData() {
return this.$options.typeMap[this.type] || {};
},
iconName() {
return this.typeData.icon || 'plus';
},
createdText() {
return sprintf(s__('ciReport|Created %{eventType}'), { eventType: this.typeData.name });
}, },
}, },
}; };
...@@ -65,24 +34,29 @@ export default { ...@@ -65,24 +34,29 @@ export default {
<template> <template>
<div class="card-body d-flex align-items-center"> <div class="card-body d-flex align-items-center">
<div class="circle-icon-container ci-status-icon-success"> <div class="circle-icon-container" :class="iconStyle">
<icon :size="16" :name="iconName" /> <icon :size="16" :name="iconName" />
</div> </div>
<div class="ml-3"> <div class="ml-3">
<div> <div class="note-header-info pb-0">
<strong class="js-author-name">{{ authorName }}</strong> <a
<em class="js-username">@{{ authorUsername }}</em> :href="author.path"
</div> :data-user-id="author.id"
<div> :data-username="author.username"
<span v-if="typeData.name" class="js-created">{{ createdText }}</span> class="js-author"
<a class="js-action-link" :title="actionLinkText" :href="actionLinkUrl"> >
{{ actionLinkText }} <strong class="note-header-author-name">{{ author.name }}</strong>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
</a> </a>
<template v-if="projectName"> <span class="note-headline-light note-headline-meta">
<span>{{ __('at') }} </span> <template v-if="createdAt">
<a class="js-project-name" :title="projectName" :href="projectLink">{{ projectName }}</a> <span class="system-note-separator">·</span>
</template> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
</template>
</span>
</div> </div>
<slot></slot>
</div> </div>
</div> </div>
</template> </template>
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
export default {
components: {
EventItem,
},
props: {
feedback: {
type: Object,
required: true,
},
project: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
eventText() {
const { project, feedback } = this;
const issueLink = `<a href="${feedback.issue_url}">#${feedback.issue_iid}</a>`;
if (project && project.value && project.url) {
const projectLink = `<a href="${_.escape(project.url)}">${_.escape(project.value)}</a>`;
return sprintf(
__('Created issue %{issueLink} at %{projectLink}'),
{
issueLink,
projectLink,
},
false,
);
}
return sprintf(__('Created issue %{issueLink}'), { issueLink }, false);
},
},
};
</script>
<template>
<event-item :author="feedback.author" :created-at="feedback.created_at" icon-name="issue-created">
<div v-html="eventText"></div>
</event-item>
</template>
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
export default {
components: {
EventItem,
},
props: {
feedback: {
type: Object,
required: true,
},
project: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
eventText() {
const { project, feedback } = this;
const mergeRequestLink = `<a href="${feedback.merge_request_path}">!${
feedback.merge_request_iid
}</a>`;
if (project && project.value && project.url) {
const projectLink = `<a href="${_.escape(project.url)}">${_.escape(project.value)}</a>`;
return sprintf(
__('Created merge request %{mergeRequestLink} at %{projectLink}'),
{
mergeRequestLink,
projectLink,
},
false,
);
}
return sprintf(__('Created merge request %{mergeRequestLink}'), { mergeRequestLink }, false);
},
},
};
</script>
<template>
<event-item :author="feedback.author" :created-at="feedback.created_at" icon-name="merge-request">
<div v-html="eventText"></div>
</event-item>
</template>
...@@ -4,16 +4,22 @@ import Modal from '~/vue_shared/components/gl_modal.vue'; ...@@ -4,16 +4,22 @@ import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue'; import ExpandButton from '~/vue_shared/components/expand_button.vue';
import DismissalNote from 'ee/vue_shared/security_reports/components/dismissal_note.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue'; import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue'; import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue'; import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue'; import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue';
export default { export default {
components: { components: {
DismissalNote,
EventItem, EventItem,
ExpandButton, ExpandButton,
IssueNote,
LoadingButton, LoadingButton,
MergeRequestNote,
Modal, Modal,
SolutionCard, SolutionCard,
SplitButton, SplitButton,
...@@ -80,7 +86,7 @@ export default { ...@@ -80,7 +86,7 @@ export default {
); );
}, },
project() { project() {
return this.modal.data.project || {}; return this.modal.data.project;
}, },
solution() { solution() {
return this.vulnerability && this.vulnerability.solution; return this.vulnerability && this.vulnerability.solution;
...@@ -102,14 +108,20 @@ export default { ...@@ -102,14 +108,20 @@ export default {
(this.canCreateFeedbackPermission || this.canCreateIssuePermission) (this.canCreateFeedbackPermission || this.canCreateIssuePermission)
); );
}, },
vulnerability() {
return this.modal.vulnerability;
},
issueFeedback() { issueFeedback() {
return this.vulnerability && this.vulnerability.issue_feedback; return this.vulnerability && this.vulnerability.issue_feedback;
}, },
mergeRequestFeedback() { mergeRequestFeedback() {
return this.vulnerability && this.vulnerability.merge_request_feedback; return this.vulnerability && this.vulnerability.merge_request_feedback;
}, },
vulnerability() { dismissalFeedback() {
return this.modal.vulnerability; return (
this.vulnerability &&
(this.vulnerability.dismissal_feedback || this.vulnerability.dismissalFeedback)
);
}, },
valuedFields() { valuedFields() {
const { data } = this.modal; const { data } = this.modal;
...@@ -148,43 +160,21 @@ export default { ...@@ -148,43 +160,21 @@ export default {
<solution-card v-if="renderSolutionCard" :solution="solution" :remediation="remediation" /> <solution-card v-if="renderSolutionCard" :solution="solution" :remediation="remediation" />
<hr v-else /> <hr v-else />
<ul v-if="vulnerability.hasIssue || vulnerability.hasMergeRequest" class="notes card"> <ul v-if="issueFeedback || mergeRequestFeedback" class="notes card my-4">
<li v-if="vulnerability.hasIssue" class="note"> <li v-if="issueFeedback" class="note">
<event-item <issue-note :feedback="issueFeedback" :project="project" />
type="issue"
:project-name="project.value"
:project-link="project.url"
:author-name="issueFeedback.author.name"
:author-username="issueFeedback.author.username"
:action-link-text="`#${issueFeedback.issue_iid}`"
:action-link-url="issueFeedback.issue_url"
/>
</li> </li>
<li v-if="vulnerability.hasMergeRequest" class="note"> <li v-if="mergeRequestFeedback" class="note">
<event-item <merge-request-note :feedback="mergeRequestFeedback" :project="project" />
type="mergeRequest"
:project-name="project.value"
:project-link="project.url"
:author-name="mergeRequestFeedback.author.name"
:author-username="mergeRequestFeedback.author.username"
:action-link-text="`!${mergeRequestFeedback.merge_request_iid}`"
:action-link-url="mergeRequestFeedback.merge_request_path"
/>
</li> </li>
</ul> </ul>
<div class="card my-4">
<dismissal-note v-if="dismissalFeedback" :feedback="dismissalFeedback" :project="project" />
</div>
<div class="prepend-top-20 append-bottom-10"> <div class="prepend-top-20 append-bottom-10">
<div class="col-sm-12 text-secondary"> <div class="col-sm-12 text-secondary">
<template v-if="hasDismissedBy">
{{ s__('ciReport|Dismissed by') }}
<a :href="vulnerability.dismissalFeedback.author.web_url" class="pipeline-id">
@{{ vulnerability.dismissalFeedback.author.username }}
</a>
{{ s__('ciReport|on pipeline') }}
<a :href="vulnerability.dismissalFeedback.pipeline.path" class="pipeline-id"
>#{{ vulnerability.dismissalFeedback.pipeline.id }}</a
>.
</template>
<a <a
v-if="vulnerabilityFeedbackHelpPath" v-if="vulnerabilityFeedbackHelpPath"
:href="vulnerabilityFeedbackHelpPath" :href="vulnerabilityFeedbackHelpPath"
......
---
title: Adds a dismissal item to the vulnerability modal
merge_request: 11028
author:
type: added
import { shallowMount } from '@vue/test-utils';
import component from 'ee/vue_shared/security_reports/components/dismissal_note.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
describe('dismissal note', () => {
const now = new Date();
const feedback = {
author: {
name: 'Tanuki',
username: 'gitlab',
},
created_at: now.toString(),
};
const pipeline = {
path: '/path-to-the-pipeline',
id: 2,
};
const project = {
value: 'Project one',
url: '/path-to-the-project',
};
describe('with no attached project or pipeline', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback },
});
});
it('should pass the author to the event item', () => {
expect(wrapper.find(EventItem).props('author')).toBe(feedback.author);
});
it('should pass the created date to the event item', () => {
expect(wrapper.find(EventItem).props('createdAt')).toBe(feedback.created_at);
});
it('should return the event text with no project data', () => {
expect(wrapper.text()).toBe('Dismissed');
});
});
describe('with an attached project', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback, project },
});
});
it('should return the event text with project data', () => {
expect(wrapper.text()).toBe(`Dismissed at ${project.value}`);
});
});
describe('with an attached pipeline', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback: { ...feedback, pipeline } },
});
});
it('should return the event text with project data', () => {
expect(wrapper.text()).toBe(`Dismissed on pipeline #${pipeline.id}`);
});
});
describe('with an attached pipeline and project', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback: { ...feedback, pipeline }, project },
});
});
it('should return the event text with project data', () => {
expect(wrapper.text()).toBe(`Dismissed on pipeline #${pipeline.id} at ${project.value}`);
});
});
describe('with unsafe data', () => {
let wrapper;
const unsafeProject = {
...project,
value: 'Foo <script>alert("XSS")</script>',
};
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: {
feedback,
project: unsafeProject,
},
});
});
it('should escape the project name', () => {
// Note: We have to check the computed prop here because
// vue test utils unescapes the result of wrapper.text()
expect(wrapper.vm.eventText).not.toContain(project.value);
expect(wrapper.vm.eventText).toContain(
'Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;',
);
});
});
});
...@@ -5,10 +5,10 @@ import mountComponent from 'helpers/vue_mount_component_helper'; ...@@ -5,10 +5,10 @@ import mountComponent from 'helpers/vue_mount_component_helper';
describe('Event Item', () => { describe('Event Item', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
const props = { const props = {
authorName: 'Tanuki', author: {
authorUsername: 'gitlab', name: 'Tanuki',
actionLinkText: 'foo', username: 'gitlab',
actionLinkUrl: 'example.com', },
}; };
let vm; let vm;
...@@ -16,66 +16,23 @@ describe('Event Item', () => { ...@@ -16,66 +16,23 @@ describe('Event Item', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('issue item', () => { beforeEach(() => {
beforeEach(() => { vm = mountComponent(Component, props);
props.type = 'issue';
vm = mountComponent(Component, props);
});
it('uses the issue icon', () => {
expect(vm.iconName).toBe('issue-created');
});
it('uses the issue name', () => {
expect(vm.$el.querySelector('.js-created').textContent).toContain('issue');
});
it('uses the author name', () => {
expect(vm.$el.querySelector('.js-author-name').textContent).toContain(props.authorName);
});
it('uses the author username', () => {
expect(vm.$el.querySelector('.js-username').textContent).toContain(props.authorUsername);
});
it('uses the action link text', () => {
expect(vm.$el.querySelector('.js-action-link').textContent).toContain(props.actionLinkText);
});
it('uses the action link url', () => {
expect(vm.$el.querySelector('.js-action-link').getAttribute('href')).toBe(
props.actionLinkUrl,
);
});
}); });
describe('merge request item', () => { it('uses the author name', () => {
beforeEach(() => { expect(vm.$el.querySelector('.js-author').textContent).toContain(props.author.name);
props.type = 'mergeRequest';
vm = mountComponent(Component, props);
});
it('uses the merge request icon', () => {
expect(vm.iconName).toBe('merge-request');
});
it('uses the issue name', () => {
expect(vm.$el.querySelector('.js-created').textContent).toContain('merge request');
});
}); });
describe('unknown item', () => { it('uses the author username', () => {
beforeEach(() => { expect(vm.$el.querySelector('.js-author').textContent).toContain(`@${props.author.username}`);
props.type = 'notARealType'; });
vm = mountComponent(Component, props);
});
it('uses the fallback icon', () => { it('uses the fallback icon', () => {
expect(vm.iconName).toBe('plus'); expect(vm.iconName).toBe('plus');
}); });
it("doesn't display the created text", () => { it('uses the fallback icon class', () => {
expect(vm.$el.querySelector('.js-created')).toBeNull(); expect(vm.iconStyle).toBe('ci-status-icon-success');
});
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import component from 'ee/vue_shared/security_reports/components/issue_note.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
describe('Issue note', () => {
const now = new Date();
const feedback = {
author: {
name: 'Tanuki',
username: 'gitlab',
},
issue_url: '/path-to-the-issue',
issue_iid: 1,
created_at: now.toString(),
};
const project = {
value: 'Project one',
url: '/path-to-the-project',
};
describe('with no attached project', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback },
});
});
it('should pass the author to the event item', () => {
expect(wrapper.find(EventItem).props('author')).toBe(feedback.author);
});
it('should pass the created date to the event item', () => {
expect(wrapper.find(EventItem).props('createdAt')).toBe(feedback.created_at);
});
it('should return the event text with no project data', () => {
expect(wrapper.text()).toBe(`Created issue #${feedback.issue_iid}`);
});
});
describe('with an attached project', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback, project },
});
});
it('should return the event text with project data', () => {
expect(wrapper.text()).toBe(`Created issue #${feedback.issue_iid} at ${project.value}`);
});
});
describe('with unsafe data', () => {
let wrapper;
const unsafeProject = {
...project,
value: 'Foo <script>alert("XSS")</script>',
};
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: {
feedback,
project: unsafeProject,
},
});
});
it('should escape the project name', () => {
// Note: We have to check the computed prop here because
// vue test utils unescapes the result of wrapper.text()
expect(wrapper.vm.eventText).not.toContain(project.value);
expect(wrapper.vm.eventText).toContain(
'Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;',
);
});
});
});
import { shallowMount } from '@vue/test-utils';
import component from 'ee/vue_shared/security_reports/components/merge_request_note.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
describe('Merge request note', () => {
const now = new Date();
const feedback = {
author: {
name: 'Tanuki',
username: 'gitlab',
},
merge_request_path: '/path-to-the-issue',
merge_request_iid: 1,
created_at: now.toString(),
};
const project = {
value: 'Project one',
url: '/path-to-the-project',
};
describe('with no attached project', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback },
});
});
it('should pass the author to the event item', () => {
expect(wrapper.find(EventItem).props('author')).toBe(feedback.author);
});
it('should pass the created date to the event item', () => {
expect(wrapper.find(EventItem).props('createdAt')).toBe(feedback.created_at);
});
it('should return the event text with no project data', () => {
expect(wrapper.text()).toBe(`Created merge request !${feedback.merge_request_iid}`);
});
});
describe('with an attached project', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: { feedback, project },
});
});
it('should return the event text with project data', () => {
expect(wrapper.text()).toBe(
`Created merge request !${feedback.merge_request_iid} at ${project.value}`,
);
});
});
describe('with unsafe data', () => {
let wrapper;
const unsafeProject = {
...project,
value: 'Foo <script>alert("XSS")</script>',
};
beforeEach(() => {
wrapper = shallowMount(component, {
propsData: {
feedback,
project: unsafeProject,
},
});
});
it('should escape the project name', () => {
// Note: We have to check the computed prop here because
// vue test utils unescapes the result of wrapper.text()
expect(wrapper.vm.eventText).not.toContain(project.value);
expect(wrapper.vm.eventText).toContain(
'Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;',
);
});
});
});
...@@ -20,13 +20,14 @@ describe('Security Reports modal', () => { ...@@ -20,13 +20,14 @@ describe('Security Reports modal', () => {
}; };
props.modal.vulnerability.isDismissed = true; props.modal.vulnerability.isDismissed = true;
props.modal.vulnerability.dismissalFeedback = { props.modal.vulnerability.dismissalFeedback = {
author: { username: 'jsmith' }, author: { username: 'jsmith', name: 'John Smith' },
pipeline: { id: '123' }, pipeline: { id: '123', path: '#' },
}; };
vm = mountComponent(Component, props); vm = mountComponent(Component, props);
}); });
it('renders dismissal author and associated pipeline', () => { it('renders dismissal author and associated pipeline', () => {
expect(vm.$el.textContent.trim()).toContain('John Smith');
expect(vm.$el.textContent.trim()).toContain('@jsmith'); expect(vm.$el.textContent.trim()).toContain('@jsmith');
expect(vm.$el.textContent.trim()).toContain('#123'); expect(vm.$el.textContent.trim()).toContain('#123');
}); });
......
...@@ -3443,6 +3443,18 @@ msgstr "" ...@@ -3443,6 +3443,18 @@ msgstr ""
msgid "Created by me" msgid "Created by me"
msgstr "" msgstr ""
msgid "Created issue %{issueLink}"
msgstr ""
msgid "Created issue %{issueLink} at %{projectLink}"
msgstr ""
msgid "Created merge request %{mergeRequestLink}"
msgstr ""
msgid "Created merge request %{mergeRequestLink} at %{projectLink}"
msgstr ""
msgid "Created on" msgid "Created on"
msgstr "" msgstr ""
...@@ -3970,6 +3982,18 @@ msgstr "" ...@@ -3970,6 +3982,18 @@ msgstr ""
msgid "Dismiss trial promotion" msgid "Dismiss trial promotion"
msgstr "" msgstr ""
msgid "Dismissed"
msgstr ""
msgid "Dismissed at %{projectLink}"
msgstr ""
msgid "Dismissed on pipeline %{pipelineLink}"
msgstr ""
msgid "Dismissed on pipeline %{pipelineLink} at %{projectLink}"
msgstr ""
msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?" msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?"
msgstr "" msgstr ""
...@@ -9869,12 +9893,6 @@ msgstr "" ...@@ -9869,12 +9893,6 @@ msgstr ""
msgid "Reports|Vulnerability" msgid "Reports|Vulnerability"
msgstr "" msgstr ""
msgid "Reports|issue"
msgstr ""
msgid "Reports|merge request"
msgstr ""
msgid "Reports|no changed test results" msgid "Reports|no changed test results"
msgstr "" msgstr ""
...@@ -13815,9 +13833,6 @@ msgstr "" ...@@ -13815,9 +13833,6 @@ msgstr ""
msgid "among other things" msgid "among other things"
msgstr "" msgstr ""
msgid "at"
msgstr ""
msgid "attach a new file" msgid "attach a new file"
msgstr "" msgstr ""
...@@ -13965,9 +13980,6 @@ msgstr "" ...@@ -13965,9 +13980,6 @@ msgstr ""
msgid "ciReport|Create merge request" msgid "ciReport|Create merge request"
msgstr "" msgstr ""
msgid "ciReport|Created %{eventType}"
msgstr ""
msgid "ciReport|DAST" msgid "ciReport|DAST"
msgstr "" msgstr ""
...@@ -13986,9 +13998,6 @@ msgstr "" ...@@ -13986,9 +13998,6 @@ msgstr ""
msgid "ciReport|Dismiss vulnerability" msgid "ciReport|Dismiss vulnerability"
msgstr "" msgstr ""
msgid "ciReport|Dismissed by"
msgstr ""
msgid "ciReport|Download and apply the patch to fix this vulnerability." msgid "ciReport|Download and apply the patch to fix this vulnerability."
msgstr "" msgstr ""
...@@ -14127,9 +14136,6 @@ msgstr[1] "" ...@@ -14127,9 +14136,6 @@ msgstr[1] ""
msgid "ciReport|View full report" msgid "ciReport|View full report"
msgstr "" msgstr ""
msgid "ciReport|on pipeline"
msgstr ""
msgid "commented on %{link_to_project}" msgid "commented on %{link_to_project}"
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