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 {
paymentFormPath: '/-/subscriptions/payment_form',
paymentMethodPath: '/-/subscriptions/payment_method',
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',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
......@@ -290,8 +291,13 @@ export default {
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) {
const url = Api.buildUrl(this.vulnerabilitiesActionPath)
const url = Api.buildUrl(this.vulnerabilityActionPath)
.replace(':id', id)
.replace(':action', state);
......
......@@ -127,6 +127,8 @@ export default {
});
},
updateNotes(notes) {
let isVulnerabilityStateChanged = false;
notes.forEach(note => {
// If the note exists, update it.
if (this.noteDictionary[note.id]) {
......@@ -150,8 +152,18 @@ export default {
notes: [note],
};
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 @@
import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import Api from 'ee/api';
import axios from '~/lib/utils/axios_utils';
import { CancelToken } from 'axios';
import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
......@@ -39,6 +40,7 @@ export default {
isLoadingUser: false,
vulnerability: this.initialVulnerability,
user: undefined,
refreshVulnerabilitySource: undefined,
};
},
......@@ -117,6 +119,14 @@ export default {
},
},
created() {
VulnerabilitiesEventBus.$on('VULNERABILITY_STATE_CHANGED', this.refreshVulnerability);
},
destroyed() {
VulnerabilitiesEventBus.$off('VULNERABILITY_STATE_CHANGED', this.refreshVulnerability);
},
methods: {
triggerClick(action) {
const fn = this[action];
......@@ -211,6 +221,37 @@ export default {
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>
......
---
title: Refresh vulnerability state and timestamp when changed by another user
merge_request: 34837
author:
type: fixed
......@@ -141,10 +141,10 @@ describe('Vulnerability Footer', () => {
describe('new notes polling', () => {
const getDiscussion = (entries, index) => entries.at(index).props('discussion');
const createNotesRequest = note =>
const createNotesRequest = (...notes) =>
mockAxios
.onGet(minimumProps.notesUrl)
.replyOnce(200, { notes: [note], last_fetched_at: Date.now() });
.replyOnce(200, { notes, last_fetched_at: Date.now() });
beforeEach(() => {
const historyItems = [
......@@ -203,6 +203,16 @@ describe('Vulnerability Footer', () => {
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';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
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 download from '~/lib/utils/downloader';
import * as urlUtility from '~/lib/utils/url_utility';
......@@ -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', () => {
const newState = 'dismiss';
jest.spyOn(VulnerabilitiesEventBus, '$emit');
const spy = jest.spyOn(VulnerabilitiesEventBus, '$emit');
mockAxios.onPost().reply(201, { state: newState });
expect(findBadge().text()).not.toBe(newState);
......@@ -129,8 +129,8 @@ describe('Vulnerability Header', () => {
dropdown.vm.$emit('change');
return waitForPromises().then(() => {
expect(VulnerabilitiesEventBus.$emit).toHaveBeenCalledTimes(1);
expect(VulnerabilitiesEventBus.$emit).toHaveBeenCalledWith('VULNERABILITY_STATE_CHANGE');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('VULNERABILITY_STATE_CHANGE');
});
});
......@@ -354,19 +354,12 @@ describe('Vulnerability Header', () => {
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();
it('the resolution alert component should not be shown if when the vulnerability is already resolved', async () => {
wrapper.vm.vulnerability.state = 'resolved';
await wrapper.vm.$nextTick();
const alert = findResolutionAlert();
expect(alert.exists()).toBe(false);
});
expect(alert.exists()).toBe(false);
});
});
......@@ -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 ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
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."
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