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 {
},
mixins: [timeagoMixin],
props: {
listPath: {
type: String,
required: true,
},
issueUpdatePath: {
type: String,
required: true,
},
issueId: {
type: String,
required: true,
......@@ -81,7 +89,14 @@ export default {
};
},
computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
...mapState('details', [
'error',
'loading',
'loadingStacktrace',
'stacktraceData',
'updatingResolveStatus',
'updatingIgnoreStatus',
]),
...mapGetters('details', ['stacktrace']),
reported() {
return sprintf(
......@@ -137,12 +152,15 @@ export default {
this.startPollingStacktrace(this.issueStackTracePath);
},
methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace', 'updateStatus']),
trackClickErrorLinkToSentryOptions,
createIssue() {
this.issueCreationInProgress = true;
this.$refs.sentryIssueForm.submit();
},
updateIssueStatus(status) {
this.updateStatus({ endpoint: this.issueUpdatePath, redirectUrl: this.listPath, status });
},
formatDate(date) {
return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
......@@ -158,24 +176,42 @@ export default {
<div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<form ref="sentryIssueForm" :action="projectIssuesPath" method="POST">
<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]"
<div class="d-inline-flex">
<loading-button
:label="__('Ignore')"
:loading="updatingIgnoreStatus"
@click="updateIssueStatus('ignored')"
/>
<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"
class="btn-outline-info ml-2"
:label="__('Resolve')"
:loading="updatingResolveStatus"
@click="updateIssueStatus('resolved')"
/>
</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>
<tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top">
......
......@@ -25,6 +25,8 @@ export default () => {
const {
issueId,
projectPath,
listPath,
issueUpdatePath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
......@@ -34,6 +36,8 @@ export default () => {
props: {
issueId,
projectPath,
listPath,
issueUpdatePath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
......
......@@ -4,4 +4,7 @@ export default {
getSentryData({ 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 () => ({
stacktraceData: {},
loading: true,
loadingStacktrace: true,
updatingResolveStatus: false,
updatingIgnoreStatus: false,
});
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import * as listActions from './list/actions';
import listMutations from './list/mutations';
import listState from './list/state';
......@@ -24,8 +27,8 @@ export const createStore = () =>
details: {
namespaced: true,
state: detailsState(),
actions: detailsActions,
mutations: detailsMutations,
actions: { ...actions, ...detailsActions },
mutations: { ...mutations, ...detailsMutations },
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 @@
li {
@include gl-line-height-32;
}
.btn-outline-info {
color: $blue-500;
border-color: $blue-500;
}
}
.stacktrace {
......
......@@ -20,6 +20,7 @@ module Projects::ErrorTrackingHelper
{
'issue-id' => issue_id,
'project-path' => project.full_path,
'list-path' => project_error_tracking_index_path(project),
'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-update-path' => update_project_error_tracking_index_path(*opts),
'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 ""
msgid "Failed to update environment!"
msgstr ""
msgid "Failed to update issue status"
msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
......@@ -9778,6 +9781,9 @@ msgstr ""
msgid "Iglu registry URL (optional)"
msgstr ""
msgid "Ignore"
msgstr ""
msgid "Image %{imageName} was scheduled for deletion from the registry."
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."
msgstr ""
msgid "Resolve"
msgstr ""
msgid "Resolve all threads in new issue"
msgstr ""
......
......@@ -29,6 +29,8 @@ describe('ErrorDetails', () => {
propsData: {
issueId: '123',
projectPath: '/root/gitlab-test',
listPath: '/error_tracking',
issueUpdatePath: '/123',
issueDetailsPath: '/123/details',
issueStackTracePath: '/stacktrace',
projectIssuesPath: '/test-project/issues/',
......@@ -122,6 +124,7 @@ describe('ErrorDetails', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
expect(wrapper.find(GlBadge).exists()).toBe(false);
expect(wrapper.findAll('button').length).toBe(3);
});
describe('Badges', () => {
......@@ -185,7 +188,7 @@ describe('ErrorDetails', () => {
it('should submit the form', () => {
window.HTMLFormElement.prototype.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();
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';
import * as types from '~/error_tracking/store/details/mutation_types';
jest.mock('~/flash.js');
jest.mock('~/lib/utils/url_utility');
let mock;
describe('Sentry error details store actions', () => {
......
......@@ -79,6 +79,7 @@ describe Projects::ErrorTrackingHelper do
describe '#error_details_data' do
let(:issue_id) { 1234 }
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(:project_path) { project.full_path }
let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) }
......@@ -86,6 +87,10 @@ describe Projects::ErrorTrackingHelper do
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
expect(result['issue-id']).to eq issue_id
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