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 {
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.
});
});
});
......@@ -25434,6 +25434,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