Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG-EE.md7
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue68
-rw-r--r--app/assets/javascripts/error_tracking/services/index.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js30
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js12
-rw-r--r--app/assets/javascripts/error_tracking/store/list/state.js4
-rw-r--r--app/assets/javascripts/error_tracking/utils.js13
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js3
-rw-r--r--app/assets/javascripts/projects/project_new.js18
-rw-r--r--app/assets/javascripts/snippets/components/app.vue31
-rw-r--r--app/assets/javascripts/snippets/index.js34
-rw-r--r--app/assets/javascripts/snippets/queries/getSnippet.query.graphql13
-rw-r--r--app/assets/stylesheets/pages/error_tracking_list.scss5
-rw-r--r--app/assets/stylesheets/pages/groups.scss1
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--changelogs/unreleased/34068-sort-error-list.yml5
-rw-r--r--doc/ci/merge_request_pipelines/index.md30
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js37
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js103
-rw-r--r--spec/frontend/error_tracking/utils_spec.js11
-rw-r--r--spec/frontend/snippets/components/app_spec.js41
26 files changed, 398 insertions, 96 deletions
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md
index c808664207b..ef59ca31a36 100644
--- a/CHANGELOG-EE.md
+++ b/CHANGELOG-EE.md
@@ -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
+## 12.0.12
+
+### Fixed (1 change)
+
+- Backport the new reliable fetcher to 12.0.9. !20532
+
+
## 12.0.10
- No changes.
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 5cd68687329..23e251e4201 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -17,8 +17,6 @@ import AccessorUtils from '~/lib/utils/accessor';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { trackViewInSentryOptions } from '../utils';
export default {
fields: [
@@ -27,6 +25,11 @@ export default {
{ key: 'users', label: __('Users') },
{ key: 'lastSeen', label: __('Last seen'), thClass: 'w-15p' },
],
+ sortFields: {
+ last_seen: __('Last Seen'),
+ first_seen: __('First Seen'),
+ frequency: __('Frequency'),
+ },
components: {
GlEmptyState,
GlButton,
@@ -43,7 +46,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- TrackEvent: TrackEventDirective,
},
props: {
indexPath: {
@@ -74,45 +76,47 @@ export default {
};
},
computed: {
- ...mapState('list', ['errors', 'externalUrl', 'loading', 'recentSearches']),
+ ...mapState('list', ['errors', 'loading', 'searchQuery', 'sortField', 'recentSearches']),
},
created() {
if (this.errorTrackingEnabled) {
- this.startPolling(this.indexPath);
+ this.setEndpoint(this.indexPath);
+ this.startPolling();
}
},
methods: {
...mapActions('list', [
'startPolling',
'restartPolling',
+ 'setEndpoint',
+ 'searchByQuery',
+ 'sortByField',
'addRecentSearch',
'clearRecentSearches',
'loadRecentSearches',
'setIndexPath',
]),
- filterErrors() {
- const searchTerm = this.errorSearchQuery.trim();
- this.addRecentSearch(searchTerm);
-
- this.startPolling(`${this.indexPath}?search_term=${searchTerm}`);
- },
setSearchText(text) {
this.errorSearchQuery = text;
- this.filterErrors();
+ this.searchByQuery(text);
},
- trackViewInSentryOptions,
getDetailsLink(errorId) {
return `error_tracking/${errorId}/details`;
},
+ isCurrentSortField(field) {
+ return field === this.sortField;
+ },
},
};
</script>
<template>
- <div>
+ <div class="error-list">
<div v-if="errorTrackingEnabled">
- <div class="d-flex flex-row justify-content-around bg-secondary border p-3">
- <div class="filtered-search-box">
+ <div
+ 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
:text="__('Recent searches')"
class="filtered-search-history-dropdown-wrapper d-none d-md-block"
@@ -143,7 +147,7 @@ export default {
:disabled="loading"
:placeholder="__('Search or filter results…')"
autofocus
- @keyup.enter.native="filterErrors"
+ @keyup.enter.native="searchByQuery(errorSearchQuery)"
/>
</div>
<div class="gl-search-box-by-type-right-icons">
@@ -160,16 +164,28 @@ export default {
</div>
</div>
- <gl-button
- v-track-event="trackViewInSentryOptions(externalUrl)"
- class="ml-3"
- variant="primary"
- :href="externalUrl"
- target="_blank"
+ <gl-dropdown
+ :text="$options.sortFields[sortField]"
+ left
+ :disabled="loading"
+ class="mr-3"
+ menu-class="sort-dropdown"
>
- {{ __('View in Sentry') }}
- <icon name="external-link" class="flex-shrink-0" />
- </gl-button>
+ <gl-dropdown-item
+ v-for="(label, field) in $options.sortFields"
+ :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 v-if="loading" class="py-3">
diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js
index 68988296cc2..3b3f8311d67 100644
--- a/app/assets/javascripts/error_tracking/services/index.js
+++ b/app/assets/javascripts/error_tracking/services/index.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
export default {
- getSentryData({ endpoint }) {
- return axios.get(endpoint);
+ getSentryData({ endpoint, params }) {
+ return axios.get(endpoint, { params });
},
};
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 13b15549d81..b1c81b55e58 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -6,19 +6,24 @@ import { __, sprintf } from '~/locale';
let eTagPoll;
-export function startPolling({ commit, dispatch }, endpoint) {
+export function startPolling({ state, commit, dispatch }) {
commit(types.SET_LOADING, true);
eTagPoll = new Poll({
resource: Service,
method: 'getSentryData',
- data: { endpoint },
+ data: {
+ endpoint: state.endpoint,
+ params: {
+ search_term: state.searchQuery,
+ sort: state.sortField,
+ },
+ },
successCallback: ({ data }) => {
if (!data) {
return;
}
commit(types.SET_ERRORS, data.errors);
- commit(types.SET_EXTERNAL_URL, data.external_url);
commit(types.SET_LOADING, false);
dispatch('stopPolling');
},
@@ -45,7 +50,6 @@ export const stopPolling = () => {
export function restartPolling({ commit }) {
commit(types.SET_ERRORS, []);
- commit(types.SET_EXTERNAL_URL, '');
commit(types.SET_LOADING, true);
if (eTagPoll) eTagPoll.restart();
@@ -67,4 +71,22 @@ export function clearRecentSearches({ commit }) {
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 () => {};
diff --git a/app/assets/javascripts/error_tracking/store/list/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
index 4199e8d5cda..3ebfef76324 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutation_types.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
@@ -1,7 +1,9 @@
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_LOADING = 'SET_LOADING';
export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH';
export const CLEAR_RECENT_SEARCHES = 'CLEAR_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';
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index 18404d3b0af..048660eaeeb 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -6,9 +6,6 @@ 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;
},
@@ -47,4 +44,13 @@ export default {
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;
+ },
};
diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js
index f1f0369e5f3..f20b707142e 100644
--- a/app/assets/javascripts/error_tracking/store/list/state.js
+++ b/app/assets/javascripts/error_tracking/store/list/state.js
@@ -1,7 +1,9 @@
export default () => ({
errors: [],
- externalUrl: '',
loading: true,
+ endpoint: null,
+ sortField: 'last_seen',
+ searchQuery: null,
indexPath: '',
recentSearches: [],
});
diff --git a/app/assets/javascripts/error_tracking/utils.js b/app/assets/javascripts/error_tracking/utils.js
index b832b1371b1..3c382ccd1aa 100644
--- a/app/assets/javascripts/error_tracking/utils.js
+++ b/app/assets/javascripts/error_tracking/utils.js
@@ -1,15 +1,4 @@
-/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
-
-/**
- * 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,
-});
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings, import/prefer-default-export */
/**
* Tracks snowplow event when User clicks on error link to Sentry
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index 5efcc901fde..6e00c14f43e 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -3,6 +3,7 @@ import BlobViewer from '~/blob/viewer';
import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import snippetEmbed from '~/snippet/snippet_embed';
+import initSnippetsApp from '~/snippets';
document.addEventListener('DOMContentLoaded', () => {
if (!gon.features.snippetsVue) {
@@ -11,5 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
initNotes();
new ZenMode(); // eslint-disable-line no-new
snippetEmbed();
+ } else {
+ initSnippetsApp();
}
});
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index dac105d7243..92c4c05bd87 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -128,15 +128,15 @@ const bindEvents = () => {
},
iosswift: {
text: s__('ProjectTemplates|iOS (Swift)'),
- icon: '.template-option svg.icon-gitlab',
+ icon: '.template-option .icon-iosswift',
},
dotnetcore: {
text: s__('ProjectTemplates|.NET Core'),
- icon: '.template-option .icon-dotnet',
+ icon: '.template-option .icon-dotnetcore',
},
android: {
text: s__('ProjectTemplates|Android'),
- icon: '.template-option svg.icon-android',
+ icon: '.template-option .icon-android',
},
gomicro: {
text: s__('ProjectTemplates|Go Micro'),
@@ -164,27 +164,27 @@ const bindEvents = () => {
},
nfhugo: {
text: s__('ProjectTemplates|Netlify/Hugo'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfhugo',
},
nfjekyll: {
text: s__('ProjectTemplates|Netlify/Jekyll'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfjekyll',
},
nfplainhtml: {
text: s__('ProjectTemplates|Netlify/Plain HTML'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfplainhtml',
},
nfgitbook: {
text: s__('ProjectTemplates|Netlify/GitBook'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfgitbook',
},
nfhexo: {
text: s__('ProjectTemplates|Netlify/Hexo'),
- icon: '.template-option .icon-netlify',
+ icon: '.template-option .icon-nfhexo',
},
salesforcedx: {
text: s__('ProjectTemplates|SalesforceDX'),
- icon: '.template-option svg.icon-gitlab',
+ icon: '.template-option .icon-salesforcedx',
},
serverless_framework: {
text: s__('ProjectTemplates|Serverless Framework/JS'),
diff --git a/app/assets/javascripts/snippets/components/app.vue b/app/assets/javascripts/snippets/components/app.vue
new file mode 100644
index 00000000000..e3d6cdd4606
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/app.vue
@@ -0,0 +1,31 @@
+<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>
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
new file mode 100644
index 00000000000..654856f8d14
--- /dev/null
+++ b/app/assets/javascripts/snippets/index.js
@@ -0,0 +1,34 @@
+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,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/snippets/queries/getSnippet.query.graphql b/app/assets/javascripts/snippets/queries/getSnippet.query.graphql
new file mode 100644
index 00000000000..5a5f0d05c5b
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/getSnippet.query.graphql
@@ -0,0 +1,13 @@
+query getSnippet($ids: [ID!]) {
+ snippets(ids: $ids) {
+ edges {
+ node {
+ title
+ description
+ createdAt
+ updatedAt
+ visibility
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/error_tracking_list.scss b/app/assets/stylesheets/pages/error_tracking_list.scss
new file mode 100644
index 00000000000..cd1adb9a754
--- /dev/null
+++ b/app/assets/stylesheets/pages/error_tracking_list.scss
@@ -0,0 +1,5 @@
+.error-list {
+ .sort-dropdown {
+ min-width: auto;
+ }
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 1502cf18440..1cf72c51ca7 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -25,6 +25,7 @@
.description p {
margin-bottom: 0;
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 395c469255e..3444e423235 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,7 +1,7 @@
- group = local_assigns.fetch(:group)
- css_class = 'no-description' if group.description.blank?
-%li.group-row{ class: css_class }
+%li.group-row.py-3{ class: css_class }
.controls
= 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'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 609b8dce21a..e47967ef622 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,7 +1,7 @@
- user = local_assigns.fetch(:user, current_user)
- 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
%span
= icon('bookmark')
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index f38e30b0c55..9e038854c59 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,7 +1,7 @@
- link_project = local_assigns.fetch(:link_project, false)
- 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: ''
.title
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index c77b05e3ea8..080c0ab6ece 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -5,7 +5,7 @@
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
- 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
= render 'shared/snippets/header'
diff --git a/changelogs/unreleased/34068-sort-error-list.yml b/changelogs/unreleased/34068-sort-error-list.yml
new file mode 100644
index 00000000000..4c33c28a1ca
--- /dev/null
+++ b/changelogs/unreleased/34068-sort-error-list.yml
@@ -0,0 +1,5 @@
+---
+title: Sort Sentry error list by first seen, last seen or frequency
+merge_request: 21250
+author:
+type: added
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index 7a7fa08fac1..9ac41f88ff6 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -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
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
Note that the current behavior is subject to change. In the usual contribution
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 80a0966d069..162dda4d044 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7735,6 +7735,9 @@ msgstr ""
msgid "Finished"
msgstr ""
+msgid "First Seen"
+msgstr ""
+
msgid "First day of the week"
msgstr ""
@@ -7861,6 +7864,9 @@ msgstr ""
msgid "Free Trial of GitLab.com Gold"
msgstr ""
+msgid "Frequency"
+msgstr ""
+
msgid "Friday"
msgstr ""
@@ -10201,6 +10207,9 @@ msgstr ""
msgid "Last Pipeline"
msgstr ""
+msgid "Last Seen"
+msgstr ""
+
msgid "Last accessed on"
msgstr ""
@@ -19762,9 +19771,6 @@ msgstr ""
msgid "View group labels"
msgstr ""
-msgid "View in Sentry"
-msgstr ""
-
msgid "View it on GitLab"
msgstr ""
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 776ce589cff..9ec3d42f0d4 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -1,7 +1,6 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import {
- GlButton,
GlEmptyState,
GlLoadingIcon,
GlTable,
@@ -24,7 +23,9 @@ describe('ErrorTrackingList', () => {
const findErrorListTable = () => wrapper.find('table');
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);
function mountComponent({
@@ -33,6 +34,8 @@ describe('ErrorTrackingList', () => {
stubs = {
'gl-link': GlLink,
'gl-table': GlTable,
+ 'gl-dropdown': GlDropdown,
+ 'gl-dropdown-item': GlDropdownItem,
},
} = {}) {
wrapper = shallowMount(ErrorTrackingList, {
@@ -46,6 +49,9 @@ describe('ErrorTrackingList', () => {
illustrationPath: 'illustration/path',
},
stubs,
+ data() {
+ return { errorSearchQuery: 'search' };
+ },
});
}
@@ -58,6 +64,9 @@ describe('ErrorTrackingList', () => {
loadRecentSearches: jest.fn(),
setIndexPath: jest.fn(),
clearRecentSearches: jest.fn(),
+ setEndpoint: jest.fn(),
+ searchByQuery: jest.fn(),
+ sortByField: jest.fn(),
};
const state = createListState();
@@ -101,7 +110,7 @@ describe('ErrorTrackingList', () => {
it('shows table', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListTable().exists()).toBe(true);
- expect(findButton().exists()).toBe(true);
+ expect(findSortDropdown().exists()).toBe(true);
});
it('shows list of errors in a table', () => {
@@ -121,16 +130,20 @@ describe('ErrorTrackingList', () => {
describe('filtering', () => {
const findSearchBox = () => wrapper.find(GlFormInput);
- it('shows search box', () => {
+ it('shows search box & sort dropdown', () => {
expect(findSearchBox().exists()).toBe(true);
+ expect(findSortDropdown().exists()).toBe(true);
});
- it('makes network request on submit', () => {
- expect(actions.startPolling).toHaveBeenCalledTimes(1);
-
+ it('it searches by query', () => {
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', () => {
it('shows empty table', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListRows().length).toEqual(1);
- expect(findButton().exists()).toBe(true);
+ expect(findSortDropdown().exists()).toBe(true);
});
it('shows a message prompting to refresh', () => {
@@ -170,7 +183,7 @@ describe('ErrorTrackingList', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListTable().exists()).toBe(false);
- expect(findButton().exists()).toBe(false);
+ expect(findSortDropdown().exists()).toBe(false);
});
});
@@ -201,13 +214,13 @@ describe('ErrorTrackingList', () => {
it('shows empty message', () => {
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', () => {
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.at(0).text()).toBe('great');
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 408b0205e93..fb659db9ab5 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -1,8 +1,13 @@
-import MockAdapter from 'axios-mock-adapter';
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 types from '~/error_tracking/store/list/mutation_types';
+jest.mock('~/flash.js');
+
describe('error tracking actions', () => {
let mock;
@@ -15,15 +20,97 @@ describe('error tracking actions', () => {
});
describe('startPolling', () => {
- it('commits SET_LOADING', () => {
- mock.onGet().reply(200);
- const endpoint = '/errors';
- const commit = jest.fn();
- const state = {};
+ it('should start polling for data', done => {
+ const payload = { errors: [{ id: 1 }, { id: 2 }] };
+
+ mock.onGet().reply(httpStatusCodes.OK, payload);
+ 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 } }],
+ [],
+ );
});
});
});
diff --git a/spec/frontend/error_tracking/utils_spec.js b/spec/frontend/error_tracking/utils_spec.js
index 0e9047cd375..a0d6f7f009d 100644
--- a/spec/frontend/error_tracking/utils_spec.js
+++ b/spec/frontend/error_tracking/utils_spec.js
@@ -3,17 +3,6 @@ import * as errorTrackingUtils from '~/error_tracking/utils';
const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
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', () => {
it('should return correct event options', () => {
expect(errorTrackingUtils.trackClickErrorLinkToSentryOptions(externalUrl)).toEqual({
diff --git a/spec/frontend/snippets/components/app_spec.js b/spec/frontend/snippets/components/app_spec.js
new file mode 100644
index 00000000000..535e71b6da7
--- /dev/null
+++ b/spec/frontend/snippets/components/app_spec.js
@@ -0,0 +1,41 @@
+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);
+ });
+});