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() { ...@@ -34,7 +34,7 @@ function createFooterApp() {
return false; return false;
} }
const { vulnerabilityFeedbackHelpPath, hasMr, discussionsUrl } = el.dataset; const { vulnerabilityFeedbackHelpPath, hasMr, discussionsUrl, notesUrl, timestamp } = el.dataset;
const vulnerability = JSON.parse(el.dataset.vulnerabilityJson); const vulnerability = JSON.parse(el.dataset.vulnerabilityJson);
const finding = JSON.parse(el.dataset.findingJson); const finding = JSON.parse(el.dataset.findingJson);
const { issue_feedback: feedback, remediation, solution } = finding; const { issue_feedback: feedback, remediation, solution } = finding;
...@@ -44,6 +44,7 @@ function createFooterApp() { ...@@ -44,6 +44,7 @@ function createFooterApp() {
const props = { const props = {
discussionsUrl, discussionsUrl,
notesUrl,
solutionInfo: { solutionInfo: {
solution, solution,
remediation, remediation,
...@@ -53,6 +54,7 @@ function createFooterApp() { ...@@ -53,6 +54,7 @@ function createFooterApp() {
vulnerabilityFeedbackHelpPath, vulnerabilityFeedbackHelpPath,
isStandaloneVulnerability: true, isStandaloneVulnerability: true,
}, },
timestamp,
feedback, feedback,
project: { project: {
url: finding.project.full_path, url: finding.project.full_path,
......
<script> <script>
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue'; import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
...@@ -20,6 +22,10 @@ export default { ...@@ -20,6 +22,10 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
notesUrl: {
type: String,
required: true,
},
project: { project: {
type: Object, type: Object,
required: true, required: true,
...@@ -28,13 +34,23 @@ export default { ...@@ -28,13 +34,23 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
timestamp: {
type: String,
required: true,
},
}, },
data: () => ({ data() {
discussions: [], return {
}), discussions: {},
poll: null,
};
},
computed: { computed: {
discussionsValues() {
return Object.values(this.discussions);
},
hasIssue() { hasIssue() {
return Boolean(this.feedback?.issue_iid); return Boolean(this.feedback?.issue_iid);
}, },
...@@ -44,17 +60,25 @@ export default { ...@@ -44,17 +60,25 @@ export default {
}, },
created() { created() {
this.createNotesPoll();
this.fetchDiscussions(); this.fetchDiscussions();
VulnerabilitiesEventBus.$on('VULNERABILITY_STATE_CHANGE', this.fetchDiscussions); VulnerabilitiesEventBus.$on('VULNERABILITY_STATE_CHANGE', this.fetchDiscussions);
}, },
beforeDestroy() {
this.poll.stop();
},
methods: { methods: {
fetchDiscussions() { fetchDiscussions() {
axios axios
.get(this.discussionsUrl) .get(this.discussionsUrl)
.then(({ data }) => { .then(({ data }) => {
this.discussions = data; this.discussions = data.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
}) })
.catch(() => { .catch(() => {
createFlash( createFlash(
...@@ -62,8 +86,90 @@ export default { ...@@ -62,8 +86,90 @@ export default {
'VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later.', '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> </script>
...@@ -75,11 +181,12 @@ export default { ...@@ -75,11 +181,12 @@ export default {
</div> </div>
<hr /> <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 <history-entry
v-for="discussion in discussions" v-for="discussion in discussionsValues"
:key="discussion.id" :key="discussion.id"
:discussion="discussion" :discussion="discussion"
:notes-url="notesUrl"
/> />
</ul> </ul>
</div> </div>
......
...@@ -27,6 +27,10 @@ export default { ...@@ -27,6 +27,10 @@ export default {
required: false, required: false,
default: undefined, default: undefined,
}, },
notesUrl: {
type: String,
required: true,
},
}, },
data() { data() {
...@@ -39,6 +43,9 @@ export default { ...@@ -39,6 +43,9 @@ export default {
}, },
computed: { computed: {
noteIdUrl() {
return joinPaths(this.notesUrl, this.comment.id);
},
commentNote() { commentNote() {
return this.comment?.note; return this.comment?.note;
}, },
...@@ -65,13 +72,13 @@ export default { ...@@ -65,13 +72,13 @@ export default {
getSaveConfig(note) { getSaveConfig(note) {
const isUpdatingComment = Boolean(this.comment); const isUpdatingComment = Boolean(this.comment);
const method = isUpdatingComment ? 'put' : 'post'; const method = isUpdatingComment ? 'put' : 'post';
let url = joinPaths(window.location.pathname, 'notes'); let url = this.notesUrl;
const data = { note: { note } }; const data = { note: { note } };
const emitName = isUpdatingComment ? 'onCommentUpdated' : 'onCommentAdded'; 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 we're updating the comment, use the comment ID in the URL. Otherwise, use the discussion ID in the request data.
if (isUpdatingComment) { if (isUpdatingComment) {
url = joinPaths(url, this.comment.id); url = this.noteIdUrl;
} else { } else {
data.in_reply_to_discussion_id = this.discussionId; data.in_reply_to_discussion_id = this.discussionId;
} }
...@@ -100,7 +107,7 @@ export default { ...@@ -100,7 +107,7 @@ export default {
}, },
deleteComment() { deleteComment() {
this.isDeletingComment = true; this.isDeletingComment = true;
const deleteUrl = joinPaths(window.location.pathname, 'notes', this.comment.id); const deleteUrl = this.noteIdUrl;
axios axios
.delete(deleteUrl) .delete(deleteUrl)
......
...@@ -9,6 +9,10 @@ export default { ...@@ -9,6 +9,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
notesUrl: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -66,6 +70,7 @@ export default { ...@@ -66,6 +70,7 @@ export default {
ref="existingComment" ref="existingComment"
:comment="comment" :comment="comment"
:discussion-id="discussion.reply_id" :discussion-id="discussion.reply_id"
:notes-url="notesUrl"
@onCommentUpdated="updateComment" @onCommentUpdated="updateComment"
@onCommentDeleted="removeComment" @onCommentDeleted="removeComment"
/> />
...@@ -75,6 +80,7 @@ export default { ...@@ -75,6 +80,7 @@ export default {
v-else v-else
ref="newComment" ref="newComment"
:discussion-id="discussion.reply_id" :discussion-id="discussion.reply_id"
:notes-url="notesUrl"
@onCommentAdded="addComment" @onCommentAdded="addComment"
/> />
</li> </li>
......
...@@ -24382,6 +24382,9 @@ msgstr "" ...@@ -24382,6 +24382,9 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}" msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr "" 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." msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
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