Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ce130e2118
commit
0221116862
|
@ -42,7 +42,7 @@ export default class CreateMergeRequestDropdown {
|
||||||
this.refInput = this.wrapperEl.querySelector('.js-ref');
|
this.refInput = this.wrapperEl.querySelector('.js-ref');
|
||||||
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
|
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
|
||||||
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
|
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.unavailableButtonText = this.unavailableButton.querySelector('.text');
|
||||||
|
|
||||||
this.branchCreated = false;
|
this.branchCreated = false;
|
||||||
|
@ -417,12 +417,10 @@ export default class CreateMergeRequestDropdown {
|
||||||
|
|
||||||
setUnavailableButtonState(isLoading = true) {
|
setUnavailableButtonState(isLoading = true) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
this.unavailableButtonArrow.classList.remove('hide');
|
this.unavailableButtonSpinner.classList.remove('hide');
|
||||||
this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
|
|
||||||
this.unavailableButtonText.textContent = __('Checking branch availability...');
|
this.unavailableButtonText.textContent = __('Checking branch availability...');
|
||||||
} else {
|
} else {
|
||||||
this.unavailableButtonArrow.classList.add('hide');
|
this.unavailableButtonSpinner.classList.add('hide');
|
||||||
this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
|
|
||||||
this.unavailableButtonText.textContent = __('New branch unavailable');
|
this.unavailableButtonText.textContent = __('New branch unavailable');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
GlButtonGroup,
|
GlButtonGroup,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import AccessorUtils from '~/lib/utils/accessor';
|
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 TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
@ -59,7 +58,7 @@ export default {
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
label: '',
|
label: '',
|
||||||
tdClass: `${tableDataClass} text-right`,
|
tdClass: `${tableDataClass} text-center`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'details',
|
key: 'details',
|
||||||
|
@ -67,6 +66,11 @@ export default {
|
||||||
thClass: 'invisible w-0',
|
thClass: 'invisible w-0',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
statusFilters: {
|
||||||
|
unresolved: __('Unresolved'),
|
||||||
|
ignored: __('Ignored'),
|
||||||
|
resolved: __('Resolved'),
|
||||||
|
},
|
||||||
sortFields: {
|
sortFields: {
|
||||||
last_seen: __('Last Seen'),
|
last_seen: __('Last Seen'),
|
||||||
first_seen: __('First Seen'),
|
first_seen: __('First Seen'),
|
||||||
|
@ -83,7 +87,6 @@ export default {
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlTable,
|
GlTable,
|
||||||
GlFormInput,
|
GlFormInput,
|
||||||
Icon,
|
|
||||||
GlPagination,
|
GlPagination,
|
||||||
TimeAgo,
|
TimeAgo,
|
||||||
GlButtonGroup,
|
GlButtonGroup,
|
||||||
|
@ -136,6 +139,7 @@ export default {
|
||||||
'sortField',
|
'sortField',
|
||||||
'recentSearches',
|
'recentSearches',
|
||||||
'pagination',
|
'pagination',
|
||||||
|
'statusFilter',
|
||||||
'cursor',
|
'cursor',
|
||||||
]),
|
]),
|
||||||
paginationRequired() {
|
paginationRequired() {
|
||||||
|
@ -169,6 +173,7 @@ export default {
|
||||||
'fetchPaginatedResults',
|
'fetchPaginatedResults',
|
||||||
'updateStatus',
|
'updateStatus',
|
||||||
'removeIgnoredResolvedErrors',
|
'removeIgnoredResolvedErrors',
|
||||||
|
'filterByStatus',
|
||||||
]),
|
]),
|
||||||
setSearchText(text) {
|
setSearchText(text) {
|
||||||
this.errorSearchQuery = text;
|
this.errorSearchQuery = text;
|
||||||
|
@ -191,9 +196,16 @@ export default {
|
||||||
isCurrentSortField(field) {
|
isCurrentSortField(field) {
|
||||||
return field === this.sortField;
|
return field === this.sortField;
|
||||||
},
|
},
|
||||||
|
isCurrentStatusFilter(filter) {
|
||||||
|
return filter === this.statusFilter;
|
||||||
|
},
|
||||||
getIssueUpdatePath(errorId) {
|
getIssueUpdatePath(errorId) {
|
||||||
return `/${this.projectPath}/-/error_tracking/${errorId}.json`;
|
return `/${this.projectPath}/-/error_tracking/${errorId}.json`;
|
||||||
},
|
},
|
||||||
|
filterErrors(status, label) {
|
||||||
|
this.filterValue = label;
|
||||||
|
return this.filterByStatus(status);
|
||||||
|
},
|
||||||
updateIssueStatus(errorId, status) {
|
updateIssueStatus(errorId, status) {
|
||||||
this.updateStatus({
|
this.updateStatus({
|
||||||
endpoint: this.getIssueUpdatePath(errorId),
|
endpoint: this.getIssueUpdatePath(errorId),
|
||||||
|
@ -260,11 +272,32 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gl-dropdown
|
<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]"
|
:text="$options.sortFields[sortField]"
|
||||||
left
|
left
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
menu-class="sort-dropdown"
|
menu-class="dropdown"
|
||||||
>
|
>
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="(label, field) in $options.sortFields"
|
v-for="(label, field) in $options.sortFields"
|
||||||
|
@ -272,7 +305,7 @@ export default {
|
||||||
@click="sortByField(field)"
|
@click="sortByField(field)"
|
||||||
>
|
>
|
||||||
<span class="d-flex">
|
<span class="d-flex">
|
||||||
<icon
|
<gl-icon
|
||||||
class="flex-shrink-0 append-right-4"
|
class="flex-shrink-0 append-right-4"
|
||||||
:class="{ invisible: !isCurrentSortField(field) }"
|
:class="{ invisible: !isCurrentSortField(field) }"
|
||||||
name="mobile-issue-close"
|
name="mobile-issue-close"
|
||||||
|
|
|
@ -18,6 +18,7 @@ export function startPolling({ state, commit, dispatch }) {
|
||||||
search_term: state.searchQuery,
|
search_term: state.searchQuery,
|
||||||
sort: state.sortField,
|
sort: state.sortField,
|
||||||
cursor: state.cursor,
|
cursor: state.cursor,
|
||||||
|
issue_status: state.statusFilter,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
successCallback: ({ data }) => {
|
successCallback: ({ data }) => {
|
||||||
|
@ -83,6 +84,12 @@ export const searchByQuery = ({ commit, dispatch }, query) => {
|
||||||
dispatch('startPolling');
|
dispatch('startPolling');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const filterByStatus = ({ commit, dispatch }, status) => {
|
||||||
|
commit(types.SET_STATUS_FILTER, status);
|
||||||
|
dispatch('stopPolling');
|
||||||
|
dispatch('startPolling');
|
||||||
|
};
|
||||||
|
|
||||||
export const sortByField = ({ commit, dispatch }, field) => {
|
export const sortByField = ({ commit, dispatch }, field) => {
|
||||||
commit(types.SET_CURSOR, null);
|
commit(types.SET_CURSOR, null);
|
||||||
commit(types.SET_SORT_FIELD, field);
|
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_SEARCH_QUERY = 'SET_SEARCH_QUERY';
|
||||||
export const SET_CURSOR = 'SET_CURSOR';
|
export const SET_CURSOR = 'SET_CURSOR';
|
||||||
export const REMOVE_IGNORED_RESOLVED_ERRORS = 'REMOVE_IGNORED_RESOLVED_ERRORS';
|
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) {
|
[types.REMOVE_IGNORED_RESOLVED_ERRORS](state, error) {
|
||||||
state.errors = state.errors.filter(err => err.id !== 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,
|
loading: true,
|
||||||
endpoint: null,
|
endpoint: null,
|
||||||
sortField: 'last_seen',
|
sortField: 'last_seen',
|
||||||
|
statusFilter: 'unresolved',
|
||||||
searchQuery: null,
|
searchQuery: null,
|
||||||
indexPath: '',
|
indexPath: '',
|
||||||
recentSearches: [],
|
recentSearches: [],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.error-list {
|
.error-list {
|
||||||
.sort-dropdown {
|
.dropdown {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix spinner in Create MR dropdown
|
||||||
|
merge_request: 26679
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
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
|
## Redis and PostgreSQL High Availability
|
||||||
|
|
||||||
The **primary** and **secondary** Redis and PostgreSQL should be configured
|
Geo supports:
|
||||||
for high availability. Because of the additional complexity involved
|
|
||||||
in setting up this configuration for PostgreSQL and Redis,
|
- Redis and PostgreSQL on the **primary** node configured for high availability
|
||||||
it is not covered by this Geo HA documentation.
|
- 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
|
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
|
[PostgreSQL](../../high_availability/database.md) and
|
||||||
|
@ -37,10 +44,17 @@ For more information about setting up a highly available PostgreSQL cluster and
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
It is possible to use cloud hosted services for PostgreSQL and Redis, but this is beyond the scope of this document.
|
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
|
||||||
|
|
||||||
This cluster will serve as the **primary** node. Use the
|
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**.
|
||||||
|
|
||||||
|
The second cluster will serve as the **secondary** node. Again, use the
|
||||||
[GitLab HA documentation](../../high_availability/README.md) to set this up.
|
[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
|
## Configure the GitLab cluster to be the **primary** node
|
||||||
|
|
||||||
|
@ -99,7 +113,11 @@ major differences:
|
||||||
various resources.
|
various resources.
|
||||||
|
|
||||||
Therefore, we will set up the HA components one-by-one, and include deviations
|
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
|
### 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
|
### 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
|
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
|
Configure the [**secondary** database](database.md) as a read-only replica of
|
||||||
the **primary** database. Use the following as a guide.
|
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.
|
## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
|
||||||
##
|
##
|
||||||
geo_postgresql['enable'] = false
|
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.
|
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
|
Verify these services by running `sudo gitlab-ctl status` on the frontend
|
||||||
application servers.
|
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
|
### Step 5: Set up the LoadBalancer for the **secondary** node
|
||||||
|
|
||||||
In this topology, a load balancer is required at each geographic location to
|
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.
|
- Include suggested titles of any pages or subsection headings, if applicable.
|
||||||
- List any documentation that should be cross-linked, 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
|
## For all other documentation
|
||||||
|
|
||||||
These documentation changes are not associated with the release of a new or updated feature, and are
|
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:**
|
NOTE: **Note:**
|
||||||
You will need at least Reporter [permissions](../../permissions.md) to view the Error Tracking list.
|
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.
|
You can find the Error Tracking list 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.
|
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)
|
![Error Tracking list](img/error_tracking_list_v12_6.png)
|
||||||
|
|
||||||
|
|
|
@ -10533,6 +10533,9 @@ msgstr ""
|
||||||
msgid "Ignore"
|
msgid "Ignore"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Ignored"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Image %{imageName} was scheduled for deletion from the registry."
|
msgid "Image %{imageName} was scheduled for deletion from the registry."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -21104,6 +21107,9 @@ msgstr ""
|
||||||
msgid "Unresolve thread"
|
msgid "Unresolve thread"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unresolved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "UnscannedProjects|15 or more days"
|
msgid "UnscannedProjects|15 or more days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -26,7 +26,7 @@ describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_
|
||||||
|
|
||||||
page.within(find('.gl-table')) do
|
page.within(find('.gl-table')) do
|
||||||
results = page.all('.table-row')
|
results = page.all('.table-row')
|
||||||
expect(results.count).to be(2)
|
expect(results.count).to be(3)
|
||||||
end
|
end
|
||||||
|
|
||||||
find('.gl-form-input').set('NotFound').native.send_keys(:return)
|
find('.gl-form-input').set('NotFound').native.send_keys(:return)
|
||||||
|
|
|
@ -82,5 +82,47 @@
|
||||||
"name": "Internal"
|
"name": "Internal"
|
||||||
},
|
},
|
||||||
"statusDetails": {}
|
"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": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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(/
/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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
import { createLocalVue, mount } from '@vue/test-utils';
|
import { createLocalVue, mount } from '@vue/test-utils';
|
||||||
import Vuex from 'vuex';
|
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 stubChildren from 'helpers/stub_children';
|
||||||
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
|
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
|
||||||
import errorsList from './list_mock.json';
|
import errorsList from './list_mock.json';
|
||||||
|
@ -15,9 +15,19 @@ describe('ErrorTrackingList', () => {
|
||||||
|
|
||||||
const findErrorListTable = () => wrapper.find('table');
|
const findErrorListTable = () => wrapper.find('table');
|
||||||
const findErrorListRows = () => wrapper.findAll('tbody tr');
|
const findErrorListRows = () => wrapper.findAll('tbody tr');
|
||||||
const findSortDropdown = () => wrapper.find('.sort-dropdown');
|
const dropdownsArray = () => wrapper.findAll(GlDropdown);
|
||||||
const findRecentSearchesDropdown = () =>
|
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 findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||||
const findPagination = () => wrapper.find(GlPagination);
|
const findPagination = () => wrapper.find(GlPagination);
|
||||||
|
|
||||||
|
@ -60,6 +70,7 @@ describe('ErrorTrackingList', () => {
|
||||||
fetchPaginatedResults: jest.fn(),
|
fetchPaginatedResults: jest.fn(),
|
||||||
updateStatus: jest.fn(),
|
updateStatus: jest.fn(),
|
||||||
removeIgnoredResolvedErrors: jest.fn(),
|
removeIgnoredResolvedErrors: jest.fn(),
|
||||||
|
filterByStatus: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -167,10 +178,16 @@ describe('ErrorTrackingList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it sorts by fields', () => {
|
it('it sorts by fields', () => {
|
||||||
const findSortItem = () => wrapper.find('.dropdown-item');
|
const findSortItem = () => findSortDropdown().find('.dropdown-item');
|
||||||
findSortItem().trigger('click');
|
findSortItem().trigger('click');
|
||||||
expect(actions.sortByField).toHaveBeenCalled();
|
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(wrapper.find(GlEmptyState).exists()).toBe(true);
|
||||||
expect(findLoadingIcon().exists()).toBe(false);
|
expect(findLoadingIcon().exists()).toBe(false);
|
||||||
expect(findErrorListTable().exists()).toBe(false);
|
expect(findErrorListTable().exists()).toBe(false);
|
||||||
expect(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', () => {
|
describe('sortByField', () => {
|
||||||
it('should search by query', () => {
|
it('should search by query', () => {
|
||||||
const field = 'frequency';
|
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 CLEAR_RECENT_SEARCHES = mutations[types.CLEAR_RECENT_SEARCHES];
|
||||||
const LOAD_RECENT_SEARCHES = mutations[types.LOAD_RECENT_SEARCHES];
|
const LOAD_RECENT_SEARCHES = mutations[types.LOAD_RECENT_SEARCHES];
|
||||||
const REMOVE_IGNORED_RESOLVED_ERRORS = mutations[types.REMOVE_IGNORED_RESOLVED_ERRORS];
|
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('Error tracking mutations', () => {
|
||||||
describe('SET_ERRORS', () => {
|
describe('SET_ERRORS', () => {
|
||||||
|
@ -139,5 +140,15 @@ describe('Error tracking mutations', () => {
|
||||||
expect(state.errors).not.toContain(ignoredError);
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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(/
/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 'calls sentry api'
|
||||||
|
|
||||||
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
|
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
|
shared_examples 'has correct external_url' do
|
||||||
context '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 'calls sentry api'
|
||||||
|
|
||||||
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
|
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
|
||||||
|
|
||||||
context 'when cursor is present' do
|
context 'when cursor is present' do
|
||||||
|
@ -194,7 +194,7 @@ describe Sentry::Client::Issue do
|
||||||
it_behaves_like 'calls sentry api'
|
it_behaves_like 'calls sentry api'
|
||||||
|
|
||||||
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue