Commit 30e2a55a authored by Daniel Tian's avatar Daniel Tian

Refresh vuln state when changed by other user

Refresh the vulnerability state and timestamp when state is changed by
another user
parent cc687bc8
...@@ -35,7 +35,8 @@ export default { ...@@ -35,7 +35,8 @@ export default {
paymentFormPath: '/-/subscriptions/payment_form', paymentFormPath: '/-/subscriptions/payment_form',
paymentMethodPath: '/-/subscriptions/payment_method', paymentMethodPath: '/-/subscriptions/payment_method',
confirmOrderPath: '/-/subscriptions', confirmOrderPath: '/-/subscriptions',
vulnerabilitiesActionPath: '/api/:version/vulnerabilities/:id/:action', vulnerabilityPath: '/api/:version/vulnerabilities/:id',
vulnerabilityActionPath: '/api/:version/vulnerabilities/:id/:action',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists', featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
...@@ -290,8 +291,13 @@ export default { ...@@ -290,8 +291,13 @@ export default {
return axios.post(url, params); return axios.post(url, params);
}, },
fetchVulnerability(id, params) {
const url = Api.buildUrl(this.vulnerabilityPath).replace(':id', id);
return axios.get(url, params);
},
changeVulnerabilityState(id, state) { changeVulnerabilityState(id, state) {
const url = Api.buildUrl(this.vulnerabilitiesActionPath) const url = Api.buildUrl(this.vulnerabilityActionPath)
.replace(':id', id) .replace(':id', id)
.replace(':action', state); .replace(':action', state);
......
...@@ -127,6 +127,8 @@ export default { ...@@ -127,6 +127,8 @@ export default {
}); });
}, },
updateNotes(notes) { updateNotes(notes) {
let isVulnerabilityStateChanged = false;
notes.forEach(note => { notes.forEach(note => {
// If the note exists, update it. // If the note exists, update it.
if (this.noteDictionary[note.id]) { if (this.noteDictionary[note.id]) {
...@@ -150,8 +152,18 @@ export default { ...@@ -150,8 +152,18 @@ export default {
notes: [note], notes: [note],
}; };
this.$set(this.discussionsDictionary, newDiscussion.id, newDiscussion); this.$set(this.discussionsDictionary, newDiscussion.id, newDiscussion);
// If the vulnerability status has changed, the note will be a system note.
if (note.system === true) {
isVulnerabilityStateChanged = true;
}
} }
}); });
// Emit an event that tells the header to refresh the vulnerability.
if (isVulnerabilityStateChanged) {
VulnerabilitiesEventBus.$emit('VULNERABILITY_STATE_CHANGED');
}
}, },
}, },
}; };
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import Api from 'ee/api'; import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { CancelToken } from 'axios';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -39,6 +40,7 @@ export default { ...@@ -39,6 +40,7 @@ export default {
isLoadingUser: false, isLoadingUser: false,
vulnerability: this.initialVulnerability, vulnerability: this.initialVulnerability,
user: undefined, user: undefined,
refreshVulnerabilitySource: undefined,
}; };
}, },
...@@ -117,6 +119,14 @@ export default { ...@@ -117,6 +119,14 @@ export default {
}, },
}, },
created() {
VulnerabilitiesEventBus.$on('VULNERABILITY_STATE_CHANGED', this.refreshVulnerability);
},
destroyed() {
VulnerabilitiesEventBus.$off('VULNERABILITY_STATE_CHANGED', this.refreshVulnerability);
},
methods: { methods: {
triggerClick(action) { triggerClick(action) {
const fn = this[action]; const fn = this[action];
...@@ -211,6 +221,37 @@ export default { ...@@ -211,6 +221,37 @@ export default {
fileName: `remediation.patch`, fileName: `remediation.patch`,
}); });
}, },
refreshVulnerability() {
this.isLoadingVulnerability = true;
// Cancel any pending API requests.
if (this.refreshVulnerabilitySource) {
this.refreshVulnerabilitySource.cancel();
}
this.refreshVulnerabilitySource = CancelToken.source();
Api.fetchVulnerability(this.vulnerability.id, {
cancelToken: this.refreshVulnerabilitySource.token,
})
.then(({ data }) => {
Object.assign(this.vulnerability, data);
})
.catch(e => {
// Don't show an error message if the request was cancelled through the cancel token.
if (!axios.isCancel(e)) {
createFlash(
s__(
'VulnerabilityManagement|Something went wrong while trying to refresh the vulnerability. Please try again later.',
),
);
}
})
.finally(() => {
this.isLoadingVulnerability = false;
this.refreshVulnerabilitySource = undefined;
});
},
}, },
}; };
</script> </script>
......
---
title: Refresh vulnerability state and timestamp when changed by another user
merge_request: 34837
author:
type: fixed
...@@ -141,10 +141,10 @@ describe('Vulnerability Footer', () => { ...@@ -141,10 +141,10 @@ describe('Vulnerability Footer', () => {
describe('new notes polling', () => { describe('new notes polling', () => {
const getDiscussion = (entries, index) => entries.at(index).props('discussion'); const getDiscussion = (entries, index) => entries.at(index).props('discussion');
const createNotesRequest = note => const createNotesRequest = (...notes) =>
mockAxios mockAxios
.onGet(minimumProps.notesUrl) .onGet(minimumProps.notesUrl)
.replyOnce(200, { notes: [note], last_fetched_at: Date.now() }); .replyOnce(200, { notes, last_fetched_at: Date.now() });
beforeEach(() => { beforeEach(() => {
const historyItems = [ const historyItems = [
...@@ -203,6 +203,16 @@ describe('Vulnerability Footer', () => { ...@@ -203,6 +203,16 @@ describe('Vulnerability Footer', () => {
expect(createFlash).toHaveBeenCalled(); expect(createFlash).toHaveBeenCalled();
}); });
}); });
it('emits the VULNERABILITY_STATE_CHANGED event when the system note is new', async () => {
const spy = jest.spyOn(VulnerabilitiesEventBus, '$emit');
const note = { system: true, id: 1, discussion_id: 3 };
createNotesRequest(note);
await axios.waitForAll();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('VULNERABILITY_STATE_CHANGED');
});
}); });
}); });
}); });
...@@ -3,7 +3,7 @@ import { GlDeprecatedButton } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlDeprecatedButton } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import UsersMockHelper from 'helpers/user_mock_data_helper'; import UsersMockHelper from 'helpers/user_mock_data_helper';
import Api from '~/api'; import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
import * as urlUtility from '~/lib/utils/url_utility'; import * as urlUtility from '~/lib/utils/url_utility';
...@@ -120,7 +120,7 @@ describe('Vulnerability Header', () => { ...@@ -120,7 +120,7 @@ describe('Vulnerability Header', () => {
it('when the vulnerability state dropdown emits a change event, the vulnerabilities event bus event is emitted with the proper event', () => { it('when the vulnerability state dropdown emits a change event, the vulnerabilities event bus event is emitted with the proper event', () => {
const newState = 'dismiss'; const newState = 'dismiss';
jest.spyOn(VulnerabilitiesEventBus, '$emit'); const spy = jest.spyOn(VulnerabilitiesEventBus, '$emit');
mockAxios.onPost().reply(201, { state: newState }); mockAxios.onPost().reply(201, { state: newState });
expect(findBadge().text()).not.toBe(newState); expect(findBadge().text()).not.toBe(newState);
...@@ -129,8 +129,8 @@ describe('Vulnerability Header', () => { ...@@ -129,8 +129,8 @@ describe('Vulnerability Header', () => {
dropdown.vm.$emit('change'); dropdown.vm.$emit('change');
return waitForPromises().then(() => { return waitForPromises().then(() => {
expect(VulnerabilitiesEventBus.$emit).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
expect(VulnerabilitiesEventBus.$emit).toHaveBeenCalledWith('VULNERABILITY_STATE_CHANGE'); expect(spy).toHaveBeenCalledWith('VULNERABILITY_STATE_CHANGE');
}); });
}); });
...@@ -354,19 +354,12 @@ describe('Vulnerability Header', () => { ...@@ -354,19 +354,12 @@ describe('Vulnerability Header', () => {
expect(alert.props().defaultBranchName).toEqual(branchName); expect(alert.props().defaultBranchName).toEqual(branchName);
}); });
describe('when the vulnerability is already resolved', () => { it('the resolution alert component should not be shown if when the vulnerability is already resolved', async () => {
beforeEach(() => { wrapper.vm.vulnerability.state = 'resolved';
createWrapper({ await wrapper.vm.$nextTick();
resolved_on_default_branch: true, const alert = findResolutionAlert();
state: 'resolved',
});
});
it('should not show the resolution alert component', () => {
const alert = findResolutionAlert();
expect(alert.exists()).toBe(false); expect(alert.exists()).toBe(false);
});
}); });
}); });
...@@ -416,4 +409,45 @@ describe('Vulnerability Header', () => { ...@@ -416,4 +409,45 @@ describe('Vulnerability Header', () => {
}); });
}); });
}); });
describe('when vulnerability state is changed', () => {
it('refreshes the vulnerability', async () => {
const url = Api.buildUrl(Api.vulnerabilityPath).replace(':id', defaultVulnerability.id);
const vulnerability = { state: 'dismissed' };
mockAxios.onGet(url).replyOnce(200, vulnerability);
createWrapper();
VulnerabilitiesEventBus.$emit('VULNERABILITY_STATE_CHANGED');
await waitForPromises();
expect(findBadge().text()).toBe(vulnerability.state);
expect(findStatusDescription().props('vulnerability')).toMatchObject(vulnerability);
});
it('shows an error message when the vulnerability cannot be loaded', async () => {
mockAxios.onGet().replyOnce(500);
createWrapper();
VulnerabilitiesEventBus.$emit('VULNERABILITY_STATE_CHANGED');
await waitForPromises();
expect(createFlash).toHaveBeenCalledTimes(1);
expect(mockAxios.history.get).toHaveLength(1);
});
it('cancels a pending refresh request if the vulnerability state has changed', async () => {
mockAxios.onGet().reply(200);
createWrapper();
VulnerabilitiesEventBus.$emit('VULNERABILITY_STATE_CHANGED');
const source = wrapper.vm.refreshVulnerabilitySource;
const spy = jest.spyOn(source, 'cancel');
VulnerabilitiesEventBus.$emit('VULNERABILITY_STATE_CHANGED');
await waitForPromises();
expect(createFlash).toHaveBeenCalledTimes(0);
expect(mockAxios.history.get).toHaveLength(1);
expect(spy).toHaveBeenCalled();
expect(wrapper.vm.refreshVulnerabilitySource).not.toBe(source); // Check that the source has changed.
});
});
}); });
...@@ -25416,6 +25416,9 @@ msgstr "" ...@@ -25416,6 +25416,9 @@ 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 ""
msgid "VulnerabilityManagement|Something went wrong while trying to refresh the vulnerability. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later." msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. 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