Commit 811ec8fa authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'lm-create-issues-from-sentry-details-page' into 'master'

Create issues from sentry details page

See merge request gitlab-org/gitlab!20666
parents 2059e64c 23559ac3
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
......@@ -12,7 +13,8 @@ import { trackClickErrorLinkToSentryOptions } from '../utils';
export default {
components: {
GlButton,
LoadingButton,
GlFormInput,
GlLink,
GlLoadingIcon,
TooltipOnTruncate,
......@@ -32,10 +34,19 @@ export default {
type: String,
required: true,
},
issueProjectPath: {
projectIssuesPath: {
type: String,
required: true,
},
csrfToken: {
type: String,
required: true,
},
},
data() {
return {
issueCreationInProgress: false,
};
},
computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
......@@ -62,33 +73,26 @@ export default {
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
errorTitle() {
return `${this.error.title}`;
},
errorUrl() {
return sprintf(__('Sentry event: %{external_url}'), {
external_url: this.error.external_url,
});
},
errorFirstSeen() {
return sprintf(__('First seen: %{first_seen}'), { first_seen: this.error.first_seen });
},
errorLastSeen() {
return sprintf(__('Last seen: %{last_seen}'), { last_seen: this.error.last_seen });
},
errorCount() {
return sprintf(__('Events: %{count}'), { count: this.error.count });
},
errorUserCount() {
return sprintf(__('Users: %{user_count}'), { user_count: this.error.user_count });
},
issueLink() {
return `${this.issueProjectPath}?issue[title]=${encodeURIComponent(
this.errorTitle,
)}&issue[description]=${encodeURIComponent(this.issueDescription)}`;
issueTitle() {
return this.error.title;
},
issueDescription() {
return `${this.errorUrl}${this.errorFirstSeen}${this.errorLastSeen}${this.errorCount}${this.errorUserCount}`;
return sprintf(
__(
'%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}',
),
{
description: '# Error Details:\n',
errorUrl: `${this.error.external_url}\n`,
firstSeen: `\n${this.error.first_seen}\n`,
lastSeen: `${this.error.last_seen}\n`,
countLabel: n__('- Event', '- Events', this.error.count),
count: `${this.error.count}\n`,
userCountLabel: n__('- User', '- Users', this.error.user_count),
userCount: `${this.error.user_count}\n`,
},
false,
);
},
},
mounted() {
......@@ -98,6 +102,10 @@ export default {
methods: {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
trackClickErrorLinkToSentryOptions,
createIssue() {
this.issueCreationInProgress = true;
this.$refs.sentryIssueForm.submit();
},
formatDate(date) {
return `${this.timeFormated(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
......@@ -114,9 +122,17 @@ 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>
<gl-button variant="success" :href="issueLink">
{{ __('Create issue') }}
</gl-button>
<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="csrfToken" class="hidden" name="authenticity_token" />
<loading-button
class="btn-success"
:label="__('Create issue')"
:loading="issueCreationInProgress"
@click="createIssue"
/>
</form>
</div>
<div>
<tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
......
import Vue from 'vue';
import store from './store';
import ErrorDetails from './components/error_details.vue';
import csrf from '~/lib/utils/csrf';
export default () => {
// eslint-disable-next-line no-new
......@@ -12,13 +13,14 @@ export default () => {
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { issueDetailsPath, issueStackTracePath, issueProjectPath } = domEl.dataset;
const { issueDetailsPath, issueStackTracePath, projectIssuesPath } = domEl.dataset;
return createElement('error-details', {
props: {
issueDetailsPath,
issueStackTracePath,
issueProjectPath,
projectIssuesPath,
csrfToken: csrf.token,
},
});
},
......
......@@ -18,7 +18,7 @@ module Projects::ErrorTrackingHelper
opts = [project, issue_id, { format: :json }]
{
'issue-project-path' => new_project_issue_path(project),
'project-issues-path' => project_issues_path(project),
'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts)
}
......
---
title: Adds ability to create issues from sentry details page
merge_request: 20666
author:
type: added
......@@ -234,6 +234,9 @@ msgstr[1] ""
msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr ""
msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}"
msgstr ""
msgid "%{duration}ms"
msgstr ""
......@@ -474,12 +477,22 @@ msgstr ""
msgid ", or "
msgstr ""
msgid "- Event"
msgid_plural "- Events"
msgstr[0] ""
msgstr[1] ""
msgid "- Runner is active and can process any new jobs"
msgstr ""
msgid "- Runner is paused and will not receive any new jobs"
msgstr ""
msgid "- User"
msgid_plural "- Users"
msgstr[0] ""
msgstr[1] ""
msgid "- show less"
msgstr ""
......@@ -6983,9 +6996,6 @@ msgstr ""
msgid "Events"
msgstr ""
msgid "Events: %{count}"
msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr ""
......@@ -7642,9 +7652,6 @@ msgstr ""
msgid "First seen"
msgstr ""
msgid "First seen: %{first_seen}"
msgstr ""
msgid "Fixed date"
msgstr ""
......@@ -10108,9 +10115,6 @@ msgstr ""
msgid "Last seen"
msgstr ""
msgid "Last seen: %{last_seen}"
msgstr ""
msgid "Last successful update"
msgstr ""
......@@ -15637,9 +15641,6 @@ msgstr ""
msgid "Sentry event"
msgstr ""
msgid "Sentry event: %{external_url}"
msgstr ""
msgid "Sep"
msgstr ""
......@@ -19362,9 +19363,6 @@ msgstr ""
msgid "Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use."
msgstr ""
msgid "Users: %{user_count}"
msgstr ""
msgid "UsersSelect|%{name} + %{length} more"
msgstr ""
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton, GlLoadingIcon, GlLink } from '@gitlab/ui';
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.vue';
......@@ -15,12 +16,14 @@ describe('ErrorDetails', () => {
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
stubs: { LoadingButton },
localVue,
store,
propsData: {
issueDetailsPath: '/123/details',
issueStackTracePath: '/stacktrace',
issueProjectPath: '/test-project/issues/new',
projectIssuesPath: '/test-project/issues/',
csrfToken: 'fakeToken',
},
});
}
......@@ -83,36 +86,6 @@ describe('ErrorDetails', () => {
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
it('should allow an issue to be created with title and description', () => {
store.state.details.loading = false;
store.state.details.error = {
id: 1,
title: 'Issue title',
external_url: 'http://sentry.gitlab.net/gitlab',
first_seen: '2017-05-26T13:32:48Z',
last_seen: '2018-05-26T13:32:48Z',
count: 12,
user_count: 2,
};
mountComponent();
const button = wrapper.find(GlButton);
const title = 'Issue title';
const url = 'Sentry event: http://sentry.gitlab.net/gitlab';
const firstSeen = 'First seen: 2017-05-26T13:32:48Z';
const lastSeen = 'Last seen: 2018-05-26T13:32:48Z';
const count = 'Events: 12';
const userCount = 'Users: 2';
const issueDescription = `${url}${firstSeen}${lastSeen}${count}${userCount}`;
const issueLink = `/test-project/issues/new?issue[title]=${encodeURIComponent(
title,
)}&issue[description]=${encodeURIComponent(issueDescription)}`;
expect(button.exists()).toBe(true);
expect(button.attributes().href).toBe(issueLink);
});
describe('Stacktrace', () => {
it('should show stacktrace', () => {
store.state.details.loading = false;
......@@ -132,5 +105,38 @@ describe('ErrorDetails', () => {
expect(wrapper.find(Stacktrace).exists()).toBe(false);
});
});
describe('When a user clicks the create issue button', () => {
beforeEach(() => {
store.state.details.loading = false;
store.state.details.error = {
id: 1,
title: 'Issue title',
external_url: 'http://sentry.gitlab.net/gitlab',
first_seen: '2017-05-26T13:32:48Z',
last_seen: '2018-05-26T13:32:48Z',
count: 12,
user_count: 2,
};
mountComponent();
});
it('should set the form values with title and description', () => {
const csrfTokenInput = wrapper.find('glforminput-stub[name="authenticity_token"]');
const issueTitleInput = wrapper.find('glforminput-stub[name="issue[title]"]');
const issueDescriptionInput = wrapper.find('input[name="issue[description]"]');
expect(csrfTokenInput.attributes('value')).toBe('fakeToken');
expect(issueTitleInput.attributes('value')).toContain(wrapper.vm.issueTitle);
expect(issueDescriptionInput.attributes('value')).toContain(wrapper.vm.issueDescription);
});
it('should submit the form', () => {
window.HTMLFormElement.prototype.submit = () => {};
const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit');
wrapper.find('button').trigger('click');
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
});
});
});
......@@ -81,7 +81,7 @@ describe Projects::ErrorTrackingHelper do
let(:route_params) { [project.owner, project, issue_id, { format: :json }] }
let(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) }
let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) }
let(:issue_project_path) { new_project_issue_path(project) }
let(:issues_path) { project_issues_path(project) }
let(:result) { helper.error_details_data(project, issue_id) }
......@@ -93,8 +93,8 @@ describe Projects::ErrorTrackingHelper do
expect(result['issue-stack-trace-path']).to eq stack_trace_path
end
it 'returns the correct path for creating a new issue' do
expect(result['issue-project-path']).to eq issue_project_path
it 'creates an issue and redirects to issue show page' do
expect(result['project-issues-path']).to eq issues_path
end
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