summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js8
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue45
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js7
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js1
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js3
-rw-r--r--app/assets/javascripts/error_tracking/store/list/state.js1
-rw-r--r--app/assets/stylesheets/pages/error_tracking_list.scss2
-rw-r--r--changelogs/unreleased/209207-spinner-appears-to-be-broken.yml5
-rw-r--r--changelogs/unreleased/filter-sentry-error-list.yml5
-rw-r--r--doc/administration/geo/replication/high_availability.md47
-rw-r--r--doc/development/documentation/workflow.md127
-rw-r--r--doc/user/project/operations/error_tracking.md4
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/features/error_tracking/user_filters_errors_by_status_spec.rb40
-rw-r--r--spec/features/error_tracking/user_searches_sentry_errors_spec.rb2
-rw-r--r--spec/fixtures/sentry/issues_sample_response.json42
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js179
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js27
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js14
-rw-r--r--spec/frontend/error_tracking/store/list/mutation_spec.js11
-rw-r--r--spec/javascripts/diffs/components/commit_item_spec.js168
-rw-r--r--spec/lib/sentry/client/issue_spec.rb6
22 files changed, 551 insertions, 199 deletions
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 085df1ff9eb..229612f5e9d 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -42,7 +42,7 @@ export default class CreateMergeRequestDropdown {
this.refInput = this.wrapperEl.querySelector('.js-ref');
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
- this.unavailableButtonArrow = this.unavailableButton.querySelector('.spinner');
+ this.unavailableButtonSpinner = this.unavailableButton.querySelector('.spinner');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
this.branchCreated = false;
@@ -417,12 +417,10 @@ export default class CreateMergeRequestDropdown {
setUnavailableButtonState(isLoading = true) {
if (isLoading) {
- this.unavailableButtonArrow.classList.remove('hide');
- this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
+ this.unavailableButtonSpinner.classList.remove('hide');
this.unavailableButtonText.textContent = __('Checking branch availability...');
} else {
- this.unavailableButtonArrow.classList.add('hide');
- this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
+ this.unavailableButtonSpinner.classList.add('hide');
this.unavailableButtonText.textContent = __('New branch unavailable');
}
}
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 552e8cac3a7..80a88489545 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -16,7 +16,6 @@ import {
GlButtonGroup,
} from '@gitlab/ui';
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 { isEmpty } from 'lodash';
@@ -59,7 +58,7 @@ export default {
{
key: 'status',
label: '',
- tdClass: `${tableDataClass} text-right`,
+ tdClass: `${tableDataClass} text-center`,
},
{
key: 'details',
@@ -67,6 +66,11 @@ export default {
thClass: 'invisible w-0',
},
],
+ statusFilters: {
+ unresolved: __('Unresolved'),
+ ignored: __('Ignored'),
+ resolved: __('Resolved'),
+ },
sortFields: {
last_seen: __('Last Seen'),
first_seen: __('First Seen'),
@@ -83,7 +87,6 @@ export default {
GlLoadingIcon,
GlTable,
GlFormInput,
- Icon,
GlPagination,
TimeAgo,
GlButtonGroup,
@@ -136,6 +139,7 @@ export default {
'sortField',
'recentSearches',
'pagination',
+ 'statusFilter',
'cursor',
]),
paginationRequired() {
@@ -169,6 +173,7 @@ export default {
'fetchPaginatedResults',
'updateStatus',
'removeIgnoredResolvedErrors',
+ 'filterByStatus',
]),
setSearchText(text) {
this.errorSearchQuery = text;
@@ -191,9 +196,16 @@ export default {
isCurrentSortField(field) {
return field === this.sortField;
},
+ isCurrentStatusFilter(filter) {
+ return filter === this.statusFilter;
+ },
getIssueUpdatePath(errorId) {
return `/${this.projectPath}/-/error_tracking/${errorId}.json`;
},
+ filterErrors(status, label) {
+ this.filterValue = label;
+ return this.filterByStatus(status);
+ },
updateIssueStatus(errorId, status) {
this.updateStatus({
endpoint: this.getIssueUpdatePath(errorId),
@@ -260,11 +272,32 @@ export default {
</div>
<gl-dropdown
- class="sort-control"
+ :text="$options.statusFilters[statusFilter]"
+ class="status-dropdown mr-2"
+ menu-class="dropdown"
+ :disabled="loading"
+ >
+ <gl-dropdown-item
+ v-for="(label, status) in $options.statusFilters"
+ :key="status"
+ @click="filterErrors(status, label)"
+ >
+ <span class="d-flex">
+ <gl-icon
+ class="flex-shrink-0 append-right-4"
+ :class="{ invisible: !isCurrentStatusFilter(status) }"
+ name="mobile-issue-close"
+ />
+ {{ label }}
+ </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+
+ <gl-dropdown
:text="$options.sortFields[sortField]"
left
:disabled="loading"
- menu-class="sort-dropdown"
+ menu-class="dropdown"
>
<gl-dropdown-item
v-for="(label, field) in $options.sortFields"
@@ -272,7 +305,7 @@ export default {
@click="sortByField(field)"
>
<span class="d-flex">
- <icon
+ <gl-icon
class="flex-shrink-0 append-right-4"
:class="{ invisible: !isCurrentSortField(field) }"
name="mobile-issue-close"
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 6f8573c0f4d..4170c1bf759 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -18,6 +18,7 @@ export function startPolling({ state, commit, dispatch }) {
search_term: state.searchQuery,
sort: state.sortField,
cursor: state.cursor,
+ issue_status: state.statusFilter,
},
},
successCallback: ({ data }) => {
@@ -83,6 +84,12 @@ export const searchByQuery = ({ commit, dispatch }, query) => {
dispatch('startPolling');
};
+export const filterByStatus = ({ commit, dispatch }, status) => {
+ commit(types.SET_STATUS_FILTER, status);
+ dispatch('stopPolling');
+ dispatch('startPolling');
+};
+
export const sortByField = ({ commit, dispatch }, field) => {
commit(types.SET_CURSOR, null);
commit(types.SET_SORT_FIELD, field);
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 23495cbf01d..872ac8ea8fc 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutation_types.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
@@ -10,3 +10,4 @@ export const SET_SORT_FIELD = 'SET_SORT_FIELD';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_CURSOR = 'SET_CURSOR';
export const REMOVE_IGNORED_RESOLVED_ERRORS = 'REMOVE_IGNORED_RESOLVED_ERRORS';
+export const SET_STATUS_FILTER = 'SET_STATUS_FILTER';
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index 38d156263fb..be0cd4de78d 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -62,4 +62,7 @@ export default {
[types.REMOVE_IGNORED_RESOLVED_ERRORS](state, error) {
state.errors = state.errors.filter(err => err.id !== error);
},
+ [types.SET_STATUS_FILTER](state, query) {
+ state.statusFilter = query;
+ },
};
diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js
index 225a805e709..eb983fde9e0 100644
--- a/app/assets/javascripts/error_tracking/store/list/state.js
+++ b/app/assets/javascripts/error_tracking/store/list/state.js
@@ -3,6 +3,7 @@ export default () => ({
loading: true,
endpoint: null,
sortField: 'last_seen',
+ statusFilter: 'unresolved',
searchQuery: null,
indexPath: '',
recentSearches: [],
diff --git a/app/assets/stylesheets/pages/error_tracking_list.scss b/app/assets/stylesheets/pages/error_tracking_list.scss
index cd1adb9a754..cc391ca6c97 100644
--- a/app/assets/stylesheets/pages/error_tracking_list.scss
+++ b/app/assets/stylesheets/pages/error_tracking_list.scss
@@ -1,5 +1,5 @@
.error-list {
- .sort-dropdown {
+ .dropdown {
min-width: auto;
}
}
diff --git a/changelogs/unreleased/209207-spinner-appears-to-be-broken.yml b/changelogs/unreleased/209207-spinner-appears-to-be-broken.yml
new file mode 100644
index 00000000000..0b1ae5d1838
--- /dev/null
+++ b/changelogs/unreleased/209207-spinner-appears-to-be-broken.yml
@@ -0,0 +1,5 @@
+---
+title: Fix spinner in Create MR dropdown
+merge_request: 26679
+author:
+type: fixed
diff --git a/changelogs/unreleased/filter-sentry-error-list.yml b/changelogs/unreleased/filter-sentry-error-list.yml
new file mode 100644
index 00000000000..36d752ca8ba
--- /dev/null
+++ b/changelogs/unreleased/filter-sentry-error-list.yml
@@ -0,0 +1,5 @@
+---
+title: Filter sentry error list by status (unresolved/ignored/resolved)
+merge_request: 26205
+author:
+type: added
diff --git a/doc/administration/geo/replication/high_availability.md b/doc/administration/geo/replication/high_availability.md
index 5c124e9c6dc..8dd04cdace1 100644
--- a/doc/administration/geo/replication/high_availability.md
+++ b/doc/administration/geo/replication/high_availability.md
@@ -25,10 +25,17 @@ The **primary** and **secondary** Geo deployments must be able to communicate to
## Redis and PostgreSQL High Availability
-The **primary** and **secondary** Redis and PostgreSQL should be configured
-for high availability. Because of the additional complexity involved
-in setting up this configuration for PostgreSQL and Redis,
-it is not covered by this Geo HA documentation.
+Geo supports:
+
+- Redis and PostgreSQL on the **primary** node configured for high availability
+- Redis on **secondary** nodes configured for high availability.
+
+NOTE: **Note:**
+Support for PostgreSQL on **secondary** nodes in high availability configuration
+[is planned](https://gitlab.com/groups/gitlab-org/-/epics/2536).
+
+Because of the additional complexity involved in setting up this configuration
+for PostgreSQL and Redis, it is not covered by this Geo HA documentation.
For more information about setting up a highly available PostgreSQL cluster and Redis cluster using the omnibus package see the high availability documentation for
[PostgreSQL](../../high_availability/database.md) and
@@ -37,10 +44,17 @@ For more information about setting up a highly available PostgreSQL cluster and
NOTE: **Note:**
It is possible to use cloud hosted services for PostgreSQL and Redis, but this is beyond the scope of this document.
-## Prerequisites: A working GitLab HA cluster
+## Prerequisites: Two working GitLab HA clusters
+
+One cluster will serve as the **primary** node. Use the
+[GitLab HA documentation](../../high_availability/README.md) to set this up. If
+you already have a working GitLab instance that is in-use, it can be used as a
+**primary**.
-This cluster will serve as the **primary** node. Use the
+The second cluster will serve as the **secondary** node. Again, use the
[GitLab HA documentation](../../high_availability/README.md) to set this up.
+It's a good idea to log in and test it, however, note that its data will be
+wiped out as part of the process of replicating from the **primary**.
## Configure the GitLab cluster to be the **primary** node
@@ -99,7 +113,11 @@ major differences:
various resources.
Therefore, we will set up the HA components one-by-one, and include deviations
-from the normal HA setup.
+from the normal HA setup. However, we highly recommend first configuring a
+brand-new cluster as if it were not part of a Geo setup so that it can be
+tested and verified as a working cluster. And only then should it be modified
+for use as a Geo **secondary**. This helps to separate problems that are related
+and are not related to Geo setup.
### Step 1: Configure the Redis and Gitaly services on the **secondary** node
@@ -118,7 +136,8 @@ recommended.
### Step 2: Configure the main read-only replica PostgreSQL database on the **secondary** node
NOTE: **Note:** The following documentation assumes the database will be run on
-a single node only, rather than as a PostgreSQL cluster.
+a single node only. PostgreSQL HA on **secondary** nodes is
+[not currently supported](https://gitlab.com/groups/gitlab-org/-/epics/2536).
Configure the [**secondary** database](database.md) as a read-only replica of
the **primary** database. Use the following as a guide.
@@ -167,6 +186,11 @@ the **primary** database. Use the following as a guide.
## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
##
geo_postgresql['enable'] = false
+
+ ##
+ ## Disable `geo_logcursor` service so Rails doesn't get configured here
+ ##
+ geo_logcursor['enable'] = false
```
After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
@@ -335,6 +359,13 @@ On the secondary the following GitLab frontend services will be enabled:
Verify these services by running `sudo gitlab-ctl status` on the frontend
application servers.
+You may wish to run backend application services on backend-specific servers.
+For example, you can disable the `geo-logcursor` service with
+`geo_logcursor['enable'] = false` and run it on application servers not
+attached to the load balancer. On those backend application servers, you would
+disable Unicorn with `unicorn['enable'] = false`. You might also choose to do
+the same thing with the `sidekiq` service.
+
### Step 5: Set up the LoadBalancer for the **secondary** node
In this topology, a load balancer is required at each geographic location to
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index 1b8d2ee434a..5e9d62c169a 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -248,6 +248,133 @@ The following details should be included:
- Include suggested titles of any pages or subsection headings, if applicable.
- List any documentation that should be cross-linked, if applicable.
+### Including docs with code
+
+Currently, the Technical Writing team strongly encourages including documentation in
+the same merge request as the code that it relates to, but this is not strictly mandatory.
+It's still common for documentation to be added in an MR separate from the feature MR.
+
+Engineering teams may elect to adopt a workflow where it is **mandatory** that docs
+are included in the code MR, as part of their [definition of done](../contributing/merge_request_workflow.md#definition-of-done).
+When a team adopts this workflow, that team's engineers must include their docs in the **same**
+MR as their feature code, at all times.
+
+#### Downsides of separate docs MRs
+
+A workflow that has documentation separated into its own MR has many downsides.
+
+If the documentation merges **before** the feature:
+
+- GitLab.com users might try to use the feature before it's released, driving support tickets.
+- If the feature is delayed, the documentation might not be pulled/reverted in time and could be
+ accidentally included in the self-managed package for that release.
+
+If the documentation merges **after** the feature:
+
+- The feature might be included in the self-managed package, but without any documentation
+ if the docs MR misses the cutoff.
+- A feature might show up in the GitLab.com UI before any documentation exists for it.
+ Users surprised by this feature will search for documentation and won't find it, possibly driving
+ support tickets.
+
+Having two separate MRs means:
+
+- Two different people might be responsible for merging one feature, which is not workable
+ with an asynchronous work style. The feature might merge while the technical writer is asleep,
+ creating a potentially lengthy delay between the two merges.
+- If the docs MR is assigned to the same maintainer as responsible for the feature
+ code MR, they will have to review and juggle two MRs instead of dealing with just one.
+
+Documentation quality might be lower, because:
+
+- Having docs in a separate MR will mean far fewer people will see and verify them,
+ increasing the likelihood that issues will be missed.
+- In a "split" workflow, engineers might only create the documentation MR once the
+ feature MR is ready, or almost ready. This gives the technical writer little time
+ to learn about the feature in order to do a good review. It also increases pressure
+ on them to review and merge faster than desired, letting problems slip in due to haste.
+
+#### Benefits of always including docs with code
+
+Including docs with code (and doing it early in the development process) has many benefits:
+
+- There are no timing issues connected to releases:
+ - If a feature slips to the next release, the documentation slips too.
+ - If the feature *just* makes it into a release, the docs *just* make it in too.
+ - If a feature makes it to GitLab.com early, the documentation will be ready for
+ our early adopters.
+- Only a single person will be responsible for merging the feature (the code maintainer).
+- The technical writer will have more time to gain an understanding of the feature
+ and will be better able to verify the content of the docs in the Review App or GDK.
+ They will also be able to offer advice for improving the UI text or offer additional use cases.
+- The documentation will have increased visibility:
+ - Everyone involved in the merge request will see the docs. This could include product
+ managers, multiple engineers with deep domain knowledge, as well as the code reviewers
+ and maintainer. They will be more likely to catch issues with examples, as well
+ as background or concepts that the technical writer may not be aware of.
+ - Increasing visibility of the documentation also has the side effect of improving
+ *other* engineers' documentation. By reviewing each other's MRs, each engineer's
+ own documentation skills will improve.
+- Thinking about the documentation early can help engineers generate better examples,
+ as they will need to think about what examples a user will want, and will need to
+ make sure the code they write implements that example properly.
+
+#### Docs with code as a workflow
+
+In order to have docs included with code as a mandatory workflow, some changes might
+need to happen to a team's current workflow:
+
+- The engineers must strive to include the docs early in the development process,
+ to give ample time for review, not just from the technical writer, but also the
+ code reviewer and maintainer.
+- Reviewers and maintainers must also review the docs during code reviews, to make
+ sure the described processes match the expected use of the feature, and that examples
+ are correct. They do *not* need to worry about style, grammar, and so on.
+- The technical writer must be assigned the MR directly and not only pinged. Thanks
+ to the ability to have [multiple assignees for any MR](../../user/project/merge_requests/getting_started.md#multiple-assignees-starter),
+ this can be done at any time, but must be before the code maintainer review. It's
+ common to have both the docs and code reviews happening at the same time, with the
+ author, reviewer and technical writer discussing the docs together.
+- When the docs are ready, the technical writer will click **Approve** and usually
+ will no longer be involved in the MR. If the feature changes during code review and
+ the docs are updated, the technical writer must be reassigned the MR to verify the
+ update.
+- Maintainers are allowed to merge features with the docs "as-is", even if the technical
+ writer has not given final approval yet. The **docs reviews must not be blockers**. Therefore
+ it's important to get the docs included and assigned to the technical writers early.
+ If the feature is merged before final docs approval, the maintainer must create
+ a [post-merge follow-up issue](#post-merge-reviews), and assign it to both the engineer
+ and technical writer.
+
+Maintainers are allowed to merge features with the docs "as-is" even if the
+technical writer has not given final approval yet but the merge request has
+all other required approvals.
+
+You can visualize the parallel workflow for code and docs reviews as:
+
+```mermaid
+graph TD
+ A("Feature MR Created (Engineer)") --> |Assign| B("Code Review (reviewer)")
+ B --> |"Approve / Reassign"| C("Code Review (maintainer)")
+ C --> |Approve| F("Merge (maintainer)")
+ A --> D("Docs Added (Engineer)")
+ D --> |Assign| E("Docs Review (Tech Writer)")
+ E --> |Approve| F
+```
+
+For complex features split over multiple merge requests:
+
+- If a merge request is implementing components for a future feature, but the components
+ are not accessible to users yet, then no documentation should be included.
+- If a merge request will expose a feature to users in any way, such as an enabled
+ UI element, an API endpoint, or anything similar, then that MR **must** have docs.
+ Note that this may mean multiple docs additions could happen in the buildup to the
+ implementation of a single large feature, for example API docs and feature usage docs.
+- If it's unclear which engineer should add the feature documentation into their
+ MR, the engineering manager should decide during planning, and tie the documentation
+ to the last MR that must be merged before a feature is considered released.
+ This is often, but not always, a frontend MR.
+
## For all other documentation
These documentation changes are not associated with the release of a new or updated feature, and are
diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md
index e7565835be7..bc1256a1c50 100644
--- a/doc/user/project/operations/error_tracking.md
+++ b/doc/user/project/operations/error_tracking.md
@@ -41,8 +41,8 @@ You may also want to enable Sentry's GitLab integration by following the steps i
NOTE: **Note:**
You will need at least Reporter [permissions](../../permissions.md) to view the Error Tracking list.
-The Error Tracking list may be found at **Operations > Error Tracking** in your project's sidebar.
-Errors can be filtered by title or sorted by Frequency, First Seen or Last Seen. Errors are always sorted in descending order by the field specified.
+You can find the Error Tracking list at **Operations > Error Tracking** in your project's sidebar.
+Here, you can filter errors by title or by status (one of Ignored , Resolved, or Unresolved) and sort in descending order by Frequency, First Seen, or Last Seen. By default, the error list is ordered by Last Seen and filtered to Unresolved errors.
![Error Tracking list](img/error_tracking_list_v12_6.png)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4d917cbe943..3c9520fc58f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10533,6 +10533,9 @@ msgstr ""
msgid "Ignore"
msgstr ""
+msgid "Ignored"
+msgstr ""
+
msgid "Image %{imageName} was scheduled for deletion from the registry."
msgstr ""
@@ -21104,6 +21107,9 @@ msgstr ""
msgid "Unresolve thread"
msgstr ""
+msgid "Unresolved"
+msgstr ""
+
msgid "UnscannedProjects|15 or more days"
msgstr ""
diff --git a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
new file mode 100644
index 00000000000..51e29e2a5ec
--- /dev/null
+++ b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'When a user filters Sentry errors by status', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+ include_context 'sentry error tracking context feature'
+
+ let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
+ let_it_be(:filtered_errors_by_status_response) { JSON.parse(issues_response_body).filter { |error| error['status'] == 'ignored' }.to_json }
+ let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" }
+ let(:issues_api_url_filter) { "#{sentry_api_urls.issues_url}?limit=20&query=is:ignored" }
+ let(:auth_token) {{ 'Authorization' => 'Bearer access_token_123' }}
+ let(:return_header) {{ 'Content-Type' => 'application/json' }}
+
+ before do
+ stub_request(:get, issues_api_url).with(headers: auth_token)
+ .to_return(status: 200, body: issues_response_body, headers: return_header)
+
+ stub_request(:get, issues_api_url_filter).with(headers: auth_token)
+ .to_return(status: 200, body: filtered_errors_by_status_response, headers: return_header)
+ end
+
+ it 'displays the results' do
+ sign_in(project.owner)
+ visit project_error_tracking_index_path(project)
+ page.within(find('.gl-table')) do
+ results = page.all('.table-row')
+ expect(results.count).to be(3)
+ end
+
+ find('.status-dropdown .dropdown-toggle').click
+ find('.dropdown-item', text: 'Ignored').click
+
+ page.within(find('.gl-table')) do
+ results = page.all('.table-row')
+ expect(results.count).to be(1)
+ expect(results.first).to have_content(filtered_errors_by_status_response[0]['title'])
+ end
+ end
+end
diff --git a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
index 690c60a3c3f..c5559081feb 100644
--- a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
+++ b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
@@ -26,7 +26,7 @@ describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_
page.within(find('.gl-table')) do
results = page.all('.table-row')
- expect(results.count).to be(2)
+ expect(results.count).to be(3)
end
find('.gl-form-input').set('NotFound').native.send_keys(:return)
diff --git a/spec/fixtures/sentry/issues_sample_response.json b/spec/fixtures/sentry/issues_sample_response.json
index 495562ac960..980e2fb3bd6 100644
--- a/spec/fixtures/sentry/issues_sample_response.json
+++ b/spec/fixtures/sentry/issues_sample_response.json
@@ -82,5 +82,47 @@
"name": "Internal"
},
"statusDetails": {}
+ },
+ {
+ "lastSeen": "2018-12-31T12:00:11Z",
+ "numComments": 0,
+ "userCount": 0,
+ "stats": {
+ "24h": [
+ [
+ 1546437600,
+ 0
+ ]
+ ]
+ },
+ "culprit": "sentry.tasks.reports.deliver_organization_user_report",
+ "title": "Service unknown",
+ "id": "12",
+ "assignedTo": null,
+ "logger": null,
+ "type": "error",
+ "annotations": [],
+ "metadata": {
+ "type": "gaierror",
+ "value": "Service unknown"
+ },
+ "status": "ignored",
+ "subscriptionDetails": null,
+ "isPublic": false,
+ "hasSeen": false,
+ "shortId": "INTERNAL-4",
+ "shareId": null,
+ "firstSeen": "2018-12-17T12:00:14Z",
+ "count": "70",
+ "permalink": "35.228.54.90/sentry/internal/issues/12/",
+ "level": "error",
+ "isSubscribed": true,
+ "isBookmarked": false,
+ "project": {
+ "slug": "internal",
+ "id": "1",
+ "name": "Internal"
+ },
+ "statusDetails": {}
}
]
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
new file mode 100644
index 00000000000..ecc28c78fb7
--- /dev/null
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -0,0 +1,179 @@
+import { mount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import { trimText } from 'helpers/text_helper';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import Component from '~/diffs/components/commit_item.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import getDiffWithCommit from '../mock_data/diff_with_commit';
+
+jest.mock('~/user_popovers');
+
+const TEST_AUTHOR_NAME = 'test';
+const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
+const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
+const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
+const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
+
+describe('diffs/components/commit_item', () => {
+ let wrapper;
+
+ const timeago = getTimeago();
+ const { commit } = getDiffWithCommit();
+
+ const getTitleElement = () => wrapper.find('.commit-row-message.item-title');
+ const getDescElement = () => wrapper.find('pre.commit-row-description');
+ const getDescExpandElement = () =>
+ wrapper.find('.commit-content .text-expander.js-toggle-button');
+ const getShaElement = () => wrapper.find('.commit-sha-group');
+ const getAvatarElement = () => wrapper.find('.user-avatar-link');
+ const getCommitterElement = () => wrapper.find('.committer');
+ const getCommitActionsElement = () => wrapper.find('.commit-actions');
+ const getCommitPipelineStatus = () => wrapper.find(CommitPipelineStatus);
+
+ const defaultProps = {
+ commit: getDiffWithCommit().commit,
+ };
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = mount(Component, {
+ propsData,
+ stubs: {
+ CommitPipelineStatus: true,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('default state', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders commit title', () => {
+ const titleElement = getTitleElement();
+
+ expect(titleElement.attributes('href')).toBe(commit.commit_url);
+ expect(titleElement.text()).toBe(commit.title_html);
+ });
+
+ it('renders commit description', () => {
+ const descElement = getDescElement();
+ const descExpandElement = getDescExpandElement();
+
+ const expected = commit.description_html.replace(/&#x000A;/g, '');
+
+ expect(trimText(descElement.text())).toEqual(trimText(expected));
+ expect(descExpandElement.exists()).toBe(true);
+ });
+
+ it('renders commit sha', () => {
+ const shaElement = getShaElement();
+ const labelElement = shaElement.find('.label');
+ const buttonElement = shaElement.find('button');
+
+ expect(labelElement.text()).toEqual(commit.short_id);
+ expect(buttonElement.props('text')).toBe(commit.id);
+ });
+
+ it('renders author avatar', () => {
+ const avatarElement = getAvatarElement();
+ const imgElement = avatarElement.find('img');
+
+ expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
+ expect(imgElement.classes()).toContain('s40');
+ expect(imgElement.attributes('alt')).toBe(commit.author.name);
+ expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
+ });
+
+ it('renders committer text', () => {
+ const committerElement = getCommitterElement();
+ const nameElement = committerElement.find('a');
+
+ const expectTimeText = timeago.format(commit.authored_date);
+ const expectedText = `${commit.author.name} authored ${expectTimeText}`;
+
+ expect(trimText(committerElement.text())).toEqual(expectedText);
+ expect(nameElement.attributes('href')).toBe(commit.author.web_url);
+ expect(nameElement.text()).toBe(commit.author.name);
+ expect(nameElement.classes()).toContain('js-user-link');
+ expect(nameElement.attributes('data-user-id')).toEqual(commit.author.id.toString());
+ });
+ });
+
+ describe('without commit description', () => {
+ beforeEach(() => {
+ mountComponent({ defaultProps, commit: { ...defaultProps.commit, description_html: '' } });
+ });
+
+ it('hides description', () => {
+ const descElement = getDescElement();
+ const descExpandElement = getDescExpandElement();
+
+ expect(descElement.exists()).toBeFalsy();
+ expect(descExpandElement.exists()).toBeFalsy();
+ });
+ });
+
+ describe('with no matching user', () => {
+ beforeEach(() => {
+ mountComponent({
+ defaultProps,
+ commit: {
+ ...defaultProps.commit,
+ author: null,
+ author_email: TEST_AUTHOR_EMAIL,
+ author_name: TEST_AUTHOR_NAME,
+ author_gravatar_url: TEST_AUTHOR_GRAVATAR,
+ },
+ });
+ });
+
+ it('renders author avatar', () => {
+ const avatarElement = getAvatarElement();
+ const imgElement = avatarElement.find('img');
+
+ expect(avatarElement.attributes('href')).toBe(`mailto:${TEST_AUTHOR_EMAIL}`);
+ expect(imgElement.attributes('alt')).toBe(TEST_AUTHOR_NAME);
+ expect(imgElement.attributes('src')).toBe(TEST_AUTHOR_GRAVATAR);
+ });
+
+ it('renders committer text', () => {
+ const committerElement = getCommitterElement();
+ const nameElement = committerElement.find('a');
+
+ expect(nameElement.attributes('href')).toBe(`mailto:${TEST_AUTHOR_EMAIL}`);
+ expect(nameElement.text()).toBe(TEST_AUTHOR_NAME);
+ });
+ });
+
+ describe('with signature', () => {
+ beforeEach(() => {
+ mountComponent({
+ defaultProps,
+ commit: { ...defaultProps.commit, signature_html: TEST_SIGNATURE_HTML },
+ });
+ });
+
+ it('renders signature html', () => {
+ const actionsElement = getCommitActionsElement();
+
+ expect(actionsElement.html()).toContain(TEST_SIGNATURE_HTML);
+ });
+ });
+
+ describe('with pipeline status', () => {
+ beforeEach(() => {
+ mountComponent({
+ defaultProps,
+ commit: { ...defaultProps.commit, pipeline_status_path: TEST_PIPELINE_STATUS_PATH },
+ });
+ });
+
+ it('renders pipeline status', () => {
+ expect(getCommitPipelineStatus().exists()).toBe(true);
+ });
+ });
+});
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 f852a3091aa..cd6dd5c7519 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -1,6 +1,6 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import errorsList from './list_mock.json';
@@ -15,9 +15,19 @@ describe('ErrorTrackingList', () => {
const findErrorListTable = () => wrapper.find('table');
const findErrorListRows = () => wrapper.findAll('tbody tr');
- const findSortDropdown = () => wrapper.find('.sort-dropdown');
+ const dropdownsArray = () => wrapper.findAll(GlDropdown);
const findRecentSearchesDropdown = () =>
- wrapper.find('.filtered-search-history-dropdown-wrapper');
+ dropdownsArray()
+ .at(0)
+ .find(GlDropdown);
+ const findStatusFilterDropdown = () =>
+ dropdownsArray()
+ .at(1)
+ .find(GlDropdown);
+ const findSortDropdown = () =>
+ dropdownsArray()
+ .at(2)
+ .find(GlDropdown);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPagination = () => wrapper.find(GlPagination);
@@ -60,6 +70,7 @@ describe('ErrorTrackingList', () => {
fetchPaginatedResults: jest.fn(),
updateStatus: jest.fn(),
removeIgnoredResolvedErrors: jest.fn(),
+ filterByStatus: jest.fn(),
};
const state = {
@@ -167,10 +178,16 @@ describe('ErrorTrackingList', () => {
});
it('it sorts by fields', () => {
- const findSortItem = () => wrapper.find('.dropdown-item');
+ const findSortItem = () => findSortDropdown().find('.dropdown-item');
findSortItem().trigger('click');
expect(actions.sortByField).toHaveBeenCalled();
});
+
+ it('it filters by status', () => {
+ const findStatusFilter = () => findStatusFilterDropdown().find('.dropdown-item');
+ findStatusFilter().trigger('click');
+ expect(actions.filterByStatus).toHaveBeenCalled();
+ });
});
});
@@ -215,7 +232,7 @@ describe('ErrorTrackingList', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListTable().exists()).toBe(false);
- expect(findSortDropdown().exists()).toBe(false);
+ expect(dropdownsArray().length).toBe(0);
});
});
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 54fdde88818..3cb740bf05d 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -88,6 +88,20 @@ describe('error tracking actions', () => {
});
});
+ describe('filterByStatus', () => {
+ it('should search errors by status', () => {
+ const status = 'ignored';
+
+ testAction(
+ actions.filterByStatus,
+ status,
+ {},
+ [{ type: types.SET_STATUS_FILTER, payload: status }],
+ [{ type: 'stopPolling' }, { type: 'startPolling' }],
+ );
+ });
+ });
+
describe('sortByField', () => {
it('should search by query', () => {
const field = 'frequency';
diff --git a/spec/frontend/error_tracking/store/list/mutation_spec.js b/spec/frontend/error_tracking/store/list/mutation_spec.js
index 65f11aeeda1..a326a6c55c0 100644
--- a/spec/frontend/error_tracking/store/list/mutation_spec.js
+++ b/spec/frontend/error_tracking/store/list/mutation_spec.js
@@ -6,6 +6,7 @@ const ADD_RECENT_SEARCH = mutations[types.ADD_RECENT_SEARCH];
const CLEAR_RECENT_SEARCHES = mutations[types.CLEAR_RECENT_SEARCHES];
const LOAD_RECENT_SEARCHES = mutations[types.LOAD_RECENT_SEARCHES];
const REMOVE_IGNORED_RESOLVED_ERRORS = mutations[types.REMOVE_IGNORED_RESOLVED_ERRORS];
+const SET_STATUS_FILTER = mutations[types.SET_STATUS_FILTER];
describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => {
@@ -139,5 +140,15 @@ describe('Error tracking mutations', () => {
expect(state.errors).not.toContain(ignoredError);
});
});
+
+ describe('SET_STATUS_FILTER', () => {
+ it('sets the filter to ignored, resolved or unresolved', () => {
+ state.statusFilter = 'unresolved';
+
+ SET_STATUS_FILTER(state, 'ignored');
+
+ expect(state.statusFilter).toBe('ignored');
+ });
+ });
});
});
diff --git a/spec/javascripts/diffs/components/commit_item_spec.js b/spec/javascripts/diffs/components/commit_item_spec.js
deleted file mode 100644
index 9fe31a5e727..00000000000
--- a/spec/javascripts/diffs/components/commit_item_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import Vue from 'vue';
-import { TEST_HOST } from 'spec/test_constants';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { trimText } from 'spec/helpers/text_helper';
-import { getTimeago } from '~/lib/utils/datetime_utility';
-import CommitItem from '~/diffs/components/commit_item.vue';
-import getDiffWithCommit from '../mock_data/diff_with_commit';
-
-const TEST_AUTHOR_NAME = 'test';
-const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
-const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
-const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
-const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
-
-const getTitleElement = vm => vm.$el.querySelector('.commit-row-message.item-title');
-const getDescElement = vm => vm.$el.querySelector('pre.commit-row-description');
-const getDescExpandElement = vm =>
- vm.$el.querySelector('.commit-content .text-expander.js-toggle-button');
-const getShaElement = vm => vm.$el.querySelector('.commit-sha-group');
-const getAvatarElement = vm => vm.$el.querySelector('.user-avatar-link');
-const getCommitterElement = vm => vm.$el.querySelector('.committer');
-const getCommitActionsElement = vm => vm.$el.querySelector('.commit-actions');
-
-describe('diffs/components/commit_item', () => {
- const Component = Vue.extend(CommitItem);
- const timeago = getTimeago();
- const { commit } = getDiffWithCommit();
-
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- commit: getDiffWithCommit().commit,
- });
- });
-
- it('renders commit title', () => {
- const titleElement = getTitleElement(vm);
-
- expect(titleElement).toHaveAttr('href', commit.commit_url);
- expect(titleElement).toHaveText(commit.title_html);
- });
-
- // https://gitlab.com/gitlab-org/gitlab/issues/197139
- // eslint-disable-next-line jasmine/no-disabled-tests
- xit('renders commit description', () => {
- const descElement = getDescElement(vm);
- const descExpandElement = getDescExpandElement(vm);
-
- const expected = commit.description_html.replace(/&#x000A;/g, '');
-
- expect(trimText(descElement.innerHTML)).toEqual(trimText(expected));
- expect(descExpandElement).not.toBeNull();
- });
-
- it('renders commit sha', () => {
- const shaElement = getShaElement(vm);
- const labelElement = shaElement.querySelector('.label');
- const buttonElement = shaElement.querySelector('button');
-
- expect(labelElement.textContent).toEqual(commit.short_id);
- expect(buttonElement).toHaveData('clipboard-text', commit.id);
- });
-
- it('renders author avatar', () => {
- const avatarElement = getAvatarElement(vm);
- const imgElement = avatarElement.querySelector('img');
-
- expect(avatarElement).toHaveAttr('href', commit.author.web_url);
- expect(imgElement).toHaveClass('s40');
- expect(imgElement).toHaveAttr('alt', commit.author.name);
- expect(imgElement).toHaveAttr('src', commit.author.avatar_url);
- });
-
- it('renders committer text', () => {
- const committerElement = getCommitterElement(vm);
- const nameElement = committerElement.querySelector('a');
-
- const expectTimeText = timeago.format(commit.authored_date);
- const expectedText = `${commit.author.name} authored ${expectTimeText}`;
-
- expect(trimText(committerElement.textContent)).toEqual(expectedText);
- expect(nameElement).toHaveAttr('href', commit.author.web_url);
- expect(nameElement).toHaveText(commit.author.name);
- expect(nameElement).toHaveClass('js-user-link');
- expect(nameElement.dataset.userId).toEqual(commit.author.id.toString());
- });
-
- describe('without commit description', () => {
- beforeEach(done => {
- vm.commit.description_html = '';
-
- vm.$nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('hides description', () => {
- const descElement = getDescElement(vm);
- const descExpandElement = getDescExpandElement(vm);
-
- expect(descElement).toBeNull();
- expect(descExpandElement).toBeNull();
- });
- });
-
- describe('with no matching user', () => {
- beforeEach(done => {
- vm.commit.author = null;
- vm.commit.author_email = TEST_AUTHOR_EMAIL;
- vm.commit.author_name = TEST_AUTHOR_NAME;
- vm.commit.author_gravatar_url = TEST_AUTHOR_GRAVATAR;
-
- vm.$nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('renders author avatar', () => {
- const avatarElement = getAvatarElement(vm);
- const imgElement = avatarElement.querySelector('img');
-
- expect(avatarElement).toHaveAttr('href', `mailto:${TEST_AUTHOR_EMAIL}`);
- expect(imgElement).toHaveAttr('alt', TEST_AUTHOR_NAME);
- expect(imgElement).toHaveAttr('src', TEST_AUTHOR_GRAVATAR);
- });
-
- it('renders committer text', () => {
- const committerElement = getCommitterElement(vm);
- const nameElement = committerElement.querySelector('a');
-
- expect(nameElement).toHaveAttr('href', `mailto:${TEST_AUTHOR_EMAIL}`);
- expect(nameElement).toHaveText(TEST_AUTHOR_NAME);
- });
- });
-
- describe('with signature', () => {
- beforeEach(done => {
- vm.commit.signature_html = TEST_SIGNATURE_HTML;
-
- vm.$nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('renders signature html', () => {
- const actionsElement = getCommitActionsElement(vm);
-
- expect(actionsElement).toContainHtml(TEST_SIGNATURE_HTML);
- });
- });
-
- describe('with pipeline status', () => {
- beforeEach(done => {
- vm.commit.pipeline_status_path = TEST_PIPELINE_STATUS_PATH;
-
- vm.$nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('renders pipeline status', () => {
- const actionsElement = getCommitActionsElement(vm);
-
- expect(actionsElement).toContainElement('.ci-status-link');
- });
- });
-});
diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb
index d35e4b83d7f..62cbfbf0b30 100644
--- a/spec/lib/sentry/client/issue_spec.rb
+++ b/spec/lib/sentry/client/issue_spec.rb
@@ -49,7 +49,7 @@ describe Sentry::Client::Issue do
it_behaves_like 'calls sentry api'
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
- it_behaves_like 'issues have correct length', 2
+ it_behaves_like 'issues have correct length', 3
shared_examples 'has correct external_url' do
context 'external_url' do
@@ -184,7 +184,7 @@ describe Sentry::Client::Issue do
it_behaves_like 'calls sentry api'
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
- it_behaves_like 'issues have correct length', 2
+ it_behaves_like 'issues have correct length', 3
end
context 'when cursor is present' do
@@ -194,7 +194,7 @@ describe Sentry::Client::Issue do
it_behaves_like 'calls sentry api'
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
- it_behaves_like 'issues have correct length', 2
+ it_behaves_like 'issues have correct length', 3
end
end