Commit 7cc6c10c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 630101f7
...@@ -835,6 +835,13 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -835,6 +835,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Don't send CI usage email notifications for self-hosted instances. !14809 - Don't send CI usage email notifications for self-hosted instances. !14809
## 12.0.12
### Fixed (1 change)
- Backport the new reliable fetcher to 12.0.9. !20532
## 12.0.10 ## 12.0.10
- No changes. - No changes.
......
...@@ -17,8 +17,6 @@ import AccessorUtils from '~/lib/utils/accessor'; ...@@ -17,8 +17,6 @@ import AccessorUtils from '~/lib/utils/accessor';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { trackViewInSentryOptions } from '../utils';
export default { export default {
fields: [ fields: [
...@@ -27,6 +25,11 @@ export default { ...@@ -27,6 +25,11 @@ export default {
{ key: 'users', label: __('Users') }, { key: 'users', label: __('Users') },
{ key: 'lastSeen', label: __('Last seen'), thClass: 'w-15p' }, { key: 'lastSeen', label: __('Last seen'), thClass: 'w-15p' },
], ],
sortFields: {
last_seen: __('Last Seen'),
first_seen: __('First Seen'),
frequency: __('Frequency'),
},
components: { components: {
GlEmptyState, GlEmptyState,
GlButton, GlButton,
...@@ -43,7 +46,6 @@ export default { ...@@ -43,7 +46,6 @@ export default {
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
}, },
props: { props: {
indexPath: { indexPath: {
...@@ -74,45 +76,47 @@ export default { ...@@ -74,45 +76,47 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('list', ['errors', 'externalUrl', 'loading', 'recentSearches']), ...mapState('list', ['errors', 'loading', 'searchQuery', 'sortField', 'recentSearches']),
}, },
created() { created() {
if (this.errorTrackingEnabled) { if (this.errorTrackingEnabled) {
this.startPolling(this.indexPath); this.setEndpoint(this.indexPath);
this.startPolling();
} }
}, },
methods: { methods: {
...mapActions('list', [ ...mapActions('list', [
'startPolling', 'startPolling',
'restartPolling', 'restartPolling',
'setEndpoint',
'searchByQuery',
'sortByField',
'addRecentSearch', 'addRecentSearch',
'clearRecentSearches', 'clearRecentSearches',
'loadRecentSearches', 'loadRecentSearches',
'setIndexPath', 'setIndexPath',
]), ]),
filterErrors() {
const searchTerm = this.errorSearchQuery.trim();
this.addRecentSearch(searchTerm);
this.startPolling(`${this.indexPath}?search_term=${searchTerm}`);
},
setSearchText(text) { setSearchText(text) {
this.errorSearchQuery = text; this.errorSearchQuery = text;
this.filterErrors(); this.searchByQuery(text);
}, },
trackViewInSentryOptions,
getDetailsLink(errorId) { getDetailsLink(errorId) {
return `error_tracking/${errorId}/details`; return `error_tracking/${errorId}/details`;
}, },
isCurrentSortField(field) {
return field === this.sortField;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div class="error-list">
<div v-if="errorTrackingEnabled"> <div v-if="errorTrackingEnabled">
<div class="d-flex flex-row justify-content-around bg-secondary border p-3"> <div
<div class="filtered-search-box"> class="d-flex flex-row justify-content-around align-items-center bg-secondary border mt-2"
>
<div class="filtered-search-box flex-grow-1 my-3 ml-3 mr-2">
<gl-dropdown <gl-dropdown
:text="__('Recent searches')" :text="__('Recent searches')"
class="filtered-search-history-dropdown-wrapper d-none d-md-block" class="filtered-search-history-dropdown-wrapper d-none d-md-block"
...@@ -143,7 +147,7 @@ export default { ...@@ -143,7 +147,7 @@ export default {
:disabled="loading" :disabled="loading"
:placeholder="__('Search or filter results…')" :placeholder="__('Search or filter results…')"
autofocus autofocus
@keyup.enter.native="filterErrors" @keyup.enter.native="searchByQuery(errorSearchQuery)"
/> />
</div> </div>
<div class="gl-search-box-by-type-right-icons"> <div class="gl-search-box-by-type-right-icons">
...@@ -160,16 +164,28 @@ export default { ...@@ -160,16 +164,28 @@ export default {
</div> </div>
</div> </div>
<gl-button <gl-dropdown
v-track-event="trackViewInSentryOptions(externalUrl)" :text="$options.sortFields[sortField]"
class="ml-3" left
variant="primary" :disabled="loading"
:href="externalUrl" class="mr-3"
target="_blank" menu-class="sort-dropdown"
> >
{{ __('View in Sentry') }} <gl-dropdown-item
<icon name="external-link" class="flex-shrink-0" /> v-for="(label, field) in $options.sortFields"
</gl-button> :key="field"
@click="sortByField(field)"
>
<span class="d-flex">
<icon
class="flex-shrink-0 append-right-4"
:class="{ invisible: !isCurrentSortField(field) }"
name="mobile-issue-close"
/>
{{ label }}
</span>
</gl-dropdown-item>
</gl-dropdown>
</div> </div>
<div v-if="loading" class="py-3"> <div v-if="loading" class="py-3">
......
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
export default { export default {
getSentryData({ endpoint }) { getSentryData({ endpoint, params }) {
return axios.get(endpoint); return axios.get(endpoint, { params });
}, },
}; };
...@@ -6,19 +6,24 @@ import { __, sprintf } from '~/locale'; ...@@ -6,19 +6,24 @@ import { __, sprintf } from '~/locale';
let eTagPoll; let eTagPoll;
export function startPolling({ commit, dispatch }, endpoint) { export function startPolling({ state, commit, dispatch }) {
commit(types.SET_LOADING, true); commit(types.SET_LOADING, true);
eTagPoll = new Poll({ eTagPoll = new Poll({
resource: Service, resource: Service,
method: 'getSentryData', method: 'getSentryData',
data: { endpoint }, data: {
endpoint: state.endpoint,
params: {
search_term: state.searchQuery,
sort: state.sortField,
},
},
successCallback: ({ data }) => { successCallback: ({ data }) => {
if (!data) { if (!data) {
return; return;
} }
commit(types.SET_ERRORS, data.errors); commit(types.SET_ERRORS, data.errors);
commit(types.SET_EXTERNAL_URL, data.external_url);
commit(types.SET_LOADING, false); commit(types.SET_LOADING, false);
dispatch('stopPolling'); dispatch('stopPolling');
}, },
...@@ -45,7 +50,6 @@ export const stopPolling = () => { ...@@ -45,7 +50,6 @@ export const stopPolling = () => {
export function restartPolling({ commit }) { export function restartPolling({ commit }) {
commit(types.SET_ERRORS, []); commit(types.SET_ERRORS, []);
commit(types.SET_EXTERNAL_URL, '');
commit(types.SET_LOADING, true); commit(types.SET_LOADING, true);
if (eTagPoll) eTagPoll.restart(); if (eTagPoll) eTagPoll.restart();
...@@ -67,4 +71,22 @@ export function clearRecentSearches({ commit }) { ...@@ -67,4 +71,22 @@ export function clearRecentSearches({ commit }) {
commit(types.CLEAR_RECENT_SEARCHES); commit(types.CLEAR_RECENT_SEARCHES);
} }
export const searchByQuery = ({ commit, dispatch }, query) => {
const searchQuery = query.trim();
commit(types.SET_SEARCH_QUERY, searchQuery);
commit(types.ADD_RECENT_SEARCH, searchQuery);
dispatch('stopPolling');
dispatch('startPolling');
};
export const sortByField = ({ commit, dispatch }, field) => {
commit(types.SET_SORT_FIELD, field);
dispatch('stopPolling');
dispatch('startPolling');
};
export const setEndpoint = ({ commit }, endpoint) => {
commit(types.SET_ENDPOINT, endpoint);
};
export default () => {}; export default () => {};
export const SET_ERRORS = 'SET_ERRORS'; export const SET_ERRORS = 'SET_ERRORS';
export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
export const SET_INDEX_PATH = 'SET_INDEX_PATH'; export const SET_INDEX_PATH = 'SET_INDEX_PATH';
export const SET_LOADING = 'SET_LOADING'; export const SET_LOADING = 'SET_LOADING';
export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH'; export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH';
export const CLEAR_RECENT_SEARCHES = 'CLEAR_RECENT_SEARCHES'; export const CLEAR_RECENT_SEARCHES = 'CLEAR_RECENT_SEARCHES';
export const LOAD_RECENT_SEARCHES = 'LOAD_RECENT_SEARCHES'; export const LOAD_RECENT_SEARCHES = 'LOAD_RECENT_SEARCHES';
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const SET_SORT_FIELD = 'SET_SORT_FIELD';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
...@@ -6,9 +6,6 @@ export default { ...@@ -6,9 +6,6 @@ export default {
[types.SET_ERRORS](state, data) { [types.SET_ERRORS](state, data) {
state.errors = convertObjectPropsToCamelCase(data, { deep: true }); state.errors = convertObjectPropsToCamelCase(data, { deep: true });
}, },
[types.SET_EXTERNAL_URL](state, url) {
state.externalUrl = url;
},
[types.SET_LOADING](state, loading) { [types.SET_LOADING](state, loading) {
state.loading = loading; state.loading = loading;
}, },
...@@ -47,4 +44,13 @@ export default { ...@@ -47,4 +44,13 @@ export default {
throw e; throw e;
} }
}, },
[types.SET_SORT_FIELD](state, field) {
state.sortField = field;
},
[types.SET_SEARCH_QUERY](state, query) {
state.searchQuery = query;
},
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
}; };
export default () => ({ export default () => ({
errors: [], errors: [],
externalUrl: '',
loading: true, loading: true,
endpoint: null,
sortField: 'last_seen',
searchQuery: null,
indexPath: '', indexPath: '',
recentSearches: [], recentSearches: [],
}); });
/* eslint-disable @gitlab/i18n/no-non-i18n-strings */ /* eslint-disable @gitlab/i18n/no-non-i18n-strings, import/prefer-default-export */
/**
* Tracks snowplow event when user clicks View in Sentry btn
* @param {String} externalUrl that will be send as a property for the event
*/
export const trackViewInSentryOptions = url => ({
category: 'Error Tracking',
action: 'click_view_in_sentry',
label: 'External Url',
property: url,
});
/** /**
* Tracks snowplow event when User clicks on error link to Sentry * Tracks snowplow event when User clicks on error link to Sentry
......
...@@ -3,6 +3,7 @@ import BlobViewer from '~/blob/viewer'; ...@@ -3,6 +3,7 @@ import BlobViewer from '~/blob/viewer';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed'; import snippetEmbed from '~/snippet/snippet_embed';
import initSnippetsApp from '~/snippets';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
if (!gon.features.snippetsVue) { if (!gon.features.snippetsVue) {
...@@ -11,5 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -11,5 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
initNotes(); initNotes();
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
snippetEmbed(); snippetEmbed();
} else {
initSnippetsApp();
} }
}); });
...@@ -128,15 +128,15 @@ const bindEvents = () => { ...@@ -128,15 +128,15 @@ const bindEvents = () => {
}, },
iosswift: { iosswift: {
text: s__('ProjectTemplates|iOS (Swift)'), text: s__('ProjectTemplates|iOS (Swift)'),
icon: '.template-option svg.icon-gitlab', icon: '.template-option .icon-iosswift',
}, },
dotnetcore: { dotnetcore: {
text: s__('ProjectTemplates|.NET Core'), text: s__('ProjectTemplates|.NET Core'),
icon: '.template-option .icon-dotnet', icon: '.template-option .icon-dotnetcore',
}, },
android: { android: {
text: s__('ProjectTemplates|Android'), text: s__('ProjectTemplates|Android'),
icon: '.template-option svg.icon-android', icon: '.template-option .icon-android',
}, },
gomicro: { gomicro: {
text: s__('ProjectTemplates|Go Micro'), text: s__('ProjectTemplates|Go Micro'),
...@@ -164,27 +164,27 @@ const bindEvents = () => { ...@@ -164,27 +164,27 @@ const bindEvents = () => {
}, },
nfhugo: { nfhugo: {
text: s__('ProjectTemplates|Netlify/Hugo'), text: s__('ProjectTemplates|Netlify/Hugo'),
icon: '.template-option .icon-netlify', icon: '.template-option .icon-nfhugo',
}, },
nfjekyll: { nfjekyll: {
text: s__('ProjectTemplates|Netlify/Jekyll'), text: s__('ProjectTemplates|Netlify/Jekyll'),
icon: '.template-option .icon-netlify', icon: '.template-option .icon-nfjekyll',
}, },
nfplainhtml: { nfplainhtml: {
text: s__('ProjectTemplates|Netlify/Plain HTML'), text: s__('ProjectTemplates|Netlify/Plain HTML'),
icon: '.template-option .icon-netlify', icon: '.template-option .icon-nfplainhtml',
}, },
nfgitbook: { nfgitbook: {
text: s__('ProjectTemplates|Netlify/GitBook'), text: s__('ProjectTemplates|Netlify/GitBook'),
icon: '.template-option .icon-netlify', icon: '.template-option .icon-nfgitbook',
}, },
nfhexo: { nfhexo: {
text: s__('ProjectTemplates|Netlify/Hexo'), text: s__('ProjectTemplates|Netlify/Hexo'),
icon: '.template-option .icon-netlify', icon: '.template-option .icon-nfhexo',
}, },
salesforcedx: { salesforcedx: {
text: s__('ProjectTemplates|SalesforceDX'), text: s__('ProjectTemplates|SalesforceDX'),
icon: '.template-option svg.icon-gitlab', icon: '.template-option .icon-salesforcedx',
}, },
serverless_framework: { serverless_framework: {
text: s__('ProjectTemplates|Serverless Framework/JS'), text: s__('ProjectTemplates|Serverless Framework/JS'),
......
<script>
import getSnippet from '../queries/getSnippet.query.graphql';
export default {
apollo: {
snippetData: {
query: getSnippet,
variables() {
return {
ids: this.snippetGid,
};
},
update: data => data.snippets.edges[0].node,
},
},
props: {
snippetGid: {
type: String,
required: true,
},
},
data() {
return {
snippetData: {},
};
},
};
</script>
<template>
<div class="js-snippet-view"></div>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import SnippetsApp from './components/app.vue';
Vue.use(VueApollo);
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-snippet-view');
if (!el) {
return false;
}
const { snippetGid } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(SnippetsApp, {
props: {
snippetGid,
},
});
},
});
};
query getSnippet($ids: [ID!]) {
snippets(ids: $ids) {
edges {
node {
title
description
createdAt
updatedAt
visibility
}
}
}
}
.error-list {
.sort-dropdown {
min-width: auto;
}
}
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
.description p { .description p {
margin-bottom: 0; margin-bottom: 0;
color: $gl-text-color-secondary;
} }
} }
......
- group = local_assigns.fetch(:group) - group = local_assigns.fetch(:group)
- css_class = 'no-description' if group.description.blank? - css_class = 'no-description' if group.description.blank?
%li.group-row{ class: css_class } %li.group-row.py-3{ class: css_class }
.controls .controls
= link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to _('Delete'), [:admin, group], data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } }, method: :delete, class: 'btn btn-remove' = link_to _('Delete'), [:admin, group], data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } }, method: :delete, class: 'btn btn-remove'
......
- user = local_assigns.fetch(:user, current_user) - user = local_assigns.fetch(:user, current_user)
- access = user&.max_member_access_for_group(group.id) - access = user&.max_member_access_for_group(group.id)
%li.group-row{ class: ('no-description' if group.description.blank?) } %li.group-row.py-3{ class: ('no-description' if group.description.blank?) }
.stats .stats
%span %span
= icon('bookmark') = icon('bookmark')
......
- link_project = local_assigns.fetch(:link_project, false) - link_project = local_assigns.fetch(:link_project, false)
- notes_count = @noteable_meta_data[snippet.id].user_notes_count - notes_count = @noteable_meta_data[snippet.id].user_notes_count
%li.snippet-row %li.snippet-row.py-3
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: '' = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
.title .title
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") - page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
- if Feature.enabled?(:snippets_vue) - if Feature.enabled?(:snippets_vue)
#js-snippet-view{ 'data-qa-selector': 'snippet_view' } #js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
- else - else
= render 'shared/snippets/header' = render 'shared/snippets/header'
......
---
title: Sort Sentry error list by first seen, last seen or frequency
merge_request: 21250
author:
type: added
...@@ -134,6 +134,36 @@ to add that `only:` rule to all of your jobs in order to make them always run. Y ...@@ -134,6 +134,36 @@ to add that `only:` rule to all of your jobs in order to make them always run. Y
can use this for scenarios like having only pipelines with merge requests get a can use this for scenarios like having only pipelines with merge requests get a
Review App set up, helping to save resources. Review App set up, helping to save resources.
## Excluding certain branches
Pipelines for merge requests require special treatement when
using [`only`/`except`](../yaml/README.md#onlyexcept-basic). Unlike ordinary
branch refs (for example `refs/heads/my-feature-branch`), merge request refs
use a special Git reference that looks like `refs/merge-requests/:iid/head`. Because
of this, the following configuration will **not** work as expected:
```yaml
# Does not exclude a branch named "docs-my-fix"!
test:
only: [merge_requests]
except: [/^docs-/]
```
Instead, you can use the
[`$CI_COMMIT_REF_NAME` predefined environment
variable](../variables/predefined_variables.md#variables-reference) in
combination with
[`only:variables`](../yaml/README.md#onlyvariablesexceptvariables) to
accomplish this behavior:
```yaml
test:
only: [merge_requests]
except:
variables:
$CI_COMMIT_REF_NAME =~ /^docs-/
```
## Important notes about merge requests from forked projects ## Important notes about merge requests from forked projects
Note that the current behavior is subject to change. In the usual contribution Note that the current behavior is subject to change. In the usual contribution
......
...@@ -7735,6 +7735,9 @@ msgstr "" ...@@ -7735,6 +7735,9 @@ msgstr ""
msgid "Finished" msgid "Finished"
msgstr "" msgstr ""
msgid "First Seen"
msgstr ""
msgid "First day of the week" msgid "First day of the week"
msgstr "" msgstr ""
...@@ -7861,6 +7864,9 @@ msgstr "" ...@@ -7861,6 +7864,9 @@ msgstr ""
msgid "Free Trial of GitLab.com Gold" msgid "Free Trial of GitLab.com Gold"
msgstr "" msgstr ""
msgid "Frequency"
msgstr ""
msgid "Friday" msgid "Friday"
msgstr "" msgstr ""
...@@ -10201,6 +10207,9 @@ msgstr "" ...@@ -10201,6 +10207,9 @@ msgstr ""
msgid "Last Pipeline" msgid "Last Pipeline"
msgstr "" msgstr ""
msgid "Last Seen"
msgstr ""
msgid "Last accessed on" msgid "Last accessed on"
msgstr "" msgstr ""
...@@ -19762,9 +19771,6 @@ msgstr "" ...@@ -19762,9 +19771,6 @@ msgstr ""
msgid "View group labels" msgid "View group labels"
msgstr "" msgstr ""
msgid "View in Sentry"
msgstr ""
msgid "View it on GitLab" msgid "View it on GitLab"
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 { import {
GlButton,
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
...@@ -24,7 +23,9 @@ describe('ErrorTrackingList', () => { ...@@ -24,7 +23,9 @@ describe('ErrorTrackingList', () => {
const findErrorListTable = () => wrapper.find('table'); const findErrorListTable = () => wrapper.find('table');
const findErrorListRows = () => wrapper.findAll('tbody tr'); const findErrorListRows = () => wrapper.findAll('tbody tr');
const findButton = () => wrapper.find(GlButton); const findSortDropdown = () => wrapper.find('.sort-dropdown');
const findRecentSearchesDropdown = () =>
wrapper.find('.filtered-search-history-dropdown-wrapper');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
function mountComponent({ function mountComponent({
...@@ -33,6 +34,8 @@ describe('ErrorTrackingList', () => { ...@@ -33,6 +34,8 @@ describe('ErrorTrackingList', () => {
stubs = { stubs = {
'gl-link': GlLink, 'gl-link': GlLink,
'gl-table': GlTable, 'gl-table': GlTable,
'gl-dropdown': GlDropdown,
'gl-dropdown-item': GlDropdownItem,
}, },
} = {}) { } = {}) {
wrapper = shallowMount(ErrorTrackingList, { wrapper = shallowMount(ErrorTrackingList, {
...@@ -46,6 +49,9 @@ describe('ErrorTrackingList', () => { ...@@ -46,6 +49,9 @@ describe('ErrorTrackingList', () => {
illustrationPath: 'illustration/path', illustrationPath: 'illustration/path',
}, },
stubs, stubs,
data() {
return { errorSearchQuery: 'search' };
},
}); });
} }
...@@ -58,6 +64,9 @@ describe('ErrorTrackingList', () => { ...@@ -58,6 +64,9 @@ describe('ErrorTrackingList', () => {
loadRecentSearches: jest.fn(), loadRecentSearches: jest.fn(),
setIndexPath: jest.fn(), setIndexPath: jest.fn(),
clearRecentSearches: jest.fn(), clearRecentSearches: jest.fn(),
setEndpoint: jest.fn(),
searchByQuery: jest.fn(),
sortByField: jest.fn(),
}; };
const state = createListState(); const state = createListState();
...@@ -101,7 +110,7 @@ describe('ErrorTrackingList', () => { ...@@ -101,7 +110,7 @@ describe('ErrorTrackingList', () => {
it('shows table', () => { it('shows table', () => {
expect(findLoadingIcon().exists()).toBe(false); expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListTable().exists()).toBe(true); expect(findErrorListTable().exists()).toBe(true);
expect(findButton().exists()).toBe(true); expect(findSortDropdown().exists()).toBe(true);
}); });
it('shows list of errors in a table', () => { it('shows list of errors in a table', () => {
...@@ -121,16 +130,20 @@ describe('ErrorTrackingList', () => { ...@@ -121,16 +130,20 @@ describe('ErrorTrackingList', () => {
describe('filtering', () => { describe('filtering', () => {
const findSearchBox = () => wrapper.find(GlFormInput); const findSearchBox = () => wrapper.find(GlFormInput);
it('shows search box', () => { it('shows search box & sort dropdown', () => {
expect(findSearchBox().exists()).toBe(true); expect(findSearchBox().exists()).toBe(true);
expect(findSortDropdown().exists()).toBe(true);
}); });
it('makes network request on submit', () => { it('it searches by query', () => {
expect(actions.startPolling).toHaveBeenCalledTimes(1);
findSearchBox().trigger('keyup.enter'); findSearchBox().trigger('keyup.enter');
expect(actions.searchByQuery.mock.calls[0][1]).toEqual(wrapper.vm.errorSearchQuery);
});
expect(actions.startPolling).toHaveBeenCalledTimes(2); it('it sorts by fields', () => {
const findSortItem = () => wrapper.find('.dropdown-item');
findSortItem().trigger('click');
expect(actions.sortByField).toHaveBeenCalled();
}); });
}); });
}); });
...@@ -148,7 +161,7 @@ describe('ErrorTrackingList', () => { ...@@ -148,7 +161,7 @@ describe('ErrorTrackingList', () => {
it('shows empty table', () => { it('shows empty table', () => {
expect(findLoadingIcon().exists()).toBe(false); expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListRows().length).toEqual(1); expect(findErrorListRows().length).toEqual(1);
expect(findButton().exists()).toBe(true); expect(findSortDropdown().exists()).toBe(true);
}); });
it('shows a message prompting to refresh', () => { it('shows a message prompting to refresh', () => {
...@@ -170,7 +183,7 @@ describe('ErrorTrackingList', () => { ...@@ -170,7 +183,7 @@ describe('ErrorTrackingList', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true); expect(wrapper.find(GlEmptyState).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false); expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListTable().exists()).toBe(false); expect(findErrorListTable().exists()).toBe(false);
expect(findButton().exists()).toBe(false); expect(findSortDropdown().exists()).toBe(false);
}); });
}); });
...@@ -201,13 +214,13 @@ describe('ErrorTrackingList', () => { ...@@ -201,13 +214,13 @@ describe('ErrorTrackingList', () => {
it('shows empty message', () => { it('shows empty message', () => {
store.state.list.recentSearches = []; store.state.list.recentSearches = [];
expect(wrapper.find(GlDropdown).text()).toBe("You don't have any recent searches"); expect(findRecentSearchesDropdown().text()).toContain("You don't have any recent searches");
}); });
it('shows items', () => { it('shows items', () => {
store.state.list.recentSearches = ['great', 'search']; store.state.list.recentSearches = ['great', 'search'];
const dropdownItems = wrapper.findAll(GlDropdownItem); const dropdownItems = wrapper.findAll('.filtered-search-box li');
expect(dropdownItems.length).toBe(3); expect(dropdownItems.length).toBe(3);
expect(dropdownItems.at(0).text()).toBe('great'); expect(dropdownItems.at(0).text()).toBe('great');
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import createFlash from '~/flash';
import * as actions from '~/error_tracking/store/list/actions'; import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types'; import * as types from '~/error_tracking/store/list/mutation_types';
jest.mock('~/flash.js');
describe('error tracking actions', () => { describe('error tracking actions', () => {
let mock; let mock;
...@@ -15,15 +20,97 @@ describe('error tracking actions', () => { ...@@ -15,15 +20,97 @@ describe('error tracking actions', () => {
}); });
describe('startPolling', () => { describe('startPolling', () => {
it('commits SET_LOADING', () => { it('should start polling for data', done => {
mock.onGet().reply(200); const payload = { errors: [{ id: 1 }, { id: 2 }] };
const endpoint = '/errors';
const commit = jest.fn(); mock.onGet().reply(httpStatusCodes.OK, payload);
const state = {}; testAction(
actions.startPolling,
{},
{},
[
{ type: types.SET_LOADING, payload: true },
{ type: types.SET_ERRORS, payload: payload.errors },
{ type: types.SET_LOADING, payload: false },
],
[{ type: 'stopPolling' }],
() => {
done();
},
);
});
it('should show flash on API error', done => {
mock.onGet().reply(httpStatusCodes.BAD_REQUEST);
testAction(
actions.startPolling,
{},
{},
[{ type: types.SET_LOADING, payload: true }, { type: types.SET_LOADING, payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
done();
},
);
});
});
describe('restartPolling', () => {
it('should restart polling', () => {
testAction(
actions.restartPolling,
{},
{},
[{ type: types.SET_ERRORS, payload: [] }, { type: types.SET_LOADING, payload: true }],
[],
);
});
});
describe('searchByQuery', () => {
it('should search by query', () => {
const query = 'search';
testAction(
actions.searchByQuery,
query,
{},
[
{ type: types.SET_SEARCH_QUERY, payload: query },
{ type: types.ADD_RECENT_SEARCH, payload: query },
],
[{ type: 'stopPolling' }, { type: 'startPolling' }],
);
});
});
describe('sortByField', () => {
it('should search by query', () => {
const field = 'frequency';
testAction(
actions.sortByField,
{ field },
{},
[{ type: types.SET_SORT_FIELD, payload: { field } }],
[{ type: 'stopPolling' }, { type: 'startPolling' }],
);
});
});
actions.startPolling({ commit, state }, endpoint); describe('setEnpoint', () => {
it('should set search endpoint', () => {
const endpoint = 'https://sentry.io';
expect(commit).toHaveBeenCalledWith(types.SET_LOADING, true); testAction(
actions.setEndpoint,
{ endpoint },
{},
[{ type: types.SET_ENDPOINT, payload: { endpoint } }],
[],
);
}); });
}); });
}); });
...@@ -3,17 +3,6 @@ import * as errorTrackingUtils from '~/error_tracking/utils'; ...@@ -3,17 +3,6 @@ import * as errorTrackingUtils from '~/error_tracking/utils';
const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1'; const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
describe('Error Tracking Events', () => { describe('Error Tracking Events', () => {
describe('trackViewInSentryOptions', () => {
it('should return correct event options', () => {
expect(errorTrackingUtils.trackViewInSentryOptions(externalUrl)).toEqual({
category: 'Error Tracking',
action: 'click_view_in_sentry',
label: 'External Url',
property: externalUrl,
});
});
});
describe('trackClickErrorLinkToSentryOptions', () => { describe('trackClickErrorLinkToSentryOptions', () => {
it('should return correct event options', () => { it('should return correct event options', () => {
expect(errorTrackingUtils.trackClickErrorLinkToSentryOptions(externalUrl)).toEqual({ expect(errorTrackingUtils.trackClickErrorLinkToSentryOptions(externalUrl)).toEqual({
......
import SnippetApp from '~/snippets/components/app.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
describe('Snippet view app', () => {
let wrapper;
let snippetDataMock;
const localVue = createLocalVue();
const defaultProps = {
snippetGid: 'gid://gitlab/PersonalSnippet/35',
};
function createComponent({ props = defaultProps, snippetData = {} } = {}) {
snippetDataMock = jest.fn();
const $apollo = {
queries: {
snippetData: snippetDataMock,
},
};
wrapper = shallowMount(SnippetApp, {
sync: false,
mocks: { $apollo },
localVue,
propsData: {
...props,
},
});
wrapper.setData({
snippetData,
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders itself', () => {
createComponent();
expect(wrapper.find('.js-snippet-view').exists()).toBe(true);
});
});
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