Commit d66489e4 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Illya Klymov

Create issue from alert

parent 2a15ea76
......@@ -20,6 +20,8 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_SEVERITY_LABELS } from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
export default {
statuses: {
......@@ -60,7 +62,7 @@ export default {
type: String,
required: true,
},
newIssuePath: {
projectIssuesPath: {
type: String,
required: true,
},
......@@ -85,7 +87,13 @@ export default {
},
},
data() {
return { alert: null, errored: false, isErrorDismissed: false };
return {
alert: null,
errored: false,
isErrorDismissed: false,
createIssueError: '',
issueCreationInProgress: false,
};
},
computed: {
loading() {
......@@ -122,6 +130,33 @@ export default {
);
});
},
createIssue() {
this.issueCreationInProgress = true;
this.$apollo
.mutate({
mutation: createIssueQuery,
variables: {
iid: this.alert.iid,
projectPath: this.projectPath,
},
})
.then(({ data: { createAlertIssue: { errors, issue } } }) => {
if (errors?.length) {
[this.createIssueError] = errors;
this.issueCreationInProgress = false;
} else if (issue) {
visitUrl(this.issuePath(issue.iid));
}
})
.catch(error => {
this.createIssueError = error;
this.issueCreationInProgress = false;
});
},
issuePath(issueId) {
return joinPaths(this.projectIssuesPath, issueId);
},
},
};
</script>
......@@ -130,6 +165,14 @@ export default {
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError">
{{ $options.i18n.errorMsg }}
</gl-alert>
<gl-alert
v-if="createIssueError"
variant="danger"
data-testid="issueCreationError"
@dismiss="createIssueError = null"
>
{{ createIssueError }}
</gl-alert>
<div v-if="loading"><gl-loading-icon size="lg" class="gl-mt-5" /></div>
<div v-if="alert" class="alert-management-details gl-relative">
<div
......@@ -158,16 +201,29 @@ export default {
<template #tool>{{ alert.monitoringTool }}</template>
</gl-sprintf>
</div>
<gl-button
v-if="glFeatures.createIssueFromAlertEnabled"
class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-create-issue-button"
data-testid="createIssueBtn"
:href="newIssuePath"
category="primary"
variant="success"
>
{{ s__('AlertManagement|Create issue') }}
</gl-button>
<div v-if="glFeatures.alertManagementCreateAlertIssue">
<gl-button
v-if="alert.issueIid"
class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-issue-button"
data-testid="viewIssueBtn"
:href="issuePath(alert.issueIid)"
category="primary"
variant="success"
>
{{ s__('AlertManagement|View issue') }}
</gl-button>
<gl-button
v-else
class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-issue-button"
data-testid="createIssueBtn"
:loading="issueCreationInProgress"
category="primary"
variant="success"
@click="createIssue()"
>
{{ s__('AlertManagement|Create issue') }}
</gl-button>
</div>
</div>
<div
v-if="alert"
......
......@@ -8,7 +8,7 @@ Vue.use(VueApollo);
export default selector => {
const domEl = document.querySelector(selector);
const { alertId, projectPath, newIssuePath } = domEl.dataset;
const { alertId, projectPath, projectIssuesPath } = domEl.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
......@@ -39,7 +39,7 @@ export default selector => {
props: {
alertId,
projectPath,
newIssuePath,
projectIssuesPath,
},
});
},
......
......@@ -6,4 +6,5 @@ fragment AlertListItem on AlertManagementAlert {
startedAt
endedAt
eventCount
issueIid
}
mutation ($projectPath: ID!, $iid: String!) {
createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
errors
issue {
iid
}
}
}
......@@ -35,7 +35,7 @@
}
@include media-breakpoint-down(xs) {
.alert-details-create-issue-button {
.alert-details-issue-button {
width: 100%;
}
}
......
......@@ -4,7 +4,7 @@ class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
before_action do
push_frontend_feature_flag(:alert_list_status_filtering_enabled)
push_frontend_feature_flag(:create_issue_from_alert_enabled)
push_frontend_feature_flag(:alert_management_create_alert_issue)
push_frontend_feature_flag(:alert_assignee, project)
end
......
......@@ -15,7 +15,7 @@ module Projects::AlertManagementHelper
{
'alert-id' => alert_id,
'project-path' => project.full_path,
'new-issue-path' => new_project_issue_path(project)
'project-issues-path' => project_issues_path(project)
}
end
end
......@@ -1847,6 +1847,9 @@ msgstr ""
msgid "AlertManagement|Unknown"
msgstr ""
msgid "AlertManagement|View issue"
msgstr ""
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
......
......@@ -2,7 +2,9 @@ import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlDropdownItem, GlTable } from '@gitlab/ui';
import AlertDetails from '~/alert_management/components/alert_details.vue';
import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql';
import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import createFlash from '~/flash';
import { joinPaths } from '~/lib/utils/url_utility';
import mockAlerts from '../mocks/alerts.json';
......@@ -11,13 +13,15 @@ jest.mock('~/flash');
describe('AlertDetails', () => {
let wrapper;
const newIssuePath = 'root/alerts/-/issues/new';
const projectPath = 'root/alerts';
const projectIssuesPath = 'root/alerts/-/issues';
const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
const findDetailsTable = () => wrapper.find(GlTable);
function mountComponent({
data,
createIssueFromAlertEnabled = false,
alertManagementCreateAlertIssue = false,
loading = false,
mountMethod = shallowMount,
stubs = {},
......@@ -25,14 +29,14 @@ describe('AlertDetails', () => {
wrapper = mountMethod(AlertDetails, {
propsData: {
alertId: 'alertId',
projectPath: 'projectPath',
newIssuePath,
projectPath,
projectIssuesPath,
},
data() {
return { alert: { ...mockAlert }, ...data };
},
provide: {
glFeatures: { createIssueFromAlertEnabled },
glFeatures: { alertManagementCreateAlertIssue },
},
mocks: {
$apollo: {
......@@ -50,11 +54,15 @@ describe('AlertDetails', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
if (wrapper) {
wrapper.destroy();
}
}
});
const findCreatedIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]');
const findCreateIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]');
const findViewIssueBtn = () => wrapper.find('[data-testid="viewIssueBtn"]');
const findIssueCreationAlert = () => wrapper.find('[data-testid="issueCreationError"]');
describe('Alert details', () => {
describe('when alert is null', () => {
......@@ -118,17 +126,68 @@ describe('AlertDetails', () => {
describe('Create issue from alert', () => {
describe('createIssueFromAlertEnabled feature flag enabled', () => {
it('should display a button that links to new issue page', () => {
mountComponent({ createIssueFromAlertEnabled: true });
expect(findCreatedIssueBtn().exists()).toBe(true);
expect(findCreatedIssueBtn().attributes('href')).toBe(newIssuePath);
it('should display "View issue" button that links the issue page when issue exists', () => {
const issueIid = '3';
mountComponent({
alertManagementCreateAlertIssue: true,
data: { alert: { ...mockAlert, issueIid } },
});
expect(findViewIssueBtn().exists()).toBe(true);
expect(findViewIssueBtn().attributes('href')).toBe(
joinPaths(projectIssuesPath, issueIid),
);
expect(findCreateIssueBtn().exists()).toBe(false);
});
it('should display "Create issue" button when issue doesn\'t exist yet', () => {
const issueIid = null;
mountComponent({
mountMethod: mount,
alertManagementCreateAlertIssue: true,
data: { alert: { ...mockAlert, issueIid } },
});
expect(findViewIssueBtn().exists()).toBe(false);
expect(findCreateIssueBtn().exists()).toBe(true);
});
it('calls `$apollo.mutate` with `createIssueQuery`', () => {
const issueIid = '10';
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { createAlertIssue: { issue: { iid: issueIid } } } });
findCreateIssueBtn().trigger('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createIssueQuery,
variables: {
iid: mockAlert.iid,
projectPath,
},
});
});
it('shows error alert when issue creation fails ', () => {
const errorMsg = 'Something went wrong';
mountComponent({
mountMethod: mount,
alertManagementCreateAlertIssue: true,
data: { alert: { ...mockAlert, alertIid: 1 } },
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
findCreateIssueBtn().trigger('click');
setImmediate(() => {
expect(findIssueCreationAlert().text()).toBe(errorMsg);
});
});
});
describe('createIssueFromAlertEnabled feature flag disabled', () => {
it('should display a button that links to a new issue page', () => {
mountComponent({ createIssueFromAlertEnabled: false });
expect(findCreatedIssueBtn().exists()).toBe(false);
it('should not display a View or Create issue button', () => {
mountComponent({ alertManagementCreateAlertIssue: false });
expect(findCreateIssueBtn().exists()).toBe(false);
expect(findViewIssueBtn().exists()).toBe(false);
});
});
});
......@@ -223,7 +282,7 @@ describe('AlertDetails', () => {
variables: {
iid: 'alertId',
status: 'TRIGGERED',
projectPath: 'projectPath',
projectPath,
},
});
});
......
......@@ -4,6 +4,7 @@
"title": "SyntaxError: Invalid or unexpected token",
"severity": "CRITICAL",
"eventCount": 7,
"createdAt": "2020-04-17T23:18:14.996Z",
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED"
......
......@@ -69,13 +69,13 @@ describe Projects::AlertManagementHelper do
describe '#alert_management_detail_data' do
let(:alert_id) { 1 }
let(:new_issue_path) { new_project_issue_path(project) }
let(:issues_path) { project_issues_path(project) }
it 'returns detail page configuration' do
expect(helper.alert_management_detail_data(project, alert_id)).to eq(
'alert-id' => alert_id,
'project-path' => project_path,
'new-issue-path' => new_issue_path
'project-issues-path' => issues_path
)
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