Commit 283d46fe authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents c43eb7c0 f5dd9107
......@@ -122,7 +122,7 @@ gem 'faraday_middleware-aws-signers-v4'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.0.0'
gem 'deckar01-task_list', '2.0.1'
gem 'gitlab-markup', '~> 1.6.5'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'redcarpet', '~> 3.4'
......
......@@ -151,7 +151,7 @@ GEM
database_cleaner (1.7.0)
debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0)
deckar01-task_list (2.0.1)
html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
......@@ -1005,7 +1005,7 @@ DEPENDENCIES
connection_pool (~> 2.0)
creole (~> 0.5.0)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.0.0)
deckar01-task_list (= 2.0.1)
device_detector
devise (~> 4.4)
devise-two-factor (~> 3.0.0)
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
export default {
fields: [
{ key: 'error', label: __('Open errors') },
{ key: 'events', label: __('Events') },
{ key: 'users', label: __('Users') },
{ key: 'lastSeen', label: __('Last seen') },
],
components: {
GlEmptyState,
GlButton,
GlLink,
GlLoadingIcon,
GlTable,
Icon,
TimeAgo,
},
props: {
indexPath: {
type: String,
required: true,
},
enableErrorTrackingLink: {
type: String,
required: true,
},
errorTrackingEnabled: {
type: Boolean,
required: true,
},
illustrationPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['errors', 'externalUrl', 'loading']),
},
created() {
if (this.errorTrackingEnabled) {
this.startPolling(this.indexPath);
}
},
methods: {
...mapActions(['startPolling']),
},
};
</script>
<template>
<div>
<div v-if="errorTrackingEnabled">
<div v-if="loading" class="py-3"><gl-loading-icon :size="3" /></div>
<div v-else>
<div class="d-flex justify-content-end">
<gl-button class="my-3 ml-auto" variant="primary" :href="externalUrl" target="_blank"
>View in Sentry <icon name="external-link" />
</gl-button>
</div>
<gl-table
:items="errors"
:fields="$options.fields"
:show-empty="true"
:empty-text="__('No errors to display')"
>
<template slot="HEAD_events" slot-scope="data">
<div class="text-right">{{ data.label }}</div>
</template>
<template slot="HEAD_users" slot-scope="data">
<div class="text-right">{{ data.label }}</div>
</template>
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
<div class="d-flex">
<gl-link :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank">
<strong>{{ errors.item.title.trim() }}</strong>
<icon name="external-link" class="ml-1" />
</gl-link>
<span class="text-secondary ml-2">{{ errors.item.culprit }}</span>
</div>
{{ errors.item.message || __('No details available') }}
</div>
</template>
<template slot="events" slot-scope="errors">
<div class="text-right">{{ errors.item.count }}</div>
</template>
<template slot="users" slot-scope="errors">
<div class="text-right">{{ errors.item.userCount }}</div>
</template>
<template slot="lastSeen" slot-scope="errors">
<div class="d-flex align-items-center">
<icon name="calendar" css-classes="text-secondary mr-1" />
<time-ago :time="errors.item.lastSeen" class="text-secondary" />
</div>
</template>
</gl-table>
</div>
</div>
<div v-else>
<gl-empty-state
:title="__('Get started with error tracking')"
:description="__('Monitor your errors by integrating with Sentry')"
:primary-button-text="__('Enable error tracking')"
:primary-button-link="enableErrorTrackingLink"
:svg-path="illustrationPath"
/>
</div>
</div>
</template>
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import store from './store';
import ErrorTrackingList from './components/error_tracking_list.vue';
export default () => {
if (!gon.features.errorTracking) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_tracking',
components: {
ErrorTrackingList,
},
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset;
let { errorTrackingEnabled } = domEl.dataset;
errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
return createElement('error-tracking-list', {
props: {
indexPath,
enableErrorTrackingLink,
errorTrackingEnabled,
illustrationPath,
},
});
},
});
};
import axios from '~/lib/utils/axios_utils';
export default {
getErrorList({ endpoint }) {
return axios.get(endpoint);
},
};
import Service from '../services';
import * as types from './mutation_types';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
let eTagPoll;
export function startPolling({ commit }, endpoint) {
eTagPoll = new Poll({
resource: Service,
method: 'getErrorList',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
return;
}
commit(types.SET_ERRORS, data.errors);
commit(types.SET_EXTERNAL_URL, data.external_url);
commit(types.SET_LOADING, false);
},
errorCallback: () => {
commit(types.SET_LOADING, false);
createFlash(__('Failed to load errors from Sentry'));
},
});
eTagPoll.makeRequest();
}
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
state: {
errors: [],
externalUrl: '',
loading: true,
},
actions,
mutations,
});
export default createStore();
export const SET_ERRORS = 'SET_ERRORS';
export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
export const SET_LOADING = 'SET_LOADING';
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
[types.SET_ERRORS](state, data) {
state.errors = convertObjectPropsToCamelCase(data, { deep: true });
},
[types.SET_EXTERNAL_URL](state, url) {
state.externalUrl = url;
},
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
};
import ErrorTracking from '~/error_tracking';
document.addEventListener('DOMContentLoaded', () => {
ErrorTracking();
});
# frozen_string_literal: true
module Projects::ErrorTrackingHelper
def error_tracking_data(project)
error_tracking_enabled = !!project.error_tracking_setting&.enabled?
{
'index-path' => project_error_tracking_index_path(project,
format: :json),
'enable-error-tracking-link' => project_settings_operations_path(project),
'error-tracking-enabled' => error_tracking_enabled.to_s,
'illustration-path' => image_path('illustrations/cluster_popover.svg')
}
end
end
......@@ -337,6 +337,7 @@ module ProjectsHelper
builds: :read_build,
clusters: :read_cluster,
serverless: :read_cluster,
error_tracking: :read_sentry_issue,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
......@@ -581,6 +582,7 @@ module ProjectsHelper
environments
clusters
functions
error_tracking
user
gcp
]
......
......@@ -227,6 +227,12 @@
%span
= _('Environments')
- if project_nav_tab?(:error_tracking) && Feature.enabled?(:error_tracking, @project)
= nav_link(controller: :error_tracking) do
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do
%span
= _('Error Tracking')
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
......
- page_title _('Errors')
#js-error_tracking{ data: error_tracking_data(@project) }
---
title: Fix ambiguous brackets in task lists
merge_request: 18514
author: Jared Deckard <jared.deckard@gmail.com>
type: fixed
---
title: Display a list of Sentry Issues in GitLab
merge_request: 23770
author:
type: added
......@@ -3533,6 +3533,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team"
msgstr ""
msgid "Events"
msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr ""
......@@ -3647,6 +3650,9 @@ msgstr ""
msgid "Failed to load emoji list."
msgstr ""
msgid "Failed to load errors from Sentry"
msgstr ""
msgid "Failed to remove issue from board, please try again."
msgstr ""
......@@ -5159,6 +5165,9 @@ msgstr ""
msgid "Last reply by"
msgstr ""
msgid "Last seen"
msgstr ""
msgid "Last update"
msgstr ""
......@@ -5785,6 +5794,9 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
msgid "Monitor your errors by integrating with Sentry"
msgstr ""
msgid "Monitoring"
msgstr ""
......@@ -5976,6 +5988,9 @@ msgstr ""
msgid "No due date"
msgstr ""
msgid "No errors to display"
msgstr ""
msgid "No estimate or time spent"
msgstr ""
......@@ -6227,6 +6242,9 @@ msgstr ""
msgid "Open comment type dropdown"
msgstr ""
msgid "Open errors"
msgstr ""
msgid "Open in Xcode"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ErrorTrackingHelper do
include Gitlab::Routing.url_helpers
set(:project) { create(:project) }
describe '#error_tracking_data' do
let(:setting_path) { project_settings_operations_path(project) }
let(:index_path) do
project_error_tracking_index_path(project, format: :json)
end
context 'without error_tracking_setting' do
it 'returns frontend configuration' do
expect(error_tracking_data(project)).to eq(
'index-path' => index_path,
'enable-error-tracking-link' => setting_path,
'error-tracking-enabled' => 'false',
"illustration-path" => "/images/illustrations/cluster_popover.svg"
)
end
end
context 'with error_tracking_setting' do
let(:error_tracking_setting) do
create(:project_error_tracking_setting, project: project)
end
context 'when enabled' do
before do
error_tracking_setting.update!(enabled: true)
end
it 'show error tracking enabled' do
expect(error_tracking_data(project)).to include(
'error-tracking-enabled' => 'true'
)
end
end
context 'when disabled' do
before do
error_tracking_setting.update!(enabled: false)
end
it 'show error tracking not enabled' do
expect(error_tracking_data(project)).to include(
'error-tracking-enabled' => 'false'
)
end
end
end
end
end
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import { GlButton, GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ErrorTrackingList', () => {
let store;
let wrapper;
function mountComponent({ errorTrackingEnabled = true } = {}) {
wrapper = shallowMount(ErrorTrackingList, {
localVue,
store,
propsData: {
indexPath: '/path',
enableErrorTrackingLink: '/link',
errorTrackingEnabled,
illustrationPath: 'illustration/path',
},
});
}
beforeEach(() => {
const actions = {
getErrorList: () => {},
};
const state = {
errors: [],
loading: true,
};
store = new Vuex.Store({
actions,
state,
});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('loading', () => {
beforeEach(() => {
mountComponent();
});
it('shows spinner', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
expect(wrapper.find(GlTable).exists()).toBeFalsy();
expect(wrapper.find(GlButton).exists()).toBeFalsy();
});
});
describe('results', () => {
beforeEach(() => {
store.state.loading = false;
mountComponent();
});
it('shows table', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.find(GlTable).exists()).toBeTruthy();
expect(wrapper.find(GlButton).exists()).toBeTruthy();
});
});
describe('no results', () => {
beforeEach(() => {
store.state.loading = false;
mountComponent();
});
it('shows empty table', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.find(GlTable).exists()).toBeTruthy();
expect(wrapper.find(GlButton).exists()).toBeTruthy();
});
});
describe('error tracking feature disabled', () => {
beforeEach(() => {
mountComponent({ errorTrackingEnabled: false });
});
it('shows empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBeTruthy();
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.find(GlTable).exists()).toBeFalsy();
expect(wrapper.find(GlButton).exists()).toBeFalsy();
});
});
});
import mutations from '~/error_tracking/store/mutations';
import * as types from '~/error_tracking/store/mutation_types';
describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => {
let state;
beforeEach(() => {
state = { errors: [] };
});
it('camelizes response', () => {
const errors = [
{
title: 'the title',
external_url: 'localhost:3456',
count: 100,
userCount: 10,
},
];
mutations[types.SET_ERRORS](state, errors);
expect(state).toEqual({
errors: [
{
title: 'the title',
externalUrl: 'localhost:3456',
count: 100,
userCount: 10,
},
],
});
});
});
});
......@@ -3034,10 +3034,10 @@ decamelize@^2.0.0:
dependencies:
xregexp "4.0.0"
deckar01-task_list@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.0.0.tgz#7f7a595430d21b3036ed5dfbf97d6b65de18e2c9"
integrity sha1-f3pZVDDSGzA27V37+X1rZd4Y4sk=
deckar01-task_list@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.0.1.tgz#fdcfb6ab5717055a82f29e863a49990a043a06a9"
integrity sha512-i5fT8QxJ9iV6dfgy5U0NHW91O5cKsvDc4u8JNMnZ6efQc356bA9vKuXO3732agSry+bO6TolzTmuqSRi4tkkeA==
decode-uri-component@^0.2.0:
version "0.2.0"
......
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