Commit 02211168 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent ce130e21
......@@ -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');
}
}
......
......@@ -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"
......
......@@ -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);
......
......@@ -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';
......@@ -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;
},
};
......@@ -3,6 +3,7 @@ export default () => ({
loading: true,
endpoint: null,
sortField: 'last_seen',
statusFilter: 'unresolved',
searchQuery: null,
indexPath: '',
recentSearches: [],
......
.error-list {
.sort-dropdown {
.dropdown {
min-width: auto;
}
}
---
title: Fix spinner in Create MR dropdown
merge_request: 26679
author:
type: fixed
---
title: Filter sentry error list by status (unresolved/ignored/resolved)
merge_request: 26205
author:
type: added
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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 ""
......
# 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
......@@ -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)
......
......@@ -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": {}
}
]
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);
});
});
});
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);
});
});
......
......@@ -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';
......
......@@ -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');
});
});
});
});
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');
});
});
});
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment