Commit d8ba5d5a authored by lauraMon's avatar lauraMon Committed by Martin Wortschack

Refactors and adds changelog

* Adds support for plurals, and updates the international text
* Adds a disabled state for button once its clicked
* Adds a spec to test submit
* Some more function refactors
parent 4b544817
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale'; import { GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { GlButton, 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 Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue'; import Stacktrace from './stacktrace.vue';
...@@ -12,7 +13,8 @@ import { trackClickErrorLinkToSentryOptions } from '../utils'; ...@@ -12,7 +13,8 @@ import { trackClickErrorLinkToSentryOptions } from '../utils';
export default { export default {
components: { components: {
GlButton, LoadingButton,
GlFormInput,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
TooltipOnTruncate, TooltipOnTruncate,
...@@ -41,6 +43,11 @@ export default { ...@@ -41,6 +43,11 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
issueCreationInProgress: false,
};
},
computed: { computed: {
...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']), ...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
...mapGetters('details', ['stacktrace']), ...mapGetters('details', ['stacktrace']),
...@@ -67,46 +74,22 @@ export default { ...@@ -67,46 +74,22 @@ export default {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length); return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
}, },
issueTitle() { issueTitle() {
return `${this.error.title}`; return this.error.title;
},
errorUrl() {
return sprintf(
__('- Sentry Event: %{external_url}'),
{
external_url: `${this.error.external_url}\n`,
},
false,
);
},
errorFirstSeen() {
return sprintf(
__('- First seen: %{first_seen}'),
{ first_seen: `${this.error.first_seen}\n` },
false,
);
},
errorLastSeen() {
return sprintf(
__('- Last seen: %{last_seen}'),
{ last_seen: `${this.error.last_seen}\n` },
false,
);
},
errorCount() {
return sprintf(__('- Events: %{count}'), { count: `${this.error.count}\n` }, false);
},
errorUserCount() {
return sprintf(
__('- Users: %{user_count}'),
{ user_count: `${this.error.user_count}\n` },
false,
);
}, },
issueDescription() { issueDescription() {
return sprintf( return sprintf(
__('## Error Details: %{description}'), __(
'%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}',
),
{ {
description: `\n${this.errorUrl}${this.errorFirstSeen}${this.errorLastSeen}${this.errorCount}${this.errorUserCount}`, 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, false,
); );
...@@ -120,6 +103,7 @@ export default { ...@@ -120,6 +103,7 @@ export default {
...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']), ...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
trackClickErrorLinkToSentryOptions, trackClickErrorLinkToSentryOptions,
createIssue() { createIssue() {
this.issueCreationInProgress = true;
this.$refs.sentryIssueForm.submit(); this.$refs.sentryIssueForm.submit();
}, },
formatDate(date) { formatDate(date) {
...@@ -139,12 +123,15 @@ export default { ...@@ -139,12 +123,15 @@ export default {
<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"> <form ref="sentryIssueForm" :action="projectIssuesPath" method="POST">
<input name="issue[title]" :value="issueTitle" type="hidden" /> <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" /> <input name="issue[description]" :value="issueDescription" type="hidden" />
<input :value="csrfToken" type="hidden" name="authenticity_token" /> <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
<gl-button variant="success" @click="createIssue"> <loading-button
{{ __('Create issue') }} class="btn-success"
</gl-button> :label="__('Create issue')"
:loading="issueCreationInProgress"
@click="createIssue"
/>
</form> </form>
</div> </div>
<div> <div>
......
---
title: Adds ability to create issues from sentry details page
merge_request: 20666
author:
type: added
...@@ -65,9 +65,6 @@ msgstr "" ...@@ -65,9 +65,6 @@ msgstr ""
msgid "\"%{path}\" did not exist on \"%{ref}\"" msgid "\"%{path}\" did not exist on \"%{ref}\""
msgstr "" msgstr ""
msgid "## Error Details: %{description}"
msgstr ""
msgid "%d comment" msgid "%d comment"
msgid_plural "%d comments" msgid_plural "%d comments"
msgstr[0] "" msgstr[0] ""
...@@ -237,6 +234,9 @@ msgstr[1] "" ...@@ -237,6 +234,9 @@ msgstr[1] ""
msgid "%{count} related %{pluralized_subject}: %{links}" msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr "" msgstr ""
msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}"
msgstr ""
msgid "%{duration}ms" msgid "%{duration}ms"
msgstr "" msgstr ""
...@@ -477,14 +477,10 @@ msgstr "" ...@@ -477,14 +477,10 @@ msgstr ""
msgid ", or " msgid ", or "
msgstr "" msgstr ""
msgid "- Events: %{count}" msgid "- Event"
msgstr "" msgid_plural "- Events"
msgstr[0] ""
msgid "- First seen: %{first_seen}" msgstr[1] ""
msgstr ""
msgid "- Last seen: %{last_seen}"
msgstr ""
msgid "- Runner is active and can process any new jobs" msgid "- Runner is active and can process any new jobs"
msgstr "" msgstr ""
...@@ -492,11 +488,10 @@ msgstr "" ...@@ -492,11 +488,10 @@ msgstr ""
msgid "- Runner is paused and will not receive any new jobs" msgid "- Runner is paused and will not receive any new jobs"
msgstr "" msgstr ""
msgid "- Sentry Event: %{external_url}" msgid "- User"
msgstr "" msgid_plural "- Users"
msgstr[0] ""
msgid "- Users: %{user_count}" msgstr[1] ""
msgstr ""
msgid "- show less" msgid "- show less"
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 { GlLoadingIcon, GlLink } from '@gitlab/ui'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { GlFormInput, GlLoadingIcon, GlLink } from '@gitlab/ui';
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';
...@@ -15,6 +16,7 @@ describe('ErrorDetails', () => { ...@@ -15,6 +16,7 @@ describe('ErrorDetails', () => {
function mountComponent() { function mountComponent() {
wrapper = shallowMount(ErrorDetails, { wrapper = shallowMount(ErrorDetails, {
stubs: { LoadingButton },
localVue, localVue,
store, store,
propsData: { propsData: {
...@@ -84,7 +86,28 @@ describe('ErrorDetails', () => { ...@@ -84,7 +86,28 @@ describe('ErrorDetails', () => {
expect(wrapper.find(Stacktrace).exists()).toBe(false); expect(wrapper.find(Stacktrace).exists()).toBe(false);
}); });
it('should create an issue with title and description', () => { describe('Stacktrace', () => {
it('should show stacktrace', () => {
store.state.details.loading = false;
store.state.details.error.id = 1;
store.state.details.loadingStacktrace = false;
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
});
it('should NOT show stacktrace if no entries', () => {
store.state.details.loading = false;
store.state.details.loadingStacktrace = false;
store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
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.loading = false;
store.state.details.error = { store.state.details.error = {
id: 1, id: 1,
...@@ -96,35 +119,23 @@ describe('ErrorDetails', () => { ...@@ -96,35 +119,23 @@ describe('ErrorDetails', () => {
user_count: 2, user_count: 2,
}; };
mountComponent(); mountComponent();
});
const form = wrapper.find({ ref: 'sentryIssueForm' }); it('should set the form values with title and description', () => {
const csrfTokenInput = wrapper.find('input[name="authenticity_token"]'); const csrfTokenInput = wrapper.find('glforminput-stub[name="authenticity_token"]');
const issueTitleInput = wrapper.find('input[name="issue[title]"]'); const issueTitleInput = wrapper.find('glforminput-stub[name="issue[title]"]');
const issueDescriptionInput = wrapper.find('input[name="issue[description]"]'); const issueDescriptionInput = wrapper.find('input[name="issue[description]"]');
expect(form).toExist();
expect(csrfTokenInput.attributes('value')).toBe('fakeToken'); expect(csrfTokenInput.attributes('value')).toBe('fakeToken');
expect(issueTitleInput.attributes('value')).toContain(wrapper.vm.issueTitle); expect(issueTitleInput.attributes('value')).toContain(wrapper.vm.issueTitle);
expect(issueDescriptionInput.attributes('value')).toContain(wrapper.vm.issueDescription); expect(issueDescriptionInput.attributes('value')).toContain(wrapper.vm.issueDescription);
}); });
describe('Stacktrace', () => { it('should submit the form', () => {
it('should show stacktrace', () => { window.HTMLFormElement.prototype.submit = () => {};
store.state.details.loading = false; const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit');
store.state.details.error.id = 1; wrapper.find('button').trigger('click');
store.state.details.loadingStacktrace = false; expect(submitSpy).toHaveBeenCalled();
mountComponent(); submitSpy.mockRestore();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
});
it('should NOT show stacktrace if no entries', () => {
store.state.details.loading = false;
store.state.details.loadingStacktrace = false;
store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
}); });
}); });
}); });
......
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