Commit e01072df authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '221260-refresh-required-to-see-status-change-after-page-load' into 'master'

Refresh vulnerability state and timestamp when changed by other user

See merge request gitlab-org/gitlab!34837
parents 853b1f62 30e2a55a
...@@ -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.
});
});
}); });
...@@ -25434,6 +25434,9 @@ msgstr "" ...@@ -25434,6 +25434,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