Commit cd15d0e6 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent dd6afb4b
<script> <script>
import { GlLink } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '../../locale'; import { __ } from '../../locale';
import createFlash from '../../flash'; import createFlash from '../../flash';
import Api from '../../api'; import Api from '../../api';
import state from '../state'; import state from '../state';
...@@ -9,6 +9,7 @@ import Dropdown from './dropdown.vue'; ...@@ -9,6 +9,7 @@ import Dropdown from './dropdown.vue';
export default { export default {
components: { components: {
GlLink, GlLink,
GlSprintf,
Dropdown, Dropdown,
}, },
props: { props: {
...@@ -38,15 +39,6 @@ export default { ...@@ -38,15 +39,6 @@ export default {
selectedProject() { selectedProject() {
return state.selectedProject; return state.selectedProject;
}, },
noForkText() {
return sprintf(
__(
"To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private.",
),
{ link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
false,
);
},
}, },
mounted() { mounted() {
this.fetchProjects(); this.fetchProjects();
...@@ -123,8 +115,20 @@ export default { ...@@ -123,8 +115,20 @@ export default {
}} }}
</template> </template>
<template v-else> <template v-else>
{{ __('No forks available to you.') }}<br /> {{ __('No forks are available to you.') }}<br />
<span v-html="noForkText"></span> <gl-sprintf
:message="
__(
`To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private.`,
)
"
>
<template #forkLink>
<a :href="newForkPath" target="_blank" class="help-link">{{
__('fork this project')
}}</a>
</template>
</gl-sprintf>
</template> </template>
<gl-link <gl-link
:href="helpPagePath" :href="helpPagePath"
......
...@@ -288,7 +288,7 @@ ...@@ -288,7 +288,7 @@
list-style: none; list-style: none;
padding: 0 1px; padding: 0 1px;
a, a:not(.help-link),
button, button,
.menu-item { .menu-item {
@include dropdown-link; @include dropdown-link;
......
...@@ -214,7 +214,6 @@ ul.related-merge-requests > li { ...@@ -214,7 +214,6 @@ ul.related-merge-requests > li {
} }
.create-merge-request-dropdown-menu { .create-merge-request-dropdown-menu {
width: 300px;
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
......
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryDetailedErrorResolver < BaseResolver
argument :id, GraphQL::ID_TYPE,
required: true,
description: 'ID of the Sentry issue'
def resolve(**args)
project = object
current_user = context[:current_user]
issue_id = GlobalID.parse(args[:id]).model_id
# Get data from Sentry
response = ::ErrorTracking::IssueDetailsService.new(
project,
current_user,
{ issue_id: issue_id }
).execute
issue = response[:issue]
issue.gitlab_project = project if issue
issue
end
end
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
class SentryDetailedErrorType < ::Types::BaseObject
graphql_name 'SentryDetailedError'
present_using SentryDetailedErrorPresenter
authorize :read_sentry_issue
field :id, GraphQL::ID_TYPE,
null: false,
description: "ID (global ID) of the error"
field :sentry_id, GraphQL::STRING_TYPE,
method: :id,
null: false,
description: "ID (Sentry ID) of the error"
field :title, GraphQL::STRING_TYPE,
null: false,
description: "Title of the error"
field :type, GraphQL::STRING_TYPE,
null: false,
description: "Type of the error"
field :user_count, GraphQL::INT_TYPE,
null: false,
description: "Count of users affected by the error"
field :count, GraphQL::INT_TYPE,
null: false,
description: "Count of occurrences"
field :first_seen, Types::TimeType,
null: false,
description: "Timestamp when the error was first seen"
field :last_seen, Types::TimeType,
null: false,
description: "Timestamp when the error was last seen"
field :message, GraphQL::STRING_TYPE,
null: true,
description: "Sentry metadata message of the error"
field :culprit, GraphQL::STRING_TYPE,
null: false,
description: "Culprit of the error"
field :external_url, GraphQL::STRING_TYPE,
null: false,
description: "External URL of the error"
field :sentry_project_id, GraphQL::ID_TYPE,
method: :project_id,
null: false,
description: "ID of the project (Sentry project)"
field :sentry_project_name, GraphQL::STRING_TYPE,
method: :project_name,
null: false,
description: "Name of the project affected by the error"
field :sentry_project_slug, GraphQL::STRING_TYPE,
method: :project_slug,
null: false,
description: "Slug of the project affected by the error"
field :short_id, GraphQL::STRING_TYPE,
null: false,
description: "Short ID (Sentry ID) of the error"
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
null: false,
description: "Status of the error"
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
null: false,
description: "Last 24hr stats of the error"
field :first_release_last_commit, GraphQL::STRING_TYPE,
null: true,
description: "Commit the error was first seen"
field :last_release_last_commit, GraphQL::STRING_TYPE,
null: true,
description: "Commit the error was last seen"
field :first_release_short_version, GraphQL::STRING_TYPE,
null: true,
description: "Release version the error was first seen"
field :last_release_short_version, GraphQL::STRING_TYPE,
null: true,
description: "Release version the error was last seen"
def first_seen
DateTime.parse(object.first_seen)
end
def last_seen
DateTime.parse(object.last_seen)
end
def project_id
Gitlab::GlobalId.build(model_name: 'Project', id: object.project_id).to_s
end
end
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorFrequencyType < ::Types::BaseObject
graphql_name 'SentryErrorFrequency'
field :time, Types::TimeType,
null: false,
description: "Time the error frequency stats were recorded"
field :count, GraphQL::INT_TYPE,
null: false,
description: "Count of errors received since the previously recorded time"
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
class SentryErrorStatusEnum < ::Types::BaseEnum
graphql_name 'SentryErrorStatus'
description 'State of a Sentry error'
value 'RESOLVED', value: 'resolved', description: 'Error has been resolved'
value 'RESOLVED_IN_NEXT_RELEASE', value: 'resolvedInNextRelease', description: 'Error has been ignored until next release'
value 'UNRESOLVED', value: 'unresolved', description: 'Error is unresolved'
value 'IGNORED', value: 'ignored', description: 'Error has been ignored'
end
end
end
...@@ -145,5 +145,11 @@ module Types ...@@ -145,5 +145,11 @@ module Types
null: true, null: true,
description: 'Build pipelines of the project', description: 'Build pipelines of the project',
resolver: Resolvers::ProjectPipelinesResolver resolver: Resolvers::ProjectPipelinesResolver
field :sentry_detailed_error,
Types::ErrorTracking::SentryDetailedErrorType,
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
end end
end end
...@@ -128,7 +128,7 @@ module Issuable ...@@ -128,7 +128,7 @@ module Issuable
end end
scope :joins_milestone_releases, -> do scope :joins_milestone_releases, -> do
joins("JOIN milestone_releases ON issues.milestone_id = milestone_releases.milestone_id joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
JOIN releases ON milestone_releases.release_id = releases.id").distinct JOIN releases ON milestone_releases.release_id = releases.id").distinct
end end
......
# frozen_string_literal: true
module ErrorTracking
class DetailedErrorPolicy < BasePolicy
delegate { @subject.gitlab_project }
end
end
# frozen_string_literal: true
class SentryDetailedErrorPresenter < Gitlab::View::Presenter::Delegated
presents :error
FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
def frequency
utc_offset = Time.zone_offset('UTC')
error.frequency.map do |f|
FrequencyStruct.new(time: Time.at(f[0], in: utc_offset), count: f[1])
end
end
end
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } } %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } }
- if can_create_merge_request - if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } } %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
.menu-item .menu-item.text-nowrap
= icon('check', class: 'icon') = icon('check', class: 'icon')
- if can_create_confidential_merge_request? - if can_create_confidential_merge_request?
= _('Create confidential merge request and branch') = _('Create confidential merge request and branch')
......
---
title: GraphQL for Sentry rror details
merge_request: 19733
author:
type: added
---
title: Fixed query behind release filter on merge request search page.
merge_request: 38244
author:
type: fixed
---
title: Improve create confidential MR dropdown styling.
merge_request: 20176
author: Lee Tickett
type: other
...@@ -4400,6 +4400,16 @@ type Project { ...@@ -4400,6 +4400,16 @@ type Project {
""" """
requestAccessEnabled: Boolean requestAccessEnabled: Boolean
"""
Detailed version of a Sentry error on the project
"""
sentryDetailedError(
"""
ID of the Sentry issue
"""
id: ID!
): SentryDetailedError
""" """
Indicates if shared runners are enabled on the project Indicates if shared runners are enabled on the project
""" """
...@@ -4886,6 +4896,150 @@ type RootStorageStatistics { ...@@ -4886,6 +4896,150 @@ type RootStorageStatistics {
wikiSize: Int! wikiSize: Int!
} }
type SentryDetailedError {
"""
Count of occurrences
"""
count: Int!
"""
Culprit of the error
"""
culprit: String!
"""
External URL of the error
"""
externalUrl: String!
"""
Commit the error was first seen
"""
firstReleaseLastCommit: String
"""
Release version the error was first seen
"""
firstReleaseShortVersion: String
"""
Timestamp when the error was first seen
"""
firstSeen: Time!
"""
Last 24hr stats of the error
"""
frequency: [SentryErrorFrequency!]!
"""
ID (global ID) of the error
"""
id: ID!
"""
Commit the error was last seen
"""
lastReleaseLastCommit: String
"""
Release version the error was last seen
"""
lastReleaseShortVersion: String
"""
Timestamp when the error was last seen
"""
lastSeen: Time!
"""
Sentry metadata message of the error
"""
message: String
"""
ID (Sentry ID) of the error
"""
sentryId: String!
"""
ID of the project (Sentry project)
"""
sentryProjectId: ID!
"""
Name of the project affected by the error
"""
sentryProjectName: String!
"""
Slug of the project affected by the error
"""
sentryProjectSlug: String!
"""
Short ID (Sentry ID) of the error
"""
shortId: String!
"""
Status of the error
"""
status: SentryErrorStatus!
"""
Title of the error
"""
title: String!
"""
Type of the error
"""
type: String!
"""
Count of users affected by the error
"""
userCount: Int!
}
type SentryErrorFrequency {
"""
Count of errors received since the previously recorded time
"""
count: Int!
"""
Time the error frequency stats were recorded
"""
time: Time!
}
"""
State of a Sentry error
"""
enum SentryErrorStatus {
"""
Error has been ignored
"""
IGNORED
"""
Error has been resolved
"""
RESOLVED
"""
Error has been ignored until next release
"""
RESOLVED_IN_NEXT_RELEASE
"""
Error is unresolved
"""
UNRESOLVED
}
type Submodule implements Entry { type Submodule implements Entry {
flatPath: String! flatPath: String!
id: ID! id: ID!
......
...@@ -1166,6 +1166,33 @@ ...@@ -1166,6 +1166,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "sentryDetailedError",
"description": "Detailed version of a Sentry error on the project",
"args": [
{
"name": "id",
"description": "ID of the Sentry issue",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "SentryDetailedError",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "sharedRunnersEnabled", "name": "sharedRunnersEnabled",
"description": "Indicates if shared runners are enabled on the project", "description": "Indicates if shared runners are enabled on the project",
...@@ -13788,6 +13815,469 @@ ...@@ -13788,6 +13815,469 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "SentryDetailedError",
"description": null,
"fields": [
{
"name": "count",
"description": "Count of occurrences",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "culprit",
"description": "Culprit of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "externalUrl",
"description": "External URL of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstReleaseLastCommit",
"description": "Commit the error was first seen",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstReleaseShortVersion",
"description": "Release version the error was first seen",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstSeen",
"description": "Timestamp when the error was first seen",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "frequency",
"description": "Last 24hr stats of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SentryErrorFrequency",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID (global ID) of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastReleaseLastCommit",
"description": "Commit the error was last seen",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastReleaseShortVersion",
"description": "Release version the error was last seen",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastSeen",
"description": "Timestamp when the error was last seen",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "Sentry metadata message of the error",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryId",
"description": "ID (Sentry ID) of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryProjectId",
"description": "ID of the project (Sentry project)",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryProjectName",
"description": "Name of the project affected by the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryProjectSlug",
"description": "Slug of the project affected by the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "shortId",
"description": "Short ID (Sentry ID) of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": "Status of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SentryErrorStatus",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "title",
"description": "Title of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userCount",
"description": "Count of users affected by the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "SentryErrorStatus",
"description": "State of a Sentry error",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "RESOLVED",
"description": "Error has been resolved",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "RESOLVED_IN_NEXT_RELEASE",
"description": "Error has been ignored until next release",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNRESOLVED",
"description": "Error is unresolved",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "IGNORED",
"description": "Error has been ignored",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorFrequency",
"description": null,
"fields": [
{
"name": "count",
"description": "Count of errors received since the previously recorded time",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "time",
"description": "Time the error frequency stats were recorded",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Metadata", "name": "Metadata",
......
...@@ -664,6 +664,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -664,6 +664,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `repository` | Repository | Git repository of the project | | `repository` | Repository | Git repository of the project |
| `mergeRequest` | MergeRequest | A single merge request of the project | | `mergeRequest` | MergeRequest | A single merge request of the project |
| `issue` | Issue | A single issue of the project | | `issue` | Issue | A single issue of the project |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
### ProjectPermissions ### ProjectPermissions
...@@ -751,6 +752,39 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -751,6 +752,39 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `packagesSize` | Int! | The packages size in bytes | | `packagesSize` | Int! | The packages size in bytes |
| `wikiSize` | Int! | The wiki size in bytes | | `wikiSize` | Int! | The wiki size in bytes |
### SentryDetailedError
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID (global ID) of the error |
| `sentryId` | String! | ID (Sentry ID) of the error |
| `title` | String! | Title of the error |
| `type` | String! | Type of the error |
| `userCount` | Int! | Count of users affected by the error |
| `count` | Int! | Count of occurrences |
| `firstSeen` | Time! | Timestamp when the error was first seen |
| `lastSeen` | Time! | Timestamp when the error was last seen |
| `message` | String | Sentry metadata message of the error |
| `culprit` | String! | Culprit of the error |
| `externalUrl` | String! | External URL of the error |
| `sentryProjectId` | ID! | ID of the project (Sentry project) |
| `sentryProjectName` | String! | Name of the project affected by the error |
| `sentryProjectSlug` | String! | Slug of the project affected by the error |
| `shortId` | String! | Short ID (Sentry ID) of the error |
| `status` | SentryErrorStatus! | Status of the error |
| `frequency` | SentryErrorFrequency! => Array | Last 24hr stats of the error |
| `firstReleaseLastCommit` | String | Commit the error was first seen |
| `lastReleaseLastCommit` | String | Commit the error was last seen |
| `firstReleaseShortVersion` | String | Release version the error was first seen |
| `lastReleaseShortVersion` | String | Release version the error was last seen |
### SentryErrorFrequency
| Name | Type | Description |
| --- | ---- | ---------- |
| `time` | Time! | Time the error frequency stats were recorded |
| `count` | Int! | Count of errors received since the previously recorded time |
### Submodule ### Submodule
| Name | Type | Description | | Name | Type | Description |
......
...@@ -203,7 +203,7 @@ job: ...@@ -203,7 +203,7 @@ job:
You can use [YAML anchors](#anchors) with scripts, which makes it possible to You can use [YAML anchors](#anchors) with scripts, which makes it possible to
include a predefined list of commands in multiple jobs. include a predefined list of commands in multiple jobs.
Example: For example:
```yaml ```yaml
.something: &something .something: &something
...@@ -1413,6 +1413,11 @@ Also in the example, `GIT_STRATEGY` is set to `none` so that GitLab Runner won ...@@ -1413,6 +1413,11 @@ Also in the example, `GIT_STRATEGY` is set to `none` so that GitLab Runner won
try to check out the code after the branch is deleted when the `stop_review_app` try to check out the code after the branch is deleted when the `stop_review_app`
job is [automatically triggered](../environments.md#automatically-stopping-an-environment). job is [automatically triggered](../environments.md#automatically-stopping-an-environment).
NOTE: **Note:**
The above example overwrites global variables. If your stop environment job depends
on global variables, you can use [anchor variables](#yaml-anchors-for-variables) when setting the `GIT_STRATEGY`
to change it without overriding the global variables.
The `stop_review_app` job is **required** to have the following keywords defined: The `stop_review_app` job is **required** to have the following keywords defined:
- `when` - [reference](#when) - `when` - [reference](#when)
...@@ -3159,6 +3164,29 @@ which can be set in GitLab's UI. ...@@ -3159,6 +3164,29 @@ which can be set in GitLab's UI.
Learn more about [variables and their priority][variables]. Learn more about [variables and their priority][variables].
#### YAML anchors for variables
[YAML anchors](#anchors) can be used with `variables`, to easily repeat assignment
of variables across multiple jobs. It can also enable more flexibility when a job
requires a specific `variables` block that would otherwise override the global variables.
In the example below, we will override the `GIT_STRATEGY` variable without affecting
the use of the `SAMPLE_VARIABLE` variable:
```yaml
# global variables
variables: &global-variables
SAMPLE_VARIABLE: sample_variable_value
# a job that needs to set the GIT_STRATEGY variable, yet depend on global variables
job_no_git_strategy:
stage: cleanup
variables:
<<: *global-variables
GIT_STRATEGY: none
script: echo $SAMPLE_VARIABLE
```
#### Git strategy #### Git strategy
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed > Introduced in GitLab 8.9 as an experimental feature. May change or be removed
......
...@@ -4,6 +4,7 @@ module Gitlab ...@@ -4,6 +4,7 @@ module Gitlab
module ErrorTracking module ErrorTracking
class DetailedError class DetailedError
include ActiveModel::Model include ActiveModel::Model
include GlobalID::Identification
attr_accessor :count, attr_accessor :count,
:culprit, :culprit,
...@@ -13,6 +14,7 @@ module Gitlab ...@@ -13,6 +14,7 @@ module Gitlab
:first_release_short_version, :first_release_short_version,
:first_seen, :first_seen,
:frequency, :frequency,
:gitlab_project,
:id, :id,
:last_release_last_commit, :last_release_last_commit,
:last_release_short_version, :last_release_short_version,
...@@ -26,6 +28,10 @@ module Gitlab ...@@ -26,6 +28,10 @@ module Gitlab
:title, :title,
:type, :type,
:user_count :user_count
def self.declarative_policy_class
'ErrorTracking::DetailedErrorPolicy'
end
end end
end end
end end
...@@ -11641,7 +11641,7 @@ msgstr "" ...@@ -11641,7 +11641,7 @@ msgstr ""
msgid "No files found." msgid "No files found."
msgstr "" msgstr ""
msgid "No forks available to you." msgid "No forks are available to you."
msgstr "" msgstr ""
msgid "No issues for the selected time period." msgid "No issues for the selected time period."
...@@ -18447,7 +18447,7 @@ msgstr "" ...@@ -18447,7 +18447,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed." msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr "" msgstr ""
msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private." msgid "To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
msgstr "" msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected." msgid "To protect this issue's confidentiality, a private fork of this project was selected."
...@@ -21049,6 +21049,9 @@ msgstr "" ...@@ -21049,6 +21049,9 @@ msgstr ""
msgid "for this project" msgid "for this project"
msgstr "" msgstr ""
msgid "fork this project"
msgstr ""
msgid "from" msgid "from"
msgstr "" msgstr ""
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
FactoryBot.define do FactoryBot.define do
factory :detailed_error_tracking_error, class: Gitlab::ErrorTracking::DetailedError do factory :detailed_error_tracking_error, class: Gitlab::ErrorTracking::DetailedError do
id { 'id' } id { '1' }
title { 'title' } title { 'title' }
type { 'error' } type { 'error' }
user_count { 1 } user_count { 1 }
count { 2 } count { 2 }
first_seen { Time.now } first_seen { Time.now.iso8601 }
last_seen { Time.now } last_seen { Time.now.iso8601 }
message { 'message' } message { 'message' }
culprit { 'culprit' } culprit { 'culprit' }
external_url { 'http://example.com/id' } external_url { 'http://example.com/id' }
...@@ -18,7 +18,11 @@ FactoryBot.define do ...@@ -18,7 +18,11 @@ FactoryBot.define do
project_slug { 'project_name' } project_slug { 'project_name' }
short_id { 'ID' } short_id { 'ID' }
status { 'unresolved' } status { 'unresolved' }
frequency { [] } frequency do
[
[Time.now.to_i, 10]
]
end
first_release_last_commit { '68c914da9' } first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' } last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' } first_release_short_version { 'abc123' }
......
...@@ -29,7 +29,7 @@ describe 'User creates confidential merge request on issue page', :js do ...@@ -29,7 +29,7 @@ describe 'User creates confidential merge request on issue page', :js do
click_button 'Create confidential merge request' click_button 'Create confidential merge request'
page.within '.create-confidential-merge-request-dropdown-menu' do page.within '.create-confidential-merge-request-dropdown-menu' do
expect(page).to have_content('No forks available to you') expect(page).to have_content('No forks are available to you')
end end
end end
end end
......
...@@ -15,19 +15,12 @@ exports[`Confidential merge request project form group component renders empty s ...@@ -15,19 +15,12 @@ exports[`Confidential merge request project form group component renders empty s
class="text-muted mt-1 mb-0" class="text-muted mt-1 mb-0"
> >
No forks available to you. No forks are available to you.
<br /> <br />
<span> <glsprintf-stub
To protect this issue's confidentiality, message="To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
<a />
class="help-link"
href="https://test.com"
>
fork the project
</a>
and set the forks visibility to private.
</span>
<gllink-stub <gllink-stub
class="w-auto p-0 d-inline-block text-primary bg-transparent" class="w-auto p-0 d-inline-block text-primary bg-transparent"
...@@ -65,19 +58,12 @@ exports[`Confidential merge request project form group component renders fork dr ...@@ -65,19 +58,12 @@ exports[`Confidential merge request project form group component renders fork dr
class="text-muted mt-1 mb-0" class="text-muted mt-1 mb-0"
> >
No forks available to you. No forks are available to you.
<br /> <br />
<span> <glsprintf-stub
To protect this issue's confidentiality, message="To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
<a />
class="help-link"
href="https://test.com"
>
fork the project
</a>
and set the forks visibility to private.
</span>
<gllink-stub <gllink-stub
class="w-auto p-0 d-inline-block text-primary bg-transparent" class="w-auto p-0 d-inline-block text-primary bg-transparent"
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let(:issue_details_service) { spy('ErrorTracking::IssueDetailsService') }
before do
project.add_developer(current_user)
allow(ErrorTracking::IssueDetailsService)
.to receive(:new)
.and_return issue_details_service
end
describe '#resolve' do
let(:args) { { id: issue_global_id(1234) } }
it 'fetches the data via the sentry API' do
resolve_error(args)
expect(issue_details_service).to have_received(:execute)
end
context 'error matched' do
let(:detailed_error) { build(:detailed_error_tracking_error) }
before do
allow(issue_details_service).to receive(:execute)
.and_return({ issue: detailed_error })
end
it 'resolves to a detailed error' do
expect(resolve_error(args)).to eq detailed_error
end
it 'assigns the gitlab project' do
expect(resolve_error(args).gitlab_project).to eq project
end
end
it 'resolves to nil if no match' do
allow(issue_details_service).to receive(:execute)
.and_return({ issue: nil })
result = resolve_error(args)
expect(result).to eq nil
end
end
def resolve_error(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
def issue_global_id(issue_id)
Gitlab::ErrorTracking::DetailedError.new(id: issue_id).to_global_id.to_s
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryDetailedError'] do
it { expect(described_class.graphql_name).to eq('SentryDetailedError') }
it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
id
sentryId
title
type
userCount
count
firstSeen
lastSeen
message
culprit
externalUrl
sentryProjectId
sentryProjectName
sentryProjectSlug
shortId
status
frequency
firstReleaseLastCommit
lastReleaseLastCommit
firstReleaseShortVersion
lastReleaseShortVersion
]
is_expected.to have_graphql_fields(*expected_fields)
end
end
...@@ -22,8 +22,7 @@ describe GitlabSchema.types['Project'] do ...@@ -22,8 +22,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_pipeline_succeeds request_access_enabled only_allow_merge_if_pipeline_succeeds request_access_enabled
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues namespace group statistics repository merge_requests merge_request issues
issue pipelines issue pipelines removeSourceBranchAfterMerge sentryDetailedError
removeSourceBranchAfterMerge
] ]
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
......
import Vue from 'vue'; import Vue from 'vue';
import BarChart from '~/vue_shared/components/bar_chart.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import BarChart from '~/vue_shared/components/bar_chart.vue';
function getRandomArbitrary(min, max) { function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
......
import Vue from 'vue'; import Vue from 'vue';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
describe('CI Badge Link Component', () => { describe('CI Badge Link Component', () => {
let CIBadge; let CIBadge;
......
import Vue from 'vue'; import Vue from 'vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => { describe('CI Icon component', () => {
const Component = Vue.extend(ciIcon); const Component = Vue.extend(ciIcon);
......
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants'; import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
describe('ContentViewer', () => { describe('ContentViewer', () => {
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
const modalComponent = Vue.extend(DeprecatedModal2); const modalComponent = Vue.extend(DeprecatedModal2);
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
const modalComponent = Vue.extend(DeprecatedModal); const modalComponent = Vue.extend(DeprecatedModal);
......
import Vue from 'vue'; import Vue from 'vue';
import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
describe('DiffViewer', () => { describe('DiffViewer', () => {
const requiredProps = { const requiredProps = {
......
import Vue from 'vue'; import Vue from 'vue';
import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
describe('ImageDiffViewer', () => { describe('ImageDiffViewer', () => {
const requiredProps = { const requiredProps = {
......
import Vue from 'vue'; import Vue from 'vue';
import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
const defaultLabel = 'Select'; const defaultLabel = 'Select';
const customLabel = 'Select project'; const customLabel = 'Select project';
......
import Vue from 'vue'; import Vue from 'vue';
import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { mockLabels } from './mock_data'; import { mockLabels } from './mock_data';
......
import Vue from 'vue'; import Vue from 'vue';
import dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
const componentConfig = { const componentConfig = {
placeholderText: 'Search something', placeholderText: 'Search something',
......
import Vue from 'vue'; import Vue from 'vue';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import { file } from 'spec/ide/helpers'; import { file } from 'spec/ide/helpers';
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
describe('File finder item spec', () => { describe('File finder item spec', () => {
const Component = Vue.extend(FindFileComponent); const Component = Vue.extend(FindFileComponent);
......
import Vue from 'vue'; import Vue from 'vue';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
import { file } from 'spec/ide/helpers'; import { file } from 'spec/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
import createComponent from '../../../helpers/vue_mount_component_helper'; import createComponent from '../../../helpers/vue_mount_component_helper';
describe('File finder item spec', () => { describe('File finder item spec', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import { file } from 'spec/ide/helpers';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowExtra from '~/ide/components/file_row_extra.vue'; import FileRowExtra from '~/ide/components/file_row_extra.vue';
import { file } from 'spec/ide/helpers';
import mountComponent from '../../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('File row component', () => { describe('File row component', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/vue_shared/components/filtered_search_dropdown.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import component from '~/vue_shared/components/filtered_search_dropdown.vue';
describe('Filtered search dropdown', () => { describe('Filtered search dropdown', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
describe('Header CI Component', () => { describe('Header CI Component', () => {
let HeaderCi; let HeaderCi;
......
import Vue from 'vue'; import Vue from 'vue';
import Icon from '~/vue_shared/components/icon.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Icon from '~/vue_shared/components/icon.vue';
describe('Sprite Icon Component', function() { describe('Sprite Icon Component', function() {
describe('Initialization', function() { describe('Initialization', function() {
......
import Vue from 'vue'; import Vue from 'vue';
import loadingButton from '~/vue_shared/components/loading_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import loadingButton from '~/vue_shared/components/loading_button.vue';
const LABEL = 'Hello'; const LABEL = 'Hello';
......
import Vue from 'vue'; import Vue from 'vue';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
describe('toolbar', () => { describe('toolbar', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
describe('navigation tabs component', () => { describe('navigation tabs component', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
describe('Panel Resizer component', () => { describe('Panel Resizer component', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import datePicker from '~/vue_shared/components/pikaday.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import datePicker from '~/vue_shared/components/pikaday.vue';
describe('datePicker', () => { describe('datePicker', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { projectData } from 'spec/ide/mock_data'; import { projectData } from 'spec/ide/mock_data';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
describe('ProjectAvatarDefault component', () => { describe('ProjectAvatarDefault component', () => {
const Component = Vue.extend(ProjectAvatarDefault); const Component = Vue.extend(ProjectAvatarDefault);
......
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper'; import { trimText } from 'spec/helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
......
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore'; import _ from 'underscore';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper'; import { trimText } from 'spec/helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
......
import Vue from 'vue'; import Vue from 'vue';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => { describe('Toggle Button', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
const createComponent = config => { const createComponent = config => {
const Component = Vue.extend(stackedProgressBarComponent); const Component = Vue.extend(stackedProgressBarComponent);
......
import Vue from 'vue'; import Vue from 'vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
describe('Toggle Button', () => { describe('Toggle Button', () => {
let vm; let vm;
......
import Vue from 'vue'; import Vue from 'vue';
import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
import avatarSvg from 'icons/_icon_random.svg'; import avatarSvg from 'icons/_icon_random.svg';
import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg); const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg);
......
import Vue from 'vue'; import Vue from 'vue';
import Jed from 'jed'; import Jed from 'jed';
import { trimText } from 'spec/helpers/text_helper';
import locale from '~/locale'; import locale from '~/locale';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { trimText } from 'spec/helpers/text_helper';
describe('Vue translate filter', () => { describe('Vue translate filter', () => {
let el; let el;
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Issuable do describe Issuable do
include ProjectForksHelper
let(:issuable_class) { Issue } let(:issuable_class) { Issue }
let(:issue) { create(:issue, title: 'An issue', description: 'A description') } let(:issue) { create(:issue, title: 'An issue', description: 'A description') }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -855,6 +857,7 @@ describe Issuable do ...@@ -855,6 +857,7 @@ describe Issuable do
describe 'release scopes' do describe 'release scopes' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:forked_project) { fork_project(project) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) } let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) } let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
...@@ -875,52 +878,65 @@ describe Issuable do ...@@ -875,52 +878,65 @@ describe Issuable do
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) } let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
let_it_be(:issue_6) { create(:issue, project: project) } let_it_be(:issue_6) { create(:issue, project: project) }
let_it_be(:items) { Issue.all } let(:mr_1) { create(:merge_request, milestone: milestone_1, target_project: project, source_project: project) }
let(:mr_2) { create(:merge_request, milestone: milestone_3, target_project: project, source_project: forked_project) }
let(:mr_3) { create(:merge_request, source_project: project) }
let_it_be(:issue_items) { Issue.all }
let(:mr_items) { MergeRequest.all }
describe '#without_release' do describe '#without_release' do
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
expect(items.without_release).to contain_exactly(issue_5, issue_6) expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
expect(mr_items.without_release).to contain_exactly(mr_3)
end end
end end
describe '#any_release' do describe '#any_release' do
it 'returns all issues tied to a release' do it 'returns all issues or all mrs tied to a release' do
expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4) expect(issue_items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
expect(mr_items.any_release).to contain_exactly(mr_1, mr_2)
end end
end end
describe '#with_release' do describe '#with_release' do
it 'returns the issues tied a specfic release' do it 'returns the issues tied to a specfic release' do
expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3) expect(issue_items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
end
it 'returns the mrs tied to a specific release' do
expect(mr_items.with_release('v1.0', project.id)).to contain_exactly(mr_1)
end end
context 'when a release has a milestone with one issue and another one with no issue' do context 'when a release has a milestone with one issue and another one with no issue' do
it 'returns that one issue' do it 'returns that one issue' do
expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3) expect(issue_items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
end end
context 'when the milestone with no issue is added as a filter' do context 'when the milestone with no issue is added as a filter' do
it 'returns an empty list' do it 'returns an empty list' do
expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty expect(issue_items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
end end
end end
context 'when the milestone with the issue is added as a filter' do context 'when the milestone with the issue is added as a filter' do
it 'returns this issue' do it 'returns this issue' do
expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3) expect(issue_items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
end end
end end
end end
context 'when there is no issue under a specific release' do context 'when there is no issue or mr under a specific release' do
it 'returns no issue' do it 'returns no issue or no mr' do
expect(items.with_release('v4.0', project.id)).to be_empty expect(issue_items.with_release('v4.0', project.id)).to be_empty
expect(mr_items.with_release('v4.0', project.id)).to be_empty
end end
end end
context 'when a non-existent release tag is passed in' do context 'when a non-existent release tag is passed in' do
it 'returns no issue' do it 'returns no issue or no mr' do
expect(items.with_release('v999.0', project.id)).to be_empty expect(issue_items.with_release('v999.0', project.id)).to be_empty
expect(mr_items.with_release('v999.0', project.id)).to be_empty
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe SentryDetailedErrorPresenter do
let(:error) { build(:detailed_error_tracking_error) }
let(:presenter) { described_class.new(error) }
describe '#frequency' do
subject { presenter.frequency }
it 'returns an array of frequency structs' do
expect(subject).to include(a_kind_of(SentryDetailedErrorPresenter::FrequencyStruct))
end
it 'converts the times into UTC time objects' do
time = subject.first.time
expect(time).to be_a(Time)
expect(time.strftime('%z')).to eq '+0000'
end
it 'returns the correct counts' do
count = subject.first.count
expect(count).to eq error.frequency.first[1]
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'getting a detailed sentry error' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
let_it_be(:current_user) { project.owner }
let_it_be(:sentry_detailed_error) { build(:detailed_error_tracking_error) }
let(:sentry_gid) { sentry_detailed_error.to_global_id.to_s }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('SentryDetailedError'.classify)}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('sentryDetailedError', { id: sentry_gid }, fields)
)
end
let(:error_data) { graphql_data['project']['sentryDetailedError'] }
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when data is loading via reactive cache' do
before do
post_graphql(query, current_user: current_user)
end
it "is expected to return an empty error" do
expect(error_data).to eq nil
end
end
context 'reactive cache returns data' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.and_return({ issue: sentry_detailed_error })
post_graphql(query, current_user: current_user)
end
it "is expected to return a valid error" do
expect(error_data['id']).to eql sentry_gid
expect(error_data['sentryId']).to eql sentry_detailed_error.id.to_s
expect(error_data['status']).to eql sentry_detailed_error.status.upcase
expect(error_data['firstSeen']).to eql sentry_detailed_error.first_seen
expect(error_data['lastSeen']).to eql sentry_detailed_error.last_seen
end
it 'is expected to return the frequency correctly' do
expect(error_data['frequency'].count).to eql sentry_detailed_error.frequency.count
first_frequency = error_data['frequency'].first
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_detailed_error.frequency[0][0], in: 0)
expect(first_frequency['count']).to eql sentry_detailed_error.frequency[0][1]
end
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