Commit 2ae55374 authored by Clement Ho's avatar Clement Ho

Merge branch '196881-reverse-actions-for-status-update' into 'master'

Reverse actions for ignoring/resolving errors

See merge request gitlab-org/gitlab!23516
parents 0cf1bca0 69ea7ef6
...@@ -13,3 +13,9 @@ export const severityLevelVariant = { ...@@ -13,3 +13,9 @@ export const severityLevelVariant = {
[severityLevel.INFO]: 'info', [severityLevel.INFO]: 'info',
[severityLevel.DEBUG]: 'light', [severityLevel.DEBUG]: 'light',
}; };
export const errorStatus = {
IGNORED: 'ignored',
RESOLVED: 'resolved',
UNRESOLVED: 'unresolved',
};
...@@ -11,7 +11,7 @@ import Stacktrace from './stacktrace.vue'; ...@@ -11,7 +11,7 @@ import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils'; import { trackClickErrorLinkToSentryOptions } from '../utils';
import { severityLevel, severityLevelVariant } from './constants'; import { severityLevel, severityLevelVariant, errorStatus } from './constants';
import query from '../queries/details.query.graphql'; import query from '../queries/details.query.graphql';
...@@ -32,10 +32,6 @@ export default { ...@@ -32,10 +32,6 @@ export default {
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
listPath: {
type: String,
required: true,
},
issueUpdatePath: { issueUpdatePath: {
type: String, type: String,
required: true, required: true,
...@@ -80,6 +76,7 @@ export default { ...@@ -80,6 +76,7 @@ export default {
result(res) { result(res) {
if (res.data.project?.sentryDetailedError) { if (res.data.project?.sentryDetailedError) {
this.$apollo.queries.GQLerror.stopPolling(); this.$apollo.queries.GQLerror.stopPolling();
this.setStatus(this.GQLerror.status);
} }
}, },
}, },
...@@ -98,6 +95,7 @@ export default { ...@@ -98,6 +95,7 @@ export default {
'stacktraceData', 'stacktraceData',
'updatingResolveStatus', 'updatingResolveStatus',
'updatingIgnoreStatus', 'updatingIgnoreStatus',
'errorStatus',
]), ]),
...mapGetters('details', ['stacktrace']), ...mapGetters('details', ['stacktrace']),
reported() { reported() {
...@@ -153,20 +151,40 @@ export default { ...@@ -153,20 +151,40 @@ export default {
severityLevelVariant[this.error.tags.level] || severityLevelVariant[severityLevel.ERROR] severityLevelVariant[this.error.tags.level] || severityLevelVariant[severityLevel.ERROR]
); );
}, },
ignoreBtnLabel() {
return this.errorStatus !== errorStatus.IGNORED ? __('Ignore') : __('Undo ignore');
},
resolveBtnLabel() {
return this.errorStatus !== errorStatus.RESOLVED ? __('Resolve') : __('Unresolve');
},
}, },
mounted() { mounted() {
this.startPollingDetails(this.issueDetailsPath); this.startPollingDetails(this.issueDetailsPath);
this.startPollingStacktrace(this.issueStackTracePath); this.startPollingStacktrace(this.issueStackTracePath);
}, },
methods: { methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace', 'updateStatus']), ...mapActions('details', [
'startPollingDetails',
'startPollingStacktrace',
'updateStatus',
'setStatus',
'updateResolveStatus',
'updateIgnoreStatus',
]),
trackClickErrorLinkToSentryOptions, trackClickErrorLinkToSentryOptions,
createIssue() { createIssue() {
this.issueCreationInProgress = true; this.issueCreationInProgress = true;
this.$refs.sentryIssueForm.submit(); this.$refs.sentryIssueForm.submit();
}, },
updateIssueStatus(status) { onIgnoreStatusUpdate() {
this.updateStatus({ endpoint: this.issueUpdatePath, redirectUrl: this.listPath, status }); const status =
this.errorStatus === errorStatus.IGNORED ? errorStatus.UNRESOLVED : errorStatus.IGNORED;
this.updateIgnoreStatus({ endpoint: this.issueUpdatePath, status });
},
onResolveStatusUpdate() {
const status =
this.errorStatus === errorStatus.RESOLVED ? errorStatus.UNRESOLVED : errorStatus.RESOLVED;
this.updateResolveStatus({ endpoint: this.issueUpdatePath, status });
}, },
formatDate(date) { formatDate(date) {
return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`; return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
...@@ -185,15 +203,17 @@ export default { ...@@ -185,15 +203,17 @@ export default {
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<div class="d-inline-flex"> <div class="d-inline-flex">
<loading-button <loading-button
:label="__('Ignore')" :label="ignoreBtnLabel"
:loading="updatingIgnoreStatus" :loading="updatingIgnoreStatus"
@click="updateIssueStatus('ignored')" data-qa-selector="update_ignore_status_button"
@click="onIgnoreStatusUpdate"
/> />
<loading-button <loading-button
class="btn-outline-info ml-2" class="btn-outline-info ml-2"
:label="__('Resolve')" :label="resolveBtnLabel"
:loading="updatingResolveStatus" :loading="updatingResolveStatus"
@click="updateIssueStatus('resolved')" data-qa-selector="update_resolve_status_button"
@click="onResolveStatusUpdate"
/> />
<gl-button <gl-button
v-if="error.gitlab_issue" v-if="error.gitlab_issue"
......
...@@ -25,7 +25,6 @@ export default () => { ...@@ -25,7 +25,6 @@ export default () => {
const { const {
issueId, issueId,
projectPath, projectPath,
listPath,
issueUpdatePath, issueUpdatePath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
...@@ -36,7 +35,6 @@ export default () => { ...@@ -36,7 +35,6 @@ export default () => {
props: { props: {
issueId, issueId,
projectPath, projectPath,
listPath,
issueUpdatePath, issueUpdatePath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
......
...@@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) { ...@@ -6,6 +6,7 @@ query errorDetails($fullPath: ID!, $errorId: ID!) {
title title
userCount userCount
count count
status
firstSeen firstSeen
lastSeen lastSeen
message message
......
...@@ -4,16 +4,33 @@ import createFlash from '~/flash'; ...@@ -4,16 +4,33 @@ import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
export function updateStatus({ commit }, { endpoint, redirectUrl, status }) { export const setStatus = ({ commit }, status) => {
const type = commit(types.SET_ERROR_STATUS, status.toLowerCase());
status === 'resolved' ? types.SET_UPDATING_RESOLVE_STATUS : types.SET_UPDATING_IGNORE_STATUS; };
commit(type, true);
return service export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) =>
service
.updateErrorStatus(endpoint, status) .updateErrorStatus(endpoint, status)
.then(() => visitUrl(redirectUrl)) .then(() => {
.catch(() => createFlash(__('Failed to update issue status'))) if (redirectUrl) visitUrl(redirectUrl);
.finally(() => commit(type, false)); commit(types.SET_ERROR_STATUS, status);
} })
.catch(() => createFlash(__('Failed to update issue status')));
export const updateResolveStatus = ({ commit, dispatch }, params) => {
commit(types.SET_UPDATING_RESOLVE_STATUS, true);
return dispatch('updateStatus', params).finally(() => {
commit(types.SET_UPDATING_RESOLVE_STATUS, false);
});
};
export const updateIgnoreStatus = ({ commit, dispatch }, params) => {
commit(types.SET_UPDATING_IGNORE_STATUS, true);
return dispatch('updateStatus', params).finally(() => {
commit(types.SET_UPDATING_IGNORE_STATUS, false);
});
};
export default () => {}; export default () => {};
...@@ -5,4 +5,5 @@ export default () => ({ ...@@ -5,4 +5,5 @@ export default () => ({
loadingStacktrace: true, loadingStacktrace: true,
updatingResolveStatus: false, updatingResolveStatus: false,
updatingIgnoreStatus: false, updatingIgnoreStatus: false,
errorStatus: '',
}); });
export const SET_UPDATING_RESOLVE_STATUS = 'SET_UPDATING_RESOLVE_STATUS'; export const SET_UPDATING_RESOLVE_STATUS = 'SET_UPDATING_RESOLVE_STATUS';
export const SET_UPDATING_IGNORE_STATUS = 'SET_UPDATING_IGNORE_STATUS'; export const SET_UPDATING_IGNORE_STATUS = 'SET_UPDATING_IGNORE_STATUS';
export const SET_ERROR_STATUS = 'SET_ERROR_STATUS';
...@@ -7,4 +7,7 @@ export default { ...@@ -7,4 +7,7 @@ export default {
[types.SET_UPDATING_RESOLVE_STATUS](state, updating) { [types.SET_UPDATING_RESOLVE_STATUS](state, updating) {
state.updatingResolveStatus = updating; state.updatingResolveStatus = updating;
}, },
[types.SET_ERROR_STATUS](state, status) {
state.errorStatus = status;
},
}; };
...@@ -22,7 +22,6 @@ module Projects::ErrorTrackingHelper ...@@ -22,7 +22,6 @@ module Projects::ErrorTrackingHelper
{ {
'issue-id' => issue_id, 'issue-id' => issue_id,
'project-path' => project.full_path, 'project-path' => project.full_path,
'list-path' => project_error_tracking_index_path(project),
'issue-details-path' => details_project_error_tracking_index_path(*opts), 'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-update-path' => update_project_error_tracking_index_path(*opts), 'issue-update-path' => update_project_error_tracking_index_path(*opts),
'project-issues-path' => project_issues_path(project), 'project-issues-path' => project_issues_path(project),
......
---
title: Reverse actions for resolve/ignore Sentry issue
merge_request: 23516
author:
type: added
...@@ -20259,6 +20259,9 @@ msgstr "" ...@@ -20259,6 +20259,9 @@ msgstr ""
msgid "Undo" msgid "Undo"
msgstr "" msgstr ""
msgid "Undo ignore"
msgstr ""
msgid "Unfortunately, your email message to GitLab could not be processed." msgid "Unfortunately, your email message to GitLab could not be processed."
msgstr "" msgstr ""
...@@ -20310,6 +20313,9 @@ msgstr "" ...@@ -20310,6 +20313,9 @@ msgstr ""
msgid "Unmarks this %{noun} as Work In Progress." msgid "Unmarks this %{noun} as Work In Progress."
msgstr "" msgstr ""
msgid "Unresolve"
msgstr ""
msgid "Unresolve discussion" msgid "Unresolve discussion"
msgstr "" msgstr ""
......
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { __ } from '~/locale';
import { GlLoadingIcon, GlLink, GlBadge, GlFormInput } from '@gitlab/ui'; import { GlLoadingIcon, GlLink, GlBadge, GlFormInput } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Stacktrace from '~/error_tracking/components/stacktrace.vue'; import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.vue'; import ErrorDetails from '~/error_tracking/components/error_details.vue';
import { severityLevel, severityLevelVariant } from '~/error_tracking/components/constants'; import {
severityLevel,
severityLevelVariant,
errorStatus,
} from '~/error_tracking/components/constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -56,6 +61,8 @@ describe('ErrorDetails', () => { ...@@ -56,6 +61,8 @@ describe('ErrorDetails', () => {
actions = { actions = {
startPollingDetails: () => {}, startPollingDetails: () => {},
startPollingStacktrace: () => {}, startPollingStacktrace: () => {},
updateIgnoreStatus: jest.fn(),
updateResolveStatus: jest.fn(),
}; };
getters = { getters = {
...@@ -219,6 +226,96 @@ describe('ErrorDetails', () => { ...@@ -219,6 +226,96 @@ describe('ErrorDetails', () => {
}); });
}); });
describe('Status update', () => {
const findUpdateIgnoreStatusButton = () =>
wrapper.find('[data-qa-selector="update_ignore_status_button"]');
const findUpdateResolveStatusButton = () =>
wrapper.find('[data-qa-selector="update_resolve_status_button"]');
afterEach(() => {
actions.updateIgnoreStatus.mockClear();
actions.updateResolveStatus.mockClear();
});
describe('when error is unresolved', () => {
beforeEach(() => {
store.state.details.errorStatus = errorStatus.UNRESOLVED;
mountComponent();
});
it('displays Ignore and Resolve buttons', () => {
expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore'));
expect(findUpdateResolveStatusButton().text()).toBe(__('Resolve'));
});
it('marks error as ignored when ignore button is clicked', () => {
findUpdateIgnoreStatusButton().trigger('click');
expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual(
expect.objectContaining({ status: errorStatus.IGNORED }),
);
});
it('marks error as resolved when resolve button is clicked', () => {
findUpdateResolveStatusButton().trigger('click');
expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual(
expect.objectContaining({ status: errorStatus.RESOLVED }),
);
});
});
describe('when error is ignored', () => {
beforeEach(() => {
store.state.details.errorStatus = errorStatus.IGNORED;
mountComponent();
});
it('displays Undo Ignore and Resolve buttons', () => {
expect(findUpdateIgnoreStatusButton().text()).toBe(__('Undo ignore'));
expect(findUpdateResolveStatusButton().text()).toBe(__('Resolve'));
});
it('marks error as unresolved when ignore button is clicked', () => {
findUpdateIgnoreStatusButton().trigger('click');
expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual(
expect.objectContaining({ status: errorStatus.UNRESOLVED }),
);
});
it('marks error as resolved when resolve button is clicked', () => {
findUpdateResolveStatusButton().trigger('click');
expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual(
expect.objectContaining({ status: errorStatus.RESOLVED }),
);
});
});
describe('when error is resolved', () => {
beforeEach(() => {
store.state.details.errorStatus = errorStatus.RESOLVED;
mountComponent();
});
it('displays Ignore and Unresolve buttons', () => {
expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore'));
expect(findUpdateResolveStatusButton().text()).toBe(__('Unresolve'));
});
it('marks error as ignored when ignore button is clicked', () => {
findUpdateIgnoreStatusButton().trigger('click');
expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual(
expect.objectContaining({ status: errorStatus.IGNORED }),
);
});
it('marks error as unresolved when unresolve button is clicked', () => {
findUpdateResolveStatusButton().trigger('click');
expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual(
expect.objectContaining({ status: errorStatus.UNRESOLVED }),
);
});
});
});
describe('GitLab issue link', () => { describe('GitLab issue link', () => {
const gitlabIssue = 'https://gitlab.example.com/issues/1'; const gitlabIssue = 'https://gitlab.example.com/issues/1';
const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`); const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`);
......
...@@ -10,6 +10,8 @@ jest.mock('~/flash.js'); ...@@ -10,6 +10,8 @@ jest.mock('~/flash.js');
jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/url_utility');
let mock; let mock;
const commit = jest.fn();
const dispatch = jest.fn().mockResolvedValue();
describe('Sentry common store actions', () => { describe('Sentry common store actions', () => {
beforeEach(() => { beforeEach(() => {
...@@ -20,26 +22,22 @@ describe('Sentry common store actions', () => { ...@@ -20,26 +22,22 @@ describe('Sentry common store actions', () => {
mock.restore(); mock.restore();
createFlash.mockClear(); createFlash.mockClear();
}); });
const endpoint = '123/stacktrace';
const redirectUrl = '/list';
const status = 'resolved';
const params = { endpoint, redirectUrl, status };
describe('updateStatus', () => { describe('updateStatus', () => {
const endpoint = '123/stacktrace';
const redirectUrl = '/list';
const status = 'resolved';
it('should handle successful status update', done => { it('should handle successful status update', done => {
mock.onPut().reply(200, {}); mock.onPut().reply(200, {});
testAction( testAction(
actions.updateStatus, actions.updateStatus,
{ endpoint, redirectUrl, status }, params,
{}, {},
[ [
{ {
payload: true, payload: 'resolved',
type: types.SET_UPDATING_RESOLVE_STATUS, type: types.SET_ERROR_STATUS,
},
{
payload: false,
type: 'SET_UPDATING_RESOLVE_STATUS',
}, },
], ],
[], [],
...@@ -52,27 +50,29 @@ describe('Sentry common store actions', () => { ...@@ -52,27 +50,29 @@ describe('Sentry common store actions', () => {
it('should handle unsuccessful status update', done => { it('should handle unsuccessful status update', done => {
mock.onPut().reply(400, {}); mock.onPut().reply(400, {});
testAction( testAction(actions.updateStatus, params, {}, [], [], () => {
actions.updateStatus, expect(visitUrl).not.toHaveBeenCalled();
{ endpoint, redirectUrl, status }, expect(createFlash).toHaveBeenCalledTimes(1);
{}, done();
[ });
{
payload: true,
type: types.SET_UPDATING_RESOLVE_STATUS,
},
{
payload: false,
type: types.SET_UPDATING_RESOLVE_STATUS,
},
],
[],
() => {
expect(visitUrl).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledTimes(1);
done();
},
);
}); });
}); });
describe('updateResolveStatus', () => {
it('handles status update', () =>
actions.updateResolveStatus({ commit, dispatch }, params).then(() => {
expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_RESOLVE_STATUS, true);
expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_RESOLVE_STATUS, false);
expect(dispatch).toHaveBeenCalledWith('updateStatus', params);
}));
});
describe('updateIgnoreStatus', () => {
it('handles status update', () =>
actions.updateIgnoreStatus({ commit, dispatch }, params).then(() => {
expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_IGNORE_STATUS, true);
expect(commit).toHaveBeenCalledWith(types.SET_UPDATING_IGNORE_STATUS, false);
expect(dispatch).toHaveBeenCalledWith('updateStatus', params);
}));
});
}); });
...@@ -83,7 +83,6 @@ describe Projects::ErrorTrackingHelper do ...@@ -83,7 +83,6 @@ describe Projects::ErrorTrackingHelper do
describe '#error_details_data' do describe '#error_details_data' do
let(:issue_id) { 1234 } let(:issue_id) { 1234 }
let(:route_params) { [project.owner, project, issue_id, { format: :json }] } let(:route_params) { [project.owner, project, issue_id, { format: :json }] }
let(:list_path) { project_error_tracking_index_path(project) }
let(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) } let(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) }
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) } let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) }
...@@ -91,10 +90,6 @@ describe Projects::ErrorTrackingHelper do ...@@ -91,10 +90,6 @@ describe Projects::ErrorTrackingHelper do
let(:result) { helper.error_details_data(project, issue_id) } let(:result) { helper.error_details_data(project, issue_id) }
it 'returns the correct list path' do
expect(result['list-path']).to eq list_path
end
it 'returns the correct issue id' do it 'returns the correct issue id' do
expect(result['issue-id']).to eq issue_id expect(result['issue-id']).to eq issue_id
end end
......
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