summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-06 09:06:39 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-06 09:06:39 +0000
commitcd15d0e6c32da7f69689c7cff2e90aeda33b8318 (patch)
tree8343899f0873ab05f3eadca72c5f6e0a653a12ac
parentdd6afb4b4785ed1889defc6d7bb8ef114dd4eb50 (diff)
downloadgitlab-ce-cd15d0e6c32da7f69689c7cff2e90aeda33b8318.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue30
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/pages/issues.scss1
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb28
-rw-r--r--app/graphql/types/error_tracking/sentry_detailed_error_type.rb93
-rw-r--r--app/graphql/types/error_tracking/sentry_error_frequency_type.rb18
-rw-r--r--app/graphql/types/error_tracking/sentry_error_status_enum.rb15
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/policies/error_tracking/detailed_error_policy.rb7
-rw-r--r--app/presenters/sentry_detailed_error_presenter.rb15
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--changelogs/unreleased/34943-graphql-sentry-details.yml5
-rw-r--r--changelogs/unreleased/38244-fix-release-filter-on-mr-page.yml5
-rw-r--r--changelogs/unreleased/confidential_mr_styling.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql154
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json490
-rw-r--r--doc/api/graphql/reference/index.md34
-rw-r--r--doc/ci/yaml/README.md30
-rw-r--r--lib/gitlab/error_tracking/detailed_error.rb6
-rw-r--r--locale/gitlab.pot7
-rw-r--r--spec/factories/error_tracking/detailed_error.rb12
-rw-r--r--spec/features/issues/user_creates_confidential_merge_request_spec.rb2
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap30
-rw-r--r--spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb62
-rw-r--r--spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb37
-rw-r--r--spec/graphql/types/project_type_spec.rb3
-rw-r--r--spec/javascripts/vue_shared/components/bar_chart_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/ci_icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/deprecated_modal_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js3
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js3
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js3
-rw-r--r--spec/javascripts/vue_shared/components/file_finder/index_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/file_finder/item_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/loading_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/navigation_tabs_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/panel_resizer_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/pikaday_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/project_avatar/default_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/smart_virtual_list_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js3
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js2
-rw-r--r--spec/javascripts/vue_shared/translate_spec.js2
-rw-r--r--spec/models/concerns/issuable_spec.rb46
-rw-r--r--spec/presenters/sentry_detailed_error_presenter_spec.rb29
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb69
60 files changed, 1216 insertions, 103 deletions
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 4fa18b19556..f2853564f94 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import { __, sprintf } from '../../locale';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { __ } from '../../locale';
import createFlash from '../../flash';
import Api from '../../api';
import state from '../state';
@@ -9,6 +9,7 @@ import Dropdown from './dropdown.vue';
export default {
components: {
GlLink,
+ GlSprintf,
Dropdown,
},
props: {
@@ -38,15 +39,6 @@ export default {
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() {
this.fetchProjects();
@@ -123,8 +115,20 @@ export default {
}}
</template>
<template v-else>
- {{ __('No forks available to you.') }}<br />
- <span v-html="noForkText"></span>
+ {{ __('No forks are available to you.') }}<br />
+ <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>
<gl-link
:href="helpPagePath"
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d53a4c1286c..fc99f3ab5af 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -288,7 +288,7 @@
list-style: none;
padding: 0 1px;
- a,
+ a:not(.help-link),
button,
.menu-item {
@include dropdown-link;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 61542e89828..b03ad5c6b75 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -214,7 +214,6 @@ ul.related-merge-requests > li {
}
.create-merge-request-dropdown-menu {
- width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
diff --git a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb
new file mode 100644
index 00000000000..63455ff3acb
--- /dev/null
+++ b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb
@@ -0,0 +1,28 @@
+# 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
diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
new file mode 100644
index 00000000000..c680f387a9a
--- /dev/null
+++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
@@ -0,0 +1,93 @@
+# 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
diff --git a/app/graphql/types/error_tracking/sentry_error_frequency_type.rb b/app/graphql/types/error_tracking/sentry_error_frequency_type.rb
new file mode 100644
index 00000000000..a44ca0684b6
--- /dev/null
+++ b/app/graphql/types/error_tracking/sentry_error_frequency_type.rb
@@ -0,0 +1,18 @@
+# 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
diff --git a/app/graphql/types/error_tracking/sentry_error_status_enum.rb b/app/graphql/types/error_tracking/sentry_error_status_enum.rb
new file mode 100644
index 00000000000..df68eef4f3c
--- /dev/null
+++ b/app/graphql/types/error_tracking/sentry_error_status_enum.rb
@@ -0,0 +1,15 @@
+# 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
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 73255021119..d2a163b70db 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -145,5 +145,11 @@ module Types
null: true,
description: 'Build pipelines of the project',
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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index bcb86a33138..9e3fba139e3 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -128,7 +128,7 @@ module Issuable
end
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
end
diff --git a/app/policies/error_tracking/detailed_error_policy.rb b/app/policies/error_tracking/detailed_error_policy.rb
new file mode 100644
index 00000000000..cb74242d46a
--- /dev/null
+++ b/app/policies/error_tracking/detailed_error_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class DetailedErrorPolicy < BasePolicy
+ delegate { @subject.gitlab_project }
+ end
+end
diff --git a/app/presenters/sentry_detailed_error_presenter.rb b/app/presenters/sentry_detailed_error_presenter.rb
new file mode 100644
index 00000000000..9329f987879
--- /dev/null
+++ b/app/presenters/sentry_detailed_error_presenter.rb
@@ -0,0 +1,15 @@
+# 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
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 8d3e54dc455..eb76326602f 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -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 } }
- if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
- .menu-item
+ .menu-item.text-nowrap
= icon('check', class: 'icon')
- if can_create_confidential_merge_request?
= _('Create confidential merge request and branch')
diff --git a/changelogs/unreleased/34943-graphql-sentry-details.yml b/changelogs/unreleased/34943-graphql-sentry-details.yml
new file mode 100644
index 00000000000..06a0649e5ac
--- /dev/null
+++ b/changelogs/unreleased/34943-graphql-sentry-details.yml
@@ -0,0 +1,5 @@
+---
+title: GraphQL for Sentry rror details
+merge_request: 19733
+author:
+type: added
diff --git a/changelogs/unreleased/38244-fix-release-filter-on-mr-page.yml b/changelogs/unreleased/38244-fix-release-filter-on-mr-page.yml
new file mode 100644
index 00000000000..9c261d89610
--- /dev/null
+++ b/changelogs/unreleased/38244-fix-release-filter-on-mr-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed query behind release filter on merge request search page.
+merge_request: 38244
+author:
+type: fixed
diff --git a/changelogs/unreleased/confidential_mr_styling.yml b/changelogs/unreleased/confidential_mr_styling.yml
new file mode 100644
index 00000000000..fc936bbb87c
--- /dev/null
+++ b/changelogs/unreleased/confidential_mr_styling.yml
@@ -0,0 +1,5 @@
+---
+title: Improve create confidential MR dropdown styling.
+merge_request: 20176
+author: Lee Tickett
+type: other
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index fc3c16abdaa..6852e0016b0 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -4401,6 +4401,16 @@ type Project {
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
"""
sharedRunnersEnabled: Boolean
@@ -4886,6 +4896,150 @@ type RootStorageStatistics {
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 {
flatPath: String!
id: ID!
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 09f91e0f02f..f2cf473c1f3 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -1167,6 +1167,33 @@
"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",
"description": "Indicates if shared runners are enabled on the project",
"args": [
@@ -13790,6 +13817,469 @@
},
{
"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",
"name": "Metadata",
"description": null,
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 772648ec715..095d14ed4d5 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -664,6 +664,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `repository` | Repository | Git repository of the project |
| `mergeRequest` | MergeRequest | A single merge request of the project |
| `issue` | Issue | A single issue of the project |
+| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
### ProjectPermissions
@@ -751,6 +752,39 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `packagesSize` | Int! | The packages 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
| Name | Type | Description |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 8562dc646f1..83efea50a01 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -203,7 +203,7 @@ job:
You can use [YAML anchors](#anchors) with scripts, which makes it possible to
include a predefined list of commands in multiple jobs.
-Example:
+For example:
```yaml
.something: &something
@@ -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`
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:
- `when` - [reference](#when)
@@ -3159,6 +3164,29 @@ which can be set in GitLab's UI.
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
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb
index 225280a42f4..8438df2bbf8 100644
--- a/lib/gitlab/error_tracking/detailed_error.rb
+++ b/lib/gitlab/error_tracking/detailed_error.rb
@@ -4,6 +4,7 @@ module Gitlab
module ErrorTracking
class DetailedError
include ActiveModel::Model
+ include GlobalID::Identification
attr_accessor :count,
:culprit,
@@ -13,6 +14,7 @@ module Gitlab
:first_release_short_version,
:first_seen,
:frequency,
+ :gitlab_project,
:id,
:last_release_last_commit,
:last_release_short_version,
@@ -26,6 +28,10 @@ module Gitlab
:title,
:type,
:user_count
+
+ def self.declarative_policy_class
+ 'ErrorTracking::DetailedErrorPolicy'
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6dd524981a4..62f1a4c3a14 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11641,7 +11641,7 @@ msgstr ""
msgid "No files found."
msgstr ""
-msgid "No forks available to you."
+msgid "No forks are available to you."
msgstr ""
msgid "No issues for the selected time period."
@@ -18447,7 +18447,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
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 ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
@@ -21049,6 +21049,9 @@ msgstr ""
msgid "for this project"
msgstr ""
+msgid "fork this project"
+msgstr ""
+
msgid "from"
msgstr ""
diff --git a/spec/factories/error_tracking/detailed_error.rb b/spec/factories/error_tracking/detailed_error.rb
index cf7de2ece96..0fee329b808 100644
--- a/spec/factories/error_tracking/detailed_error.rb
+++ b/spec/factories/error_tracking/detailed_error.rb
@@ -2,13 +2,13 @@
FactoryBot.define do
factory :detailed_error_tracking_error, class: Gitlab::ErrorTracking::DetailedError do
- id { 'id' }
+ id { '1' }
title { 'title' }
type { 'error' }
user_count { 1 }
count { 2 }
- first_seen { Time.now }
- last_seen { Time.now }
+ first_seen { Time.now.iso8601 }
+ last_seen { Time.now.iso8601 }
message { 'message' }
culprit { 'culprit' }
external_url { 'http://example.com/id' }
@@ -18,7 +18,11 @@ FactoryBot.define do
project_slug { 'project_name' }
short_id { 'ID' }
status { 'unresolved' }
- frequency { [] }
+ frequency do
+ [
+ [Time.now.to_i, 10]
+ ]
+ end
first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' }
diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
index 838c0a6349c..84f358061e6 100644
--- a/spec/features/issues/user_creates_confidential_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
@@ -29,7 +29,7 @@ describe 'User creates confidential merge request on issue page', :js do
click_button 'Create confidential merge request'
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
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index 3c603c7f573..d69a9f90d65 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -15,19 +15,12 @@ exports[`Confidential merge request project form group component renders empty s
class="text-muted mt-1 mb-0"
>
- No forks available to you.
+ No forks are available to you.
<br />
- <span>
- To protect this issue's confidentiality,
- <a
- class="help-link"
- href="https://test.com"
- >
- fork the project
- </a>
- and set the forks visibility to private.
- </span>
+ <glsprintf-stub
+ message="To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
+ />
<gllink-stub
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
class="text-muted mt-1 mb-0"
>
- No forks available to you.
+ No forks are available to you.
<br />
- <span>
- To protect this issue's confidentiality,
- <a
- class="help-link"
- href="https://test.com"
- >
- fork the project
- </a>
- and set the forks visibility to private.
- </span>
+ <glsprintf-stub
+ message="To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
+ />
<gllink-stub
class="w-auto p-0 d-inline-block text-primary bg-transparent"
diff --git a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb
new file mode 100644
index 00000000000..5e7f18636ec
--- /dev/null
+++ b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb
@@ -0,0 +1,62 @@
+# 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
diff --git a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
new file mode 100644
index 00000000000..3576adb5272
--- /dev/null
+++ b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
@@ -0,0 +1,37 @@
+# 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
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 19a433f090e..8a697b1bcae 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -22,8 +22,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_pipeline_succeeds request_access_enabled
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues
- issue pipelines
- removeSourceBranchAfterMerge
+ issue pipelines removeSourceBranchAfterMerge sentryDetailedError
]
is_expected.to have_graphql_fields(*expected_fields)
diff --git a/spec/javascripts/vue_shared/components/bar_chart_spec.js b/spec/javascripts/vue_shared/components/bar_chart_spec.js
index 8f753876e44..8f673c146ec 100644
--- a/spec/javascripts/vue_shared/components/bar_chart_spec.js
+++ b/spec/javascripts/vue_shared/components/bar_chart_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import BarChart from '~/vue_shared/components/bar_chart.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import BarChart from '~/vue_shared/components/bar_chart.vue';
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index 42481f8c334..367e07d3ad3 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
describe('CI Badge Link Component', () => {
let CIBadge;
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js
index b59a7d7544f..9486d7d4f23 100644
--- a/spec/javascripts/vue_shared/components/ci_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => {
const Component = Vue.extend(ciIcon);
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
index 16997e9dc67..e3f6609f128 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
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 { 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';
describe('ContentViewer', () => {
diff --git a/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js b/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js
index 64fb984d9fc..e031583b43a 100644
--- a/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js
+++ b/spec/javascripts/vue_shared/components/deprecated_modal_2_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
-import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
const modalComponent = Vue.extend(DeprecatedModal2);
diff --git a/spec/javascripts/vue_shared/components/deprecated_modal_spec.js b/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
index be75be92158..d6c10e32794 100644
--- a/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
+++ b/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
const modalComponent = Vue.extend(DeprecatedModal);
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 1acd6b3ebe7..c743f1f6ad7 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -1,8 +1,8 @@
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 { 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', () => {
const requiredProps = {
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 0cb26d5000b..81f194395ef 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -1,7 +1,7 @@
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 { 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', () => {
const requiredProps = {
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
index 2fc4943de30..b00fa785a0e 100644
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
-import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
-
import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
const defaultLabel = 'Select';
const customLabel = 'Select project';
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
index 445ab0cb40e..402de2a8788 100644
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
@@ -1,8 +1,7 @@
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 dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { mockLabels } from './mock_data';
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
index 551520721e5..456f310d10c 100644
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -1,8 +1,7 @@
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 dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
const componentConfig = {
placeholderText: 'Search something',
diff --git a/spec/javascripts/vue_shared/components/file_finder/index_spec.js b/spec/javascripts/vue_shared/components/file_finder/index_spec.js
index bae4741f652..7ded228d3ea 100644
--- a/spec/javascripts/vue_shared/components/file_finder/index_spec.js
+++ b/spec/javascripts/vue_shared/components/file_finder/index_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
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 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', () => {
const Component = Vue.extend(FindFileComponent);
diff --git a/spec/javascripts/vue_shared/components/file_finder/item_spec.js b/spec/javascripts/vue_shared/components/file_finder/item_spec.js
index c1511643a9d..e18d0a46223 100644
--- a/spec/javascripts/vue_shared/components/file_finder/item_spec.js
+++ b/spec/javascripts/vue_shared/components/file_finder/item_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
import { file } from 'spec/ide/helpers';
+import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
import createComponent from '../../../helpers/vue_mount_component_helper';
describe('File finder item spec', () => {
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index 7da69e3fa84..2d80099fafe 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import { file } from 'spec/ide/helpers';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
-import { file } from 'spec/ide/helpers';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('File row component', () => {
diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
index 3d251426b5a..0bb4a04557b 100644
--- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
+++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import component from '~/vue_shared/components/filtered_search_dropdown.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import component from '~/vue_shared/components/filtered_search_dropdown.vue';
describe('Filtered search dropdown', () => {
const Component = Vue.extend(component);
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 2b059e5e9f4..7bd5e5a64b1 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import headerCi from '~/vue_shared/components/header_ci_component.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import headerCi from '~/vue_shared/components/header_ci_component.vue';
describe('Header CI Component', () => {
let HeaderCi;
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index ecaef414464..5a3e483fb03 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -1,7 +1,7 @@
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 mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Icon from '~/vue_shared/components/icon.vue';
describe('Sprite Icon Component', function() {
describe('Initialization', function() {
diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js
index db89d4a934c..6b03c354e01 100644
--- a/spec/javascripts/vue_shared/components/loading_button_spec.js
+++ b/spec/javascripts/vue_shared/components/loading_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import loadingButton from '~/vue_shared/components/loading_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import loadingButton from '~/vue_shared/components/loading_button.vue';
const LABEL = 'Hello';
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
index e6c7abd9d3b..288eb40cc76 100644
--- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
describe('toolbar', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
index 462bfc10664..beb980a6556 100644
--- a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
describe('navigation tabs component', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
index caabe3aa260..d65ee8eeb2d 100644
--- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js
+++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import panelResizer from '~/vue_shared/components/panel_resizer.vue';
describe('Panel Resizer component', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js
index 61f05e7a230..b787ba7596f 100644
--- a/spec/javascripts/vue_shared/components/pikaday_spec.js
+++ b/spec/javascripts/vue_shared/components/pikaday_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import datePicker from '~/vue_shared/components/pikaday.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import datePicker from '~/vue_shared/components/pikaday.vue';
describe('datePicker', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js b/spec/javascripts/vue_shared/components/project_avatar/default_spec.js
index 5fed3f4b892..2ec19ebf80e 100644
--- a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js
+++ b/spec/javascripts/vue_shared/components/project_avatar/default_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { projectData } from 'spec/ide/mock_data';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
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', () => {
const Component = Vue.extend(ProjectAvatarDefault);
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
index 47964a1702a..271ae1b645f 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,6 +1,6 @@
-import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper';
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
const localVue = createLocalVue();
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
index 323a0f03017..6815da31436 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
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 { mount, createLocalVue } from '@vue/test-utils';
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();
diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
index e723fead65e..47ebdc505c9 100644
--- a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
+++ b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
index 073d111989c..e4db67f5b18 100644
--- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
-import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
-
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
const createComponent = config => {
const Component = Vue.extend(stackedProgressBarComponent);
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
index 444ca451534..ea0a89a3ab5 100644
--- a/spec/javascripts/vue_shared/components/toggle_button_spec.js
+++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import toggleButton from '~/vue_shared/components/toggle_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
describe('Toggle Button', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
index 9152fa8e12f..31644416439 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
import avatarSvg from 'icons/_icon_random.svg';
+import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg);
diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js
index 0aaa4050cba..adca7cd64a1 100644
--- a/spec/javascripts/vue_shared/translate_spec.js
+++ b/spec/javascripts/vue_shared/translate_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Jed from 'jed';
+import { trimText } from 'spec/helpers/text_helper';
import locale from '~/locale';
import Translate from '~/vue_shared/translate';
-import { trimText } from 'spec/helpers/text_helper';
describe('Vue translate filter', () => {
let el;
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4a6a9026f77..76a3a825978 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Issuable do
+ include ProjectForksHelper
+
let(:issuable_class) { Issue }
let(:issue) { create(:issue, title: 'An issue', description: 'A description') }
let(:user) { create(:user) }
@@ -855,6 +857,7 @@ describe Issuable do
describe 'release scopes' do
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_2) { create(:release, tag: 'v2.0', project: project) }
@@ -875,52 +878,65 @@ describe Issuable do
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, 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
- it 'returns the issues 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)
+ it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
+ expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
+ expect(mr_items.without_release).to contain_exactly(mr_3)
end
end
describe '#any_release' do
- it 'returns all issues tied to a release' do
- expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
+ it 'returns all issues or all mrs tied to a release' do
+ 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
describe '#with_release' do
- it 'returns the issues tied a specfic release' do
- expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
+ it 'returns the issues tied to a specfic release' do
+ 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
context 'when a release has a milestone with one issue and another one with no 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
context 'when the milestone with no issue is added as a filter' 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
context 'when the milestone with the issue is added as a filter' 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
- context 'when there is no issue under a specific release' do
- it 'returns no issue' do
- expect(items.with_release('v4.0', project.id)).to be_empty
+ context 'when there is no issue or mr under a specific release' do
+ it 'returns no issue or no mr' do
+ 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
context 'when a non-existent release tag is passed in' do
- it 'returns no issue' do
- expect(items.with_release('v999.0', project.id)).to be_empty
+ it 'returns no issue or no mr' do
+ 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
diff --git a/spec/presenters/sentry_detailed_error_presenter_spec.rb b/spec/presenters/sentry_detailed_error_presenter_spec.rb
new file mode 100644
index 00000000000..e483b6d41a1
--- /dev/null
+++ b/spec/presenters/sentry_detailed_error_presenter_spec.rb
@@ -0,0 +1,29 @@
+# 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
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
new file mode 100644
index 00000000000..d10380dab3a
--- /dev/null
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
@@ -0,0 +1,69 @@
+# 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