diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-06 09:06:39 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-06 09:06:39 +0000 |
commit | cd15d0e6c32da7f69689c7cff2e90aeda33b8318 (patch) | |
tree | 8343899f0873ab05f3eadca72c5f6e0a653a12ac | |
parent | dd6afb4b4785ed1889defc6d7bb8ef114dd4eb50 (diff) | |
download | gitlab-ce-cd15d0e6c32da7f69689c7cff2e90aeda33b8318.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |