Commit 70c69ec8 authored by Alexander Turinske's avatar Alexander Turinske

Add polling for vulnerability comments and state

- add polling component for comments
- refetch discussions if a note with unknown discussion
  id comes in
- update X-Last-Fetched-At to only get latest comments
- use newly available discussion url and notes url for
  updates
parent c34113e5
......@@ -34,7 +34,7 @@ function createFooterApp() {
return false;
}
const { vulnerabilityFeedbackHelpPath, hasMr, discussionsUrl } = el.dataset;
const { vulnerabilityFeedbackHelpPath, hasMr, discussionsUrl, notesUrl, timestamp } = el.dataset;
const vulnerability = JSON.parse(el.dataset.vulnerabilityJson);
const finding = JSON.parse(el.dataset.findingJson);
const { issue_feedback: feedback, remediation, solution } = finding;
......@@ -44,6 +44,7 @@ function createFooterApp() {
const props = {
discussionsUrl,
notesUrl,
solutionInfo: {
solution,
remediation,
......@@ -53,6 +54,7 @@ function createFooterApp() {
vulnerabilityFeedbackHelpPath,
isStandaloneVulnerability: true,
},
timestamp,
feedback,
project: {
url: finding.project.full_path,
......
<script>
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
......@@ -20,6 +22,10 @@ export default {
required: false,
default: null,
},
notesUrl: {
type: String,
required: true,
},
project: {
type: Object,
required: true,
......@@ -28,13 +34,23 @@ export default {
type: Object,
required: true,
},
timestamp: {
type: String,
required: true,
},
},
data: () => ({
discussions: [],
}),
data() {
return {
discussions: {},
poll: null,
};
},
computed: {
discussionsValues() {
return Object.values(this.discussions);
},
hasIssue() {
return Boolean(this.feedback?.issue_iid);
},
......@@ -44,17 +60,25 @@ export default {
},
created() {
this.createNotesPoll();
this.fetchDiscussions();
VulnerabilitiesEventBus.$on('VULNERABILITY_STATE_CHANGE', this.fetchDiscussions);
},
beforeDestroy() {
this.poll.stop();
},
methods: {
fetchDiscussions() {
axios
.get(this.discussionsUrl)
.then(({ data }) => {
this.discussions = data;
this.discussions = data.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
})
.catch(() => {
createFlash(
......@@ -62,8 +86,90 @@ export default {
'VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later.',
),
);
})
.finally(() => {
if (!Visibility.hidden()) {
this.poll.enable();
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
});
},
createNotesPoll() {
// Create headers object to update the X-Last-Fetched-At property on each update
const headers = {
'X-Last-Fetched-At': parseInt(this.timestamp, 10),
};
this.poll = new Poll({
resource: {
fetchNotes: data => axios(data),
},
method: 'fetchNotes',
data: {
method: 'get',
url: this.notesUrl,
headers,
},
successCallback: ({ data }) => {
const { notes } = data;
if (!notes.length) return;
const updatedDiscussions = this.getUpdatedDiscussions(this.discussions, notes);
this.discussions = { ...this.discussions, ...updatedDiscussions };
headers['X-Last-Fetched-At'] = data.last_fetched_at;
},
errorCallback: () =>
createFlash(
s__(
'VulnerabilityManagement|Something went wrong while fetching latest comments. Please try again later.',
),
),
});
},
getUpdatedDiscussions(discussions, notes) {
return notes.reduce((acc, note) => {
const discussion = discussions[note.discussion_id];
if (!discussion) {
this.poll.stop();
this.fetchDiscussions();
} else {
const newDiscussion = this.updateDiscussion(discussion, note);
acc[newDiscussion.id] = newDiscussion;
}
return acc;
}, {});
},
updateDiscussion(discussion, note) {
const newDiscussion = { ...discussion };
const { existingNote, index } = this.getExistingNote(discussion, note);
if (existingNote) {
newDiscussion.notes.splice(index, 1, note);
} else {
newDiscussion.notes.push(note);
}
return newDiscussion;
},
getExistingNote(discussion, note) {
let index = -1;
const existingNote = discussion.notes.find((dnote, i) => {
if (dnote.id === note.id) {
index = i;
return true;
}
return false;
});
return { index, existingNote };
},
},
};
</script>
......@@ -75,11 +181,12 @@ export default {
</div>
<hr />
<ul v-if="discussions.length" ref="historyList" class="notes discussion-body">
<ul v-if="discussionsValues.length" ref="historyList" class="notes discussion-body">
<history-entry
v-for="discussion in discussions"
v-for="discussion in discussionsValues"
:key="discussion.id"
:discussion="discussion"
:notes-url="notesUrl"
/>
</ul>
</div>
......
......@@ -27,6 +27,10 @@ export default {
required: false,
default: undefined,
},
notesUrl: {
type: String,
required: true,
},
},
data() {
......@@ -39,6 +43,9 @@ export default {
},
computed: {
noteIdUrl() {
return joinPaths(this.notesUrl, this.comment.id);
},
commentNote() {
return this.comment?.note;
},
......@@ -65,13 +72,13 @@ export default {
getSaveConfig(note) {
const isUpdatingComment = Boolean(this.comment);
const method = isUpdatingComment ? 'put' : 'post';
let url = joinPaths(window.location.pathname, 'notes');
let url = this.notesUrl;
const data = { note: { note } };
const emitName = isUpdatingComment ? 'onCommentUpdated' : 'onCommentAdded';
// If we're updating the comment, use the comment ID in the URL. Otherwise, use the discussion ID in the request data.
if (isUpdatingComment) {
url = joinPaths(url, this.comment.id);
url = this.noteIdUrl;
} else {
data.in_reply_to_discussion_id = this.discussionId;
}
......@@ -100,7 +107,7 @@ export default {
},
deleteComment() {
this.isDeletingComment = true;
const deleteUrl = joinPaths(window.location.pathname, 'notes', this.comment.id);
const deleteUrl = this.noteIdUrl;
axios
.delete(deleteUrl)
......
......@@ -9,6 +9,10 @@ export default {
type: Object,
required: true,
},
notesUrl: {
type: String,
required: true,
},
},
data() {
return {
......@@ -66,6 +70,7 @@ export default {
ref="existingComment"
:comment="comment"
:discussion-id="discussion.reply_id"
:notes-url="notesUrl"
@onCommentUpdated="updateComment"
@onCommentDeleted="removeComment"
/>
......@@ -75,6 +80,7 @@ export default {
v-else
ref="newComment"
:discussion-id="discussion.reply_id"
:notes-url="notesUrl"
@onCommentAdded="addComment"
/>
</li>
......
......@@ -24382,6 +24382,9 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while fetching latest comments. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
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