diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-13 18:08:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-13 18:08:06 +0000 |
commit | 7cc6c10c68915f5019ab8c2029eeb462c8fed4ef (patch) | |
tree | 419e5fee5bb60e71bef076157627812d54e142bc | |
parent | 630101f7f93847f39a4d2f87d92f514c973cdc1e (diff) | |
download | gitlab-ce-7cc6c10c68915f5019ab8c2029eeb462c8fed4ef.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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); + }); +}); |