Commit b3c179d7 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Mike Greiling

Ignore/resolve error from details page

Update issues status to mark it ignored or resolved
parent a3955d43
...@@ -30,6 +30,14 @@ export default { ...@@ -30,6 +30,14 @@ export default {
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
listPath: {
type: String,
required: true,
},
issueUpdatePath: {
type: String,
required: true,
},
issueId: { issueId: {
type: String, type: String,
required: true, required: true,
...@@ -81,7 +89,14 @@ export default { ...@@ -81,7 +89,14 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']), ...mapState('details', [
'error',
'loading',
'loadingStacktrace',
'stacktraceData',
'updatingResolveStatus',
'updatingIgnoreStatus',
]),
...mapGetters('details', ['stacktrace']), ...mapGetters('details', ['stacktrace']),
reported() { reported() {
return sprintf( return sprintf(
...@@ -137,12 +152,15 @@ export default { ...@@ -137,12 +152,15 @@ export default {
this.startPollingStacktrace(this.issueStackTracePath); this.startPollingStacktrace(this.issueStackTracePath);
}, },
methods: { methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']), ...mapActions('details', ['startPollingDetails', 'startPollingStacktrace', 'updateStatus']),
trackClickErrorLinkToSentryOptions, trackClickErrorLinkToSentryOptions,
createIssue() { createIssue() {
this.issueCreationInProgress = true; this.issueCreationInProgress = true;
this.$refs.sentryIssueForm.submit(); this.$refs.sentryIssueForm.submit();
}, },
updateIssueStatus(status) {
this.updateStatus({ endpoint: this.issueUpdatePath, redirectUrl: this.listPath, 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')})`;
}, },
...@@ -158,24 +176,42 @@ export default { ...@@ -158,24 +176,42 @@ export default {
<div v-else-if="showDetails" class="error-details"> <div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3"> <div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<form ref="sentryIssueForm" :action="projectIssuesPath" method="POST"> <div class="d-inline-flex">
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" /> <loading-button
<input name="issue[description]" :value="issueDescription" type="hidden" /> :label="__('Ignore')"
<gl-form-input :loading="updatingIgnoreStatus"
:value="GQLerror.id" @click="updateIssueStatus('ignored')"
class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/> />
<gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
<loading-button <loading-button
v-if="!error.gitlab_issue" class="btn-outline-info ml-2"
class="btn-success" :label="__('Resolve')"
:label="__('Create issue')" :loading="updatingResolveStatus"
:loading="issueCreationInProgress" @click="updateIssueStatus('resolved')"
data-qa-selector="create_issue_button"
@click="createIssue"
/> />
</form> <form
ref="sentryIssueForm"
:action="projectIssuesPath"
method="POST"
class="d-inline-block ml-2"
>
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input
:value="GQLerror.id"
class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/>
<gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
<loading-button
v-if="!error.gitlab_issue"
class="btn-success"
:label="__('Create issue')"
:loading="issueCreationInProgress"
data-qa-selector="create_issue_button"
@click="createIssue"
/>
</form>
</div>
</div> </div>
<div> <div>
<tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top"> <tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top">
......
...@@ -25,6 +25,8 @@ export default () => { ...@@ -25,6 +25,8 @@ export default () => {
const { const {
issueId, issueId,
projectPath, projectPath,
listPath,
issueUpdatePath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
projectIssuesPath, projectIssuesPath,
...@@ -34,6 +36,8 @@ export default () => { ...@@ -34,6 +36,8 @@ export default () => {
props: { props: {
issueId, issueId,
projectPath, projectPath,
listPath,
issueUpdatePath,
issueDetailsPath, issueDetailsPath,
issueStackTracePath, issueStackTracePath,
projectIssuesPath, projectIssuesPath,
......
...@@ -4,4 +4,7 @@ export default { ...@@ -4,4 +4,7 @@ export default {
getSentryData({ endpoint, params }) { getSentryData({ endpoint, params }) {
return axios.get(endpoint, { params }); return axios.get(endpoint, { params });
}, },
updateErrorStatus(endpoint, status) {
return axios.put(endpoint, { status });
},
}; };
import service from './../services';
import * as types from './mutation_types';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
export function updateStatus({ commit }, { endpoint, redirectUrl, status }) {
const type =
status === 'resolved' ? types.SET_UPDATING_RESOLVE_STATUS : types.SET_UPDATING_IGNORE_STATUS;
commit(type, true);
return service
.updateErrorStatus(endpoint, status)
.then(() => visitUrl(redirectUrl))
.catch(() => createFlash(__('Failed to update issue status')))
.finally(() => commit(type, false));
}
export default () => {};
...@@ -3,4 +3,6 @@ export default () => ({ ...@@ -3,4 +3,6 @@ export default () => ({
stacktraceData: {}, stacktraceData: {},
loading: true, loading: true,
loadingStacktrace: true, loadingStacktrace: true,
updatingResolveStatus: false,
updatingIgnoreStatus: false,
}); });
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import * as listActions from './list/actions'; import * as listActions from './list/actions';
import listMutations from './list/mutations'; import listMutations from './list/mutations';
import listState from './list/state'; import listState from './list/state';
...@@ -24,8 +27,8 @@ export const createStore = () => ...@@ -24,8 +27,8 @@ export const createStore = () =>
details: { details: {
namespaced: true, namespaced: true,
state: detailsState(), state: detailsState(),
actions: detailsActions, actions: { ...actions, ...detailsActions },
mutations: detailsMutations, mutations: { ...mutations, ...detailsMutations },
getters: detailsGetters, getters: detailsGetters,
}, },
}, },
......
export const SET_UPDATING_RESOLVE_STATUS = 'SET_UPDATING_RESOLVE_STATUS';
export const SET_UPDATING_IGNORE_STATUS = 'SET_UPDATING_IGNORE_STATUS';
import * as types from './mutation_types';
export default {
[types.SET_UPDATING_IGNORE_STATUS](state, updating) {
state.updatingIgnoreStatus = updating;
},
[types.SET_UPDATING_RESOLVE_STATUS](state, updating) {
state.updatingResolveStatus = updating;
},
};
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
li { li {
@include gl-line-height-32; @include gl-line-height-32;
} }
.btn-outline-info {
color: $blue-500;
border-color: $blue-500;
}
} }
.stacktrace { .stacktrace {
......
...@@ -20,6 +20,7 @@ module Projects::ErrorTrackingHelper ...@@ -20,6 +20,7 @@ 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: Add ability to ignore/resolve errors from error tracking detail page
merge_request: 22475
author:
type: added
...@@ -7782,6 +7782,9 @@ msgstr "" ...@@ -7782,6 +7782,9 @@ msgstr ""
msgid "Failed to update environment!" msgid "Failed to update environment!"
msgstr "" msgstr ""
msgid "Failed to update issue status"
msgstr ""
msgid "Failed to update issues, please try again." msgid "Failed to update issues, please try again."
msgstr "" msgstr ""
...@@ -9778,6 +9781,9 @@ msgstr "" ...@@ -9778,6 +9781,9 @@ msgstr ""
msgid "Iglu registry URL (optional)" msgid "Iglu registry URL (optional)"
msgstr "" msgstr ""
msgid "Ignore"
msgstr ""
msgid "Image %{imageName} was scheduled for deletion from the registry." msgid "Image %{imageName} was scheduled for deletion from the registry."
msgstr "" msgstr ""
...@@ -15566,6 +15572,9 @@ msgstr "" ...@@ -15566,6 +15572,9 @@ msgstr ""
msgid "Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key." msgid "Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key."
msgstr "" msgstr ""
msgid "Resolve"
msgstr ""
msgid "Resolve all threads in new issue" msgid "Resolve all threads in new issue"
msgstr "" msgstr ""
......
...@@ -29,6 +29,8 @@ describe('ErrorDetails', () => { ...@@ -29,6 +29,8 @@ describe('ErrorDetails', () => {
propsData: { propsData: {
issueId: '123', issueId: '123',
projectPath: '/root/gitlab-test', projectPath: '/root/gitlab-test',
listPath: '/error_tracking',
issueUpdatePath: '/123',
issueDetailsPath: '/123/details', issueDetailsPath: '/123/details',
issueStackTracePath: '/stacktrace', issueStackTracePath: '/stacktrace',
projectIssuesPath: '/test-project/issues/', projectIssuesPath: '/test-project/issues/',
...@@ -122,6 +124,7 @@ describe('ErrorDetails', () => { ...@@ -122,6 +124,7 @@ describe('ErrorDetails', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false); expect(wrapper.find(Stacktrace).exists()).toBe(false);
expect(wrapper.find(GlBadge).exists()).toBe(false); expect(wrapper.find(GlBadge).exists()).toBe(false);
expect(wrapper.findAll('button').length).toBe(3);
}); });
describe('Badges', () => { describe('Badges', () => {
...@@ -185,7 +188,7 @@ describe('ErrorDetails', () => { ...@@ -185,7 +188,7 @@ describe('ErrorDetails', () => {
it('should submit the form', () => { it('should submit the form', () => {
window.HTMLFormElement.prototype.submit = () => {}; window.HTMLFormElement.prototype.submit = () => {};
const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit'); const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit');
wrapper.find('button').trigger('click'); wrapper.find('[data-qa-selector="create_issue_button"]').trigger('click');
expect(submitSpy).toHaveBeenCalled(); expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore(); submitSpy.mockRestore();
}); });
......
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/flash.js');
jest.mock('~/lib/utils/url_utility');
let mock;
describe('Sentry common store actions', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
createFlash.mockClear();
});
describe('updateStatus', () => {
const endpoint = '123/stacktrace';
const redirectUrl = '/list';
const status = 'resolved';
it('should handle successful status update', done => {
mock.onPut().reply(200, {});
testAction(
actions.updateStatus,
{ endpoint, redirectUrl, status },
{},
[
{
payload: true,
type: types.SET_UPDATING_RESOLVE_STATUS,
},
{
payload: false,
type: 'SET_UPDATING_RESOLVE_STATUS',
},
],
[],
() => {
done();
expect(visitUrl).toHaveBeenCalledWith(redirectUrl);
},
);
});
it('should handle unsuccessful status update', done => {
mock.onPut().reply(400, {});
testAction(
actions.updateStatus,
{ endpoint, redirectUrl, status },
{},
[
{
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();
},
);
});
});
});
...@@ -6,6 +6,8 @@ import * as actions from '~/error_tracking/store/details/actions'; ...@@ -6,6 +6,8 @@ import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types'; import * as types from '~/error_tracking/store/details/mutation_types';
jest.mock('~/flash.js'); jest.mock('~/flash.js');
jest.mock('~/lib/utils/url_utility');
let mock; let mock;
describe('Sentry error details store actions', () => { describe('Sentry error details store actions', () => {
......
...@@ -79,6 +79,7 @@ describe Projects::ErrorTrackingHelper do ...@@ -79,6 +79,7 @@ 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) }
...@@ -86,6 +87,10 @@ describe Projects::ErrorTrackingHelper do ...@@ -86,6 +87,10 @@ 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