summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue11
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue63
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js5
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/performance_bar/index.js50
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js10
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/stylesheets/pages/error_details.scss6
-rw-r--r--app/models/clusters/applications/knative.rb10
-rw-r--r--app/models/deployment.rb7
-rw-r--r--app/models/environment_status.rb2
-rw-r--r--app/models/project.rb7
-rw-r--r--app/serializers/deployment_entity.rb11
-rw-r--r--app/serializers/environment_status_entity.rb4
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb4
-rw-r--r--app/services/issues/base_service.rb2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--changelogs/unreleased/33318-make-internal-projects-poolable.yml5
-rw-r--r--changelogs/unreleased/35458-expose-manual-actions-retry.yml5
-rw-r--r--changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml5
-rw-r--r--changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml5
-rw-r--r--changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml5
-rw-r--r--changelogs/unreleased/fixes-35624.yml5
-rw-r--r--changelogs/unreleased/tz-fe-timings-performancebar.yml5
-rw-r--r--doc/administration/monitoring/performance/img/performance_bar.pngbin58439 -> 71551 bytes
-rw-r--r--doc/administration/monitoring/performance/img/performance_bar_frontend.pngbin0 -> 362077 bytes
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md6
-rw-r--r--doc/api/issues.md6
-rw-r--r--doc/development/git_object_deduplication.md2
-rw-r--r--doc/user/project/deploy_boards.md2
-rw-r--r--lib/api/helpers/common_helpers.rb2
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb6
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js66
-rw-r--r--spec/frontend/error_tracking/components/list_mock.json29
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js56
-rw-r--r--spec/frontend/error_tracking/store/details/getters_spec.js14
-rw-r--r--spec/frontend/performance_bar/stores/performance_bar_store_spec.js17
-rw-r--r--spec/javascripts/boards/components/board_spec.js17
-rw-r--r--spec/javascripts/boards/issue_spec.js4
-rw-r--r--spec/javascripts/user_popovers_spec.js2
-rw-r--r--spec/models/deployment_spec.rb32
-rw-r--r--spec/models/project_spec.rb14
-rw-r--r--spec/serializers/deployment_entity_spec.rb30
-rw-r--r--spec/serializers/environment_status_entity_spec.rb3
-rw-r--r--spec/serializers/pipeline_entity_spec.rb23
-rw-r--r--spec/services/projects/git_deduplication_service_spec.rb59
51 files changed, 551 insertions, 111 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index a001b315d4f..9d8e5396dea 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -8,7 +8,6 @@ import {
GlTable,
GlSearchBoxByClick,
} from '@gitlab/ui';
-import { visitUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
@@ -76,8 +75,8 @@ export default {
this.startPolling(`${this.indexPath}?search_term=${this.errorSearchQuery}`);
},
trackViewInSentryOptions,
- viewDetails(errorId) {
- visitUrl(`error_tracking/${errorId}/details`);
+ getDetailsLink(errorId) {
+ return `error_tracking/${errorId}/details`;
},
},
};
@@ -129,11 +128,7 @@ export default {
</template>
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
- <gl-link
- class="d-flex text-dark"
- target="_blank"
- @click="viewDetails(errors.item.id)"
- >
+ <gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)">
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
</gl-link>
<span class="text-secondary text-truncate">
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace.vue b/app/assets/javascripts/error_tracking/components/stacktrace.vue
index 6b71967624f..f58d54f2933 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace.vue
@@ -27,6 +27,8 @@ export default {
:lines="entry.context"
:file-path="entry.filename"
:error-line="entry.lineNo"
+ :error-fn="entry.function"
+ :error-column="entry.colNo"
:expanded="isFirstEntry(index)"
/>
</div>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index ad542c579a9..9ed5b26a1c2 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,4 +1,5 @@
<script>
+import { __, sprintf } from '~/locale';
import { GlTooltip } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -22,9 +23,20 @@ export default {
type: String,
required: true,
},
+ errorFn: {
+ type: String,
+ required: false,
+ default: '',
+ },
errorLine: {
type: Number,
- required: true,
+ required: false,
+ default: 0,
+ },
+ errorColumn: {
+ type: Number,
+ required: false,
+ default: 0,
},
expanded: {
type: Boolean,
@@ -38,12 +50,23 @@ export default {
};
},
computed: {
- linesLength() {
- return this.lines.length;
+ hasCode() {
+ return Boolean(this.lines.length);
},
collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
+ noCodeFn() {
+ return this.errorFn ? sprintf(__('in %{errorFn} '), { errorFn: this.errorFn }) : '';
+ },
+ noCodeLine() {
+ return this.errorLine
+ ? sprintf(__('at line %{errorLine}%{errorColumn}'), {
+ errorLine: this.errorLine,
+ errorColumn: this.errorColumn ? `:${this.errorColumn}` : '',
+ })
+ : '';
+ },
},
methods: {
isHighlighted(lineNum) {
@@ -66,27 +89,31 @@ export default {
<template>
<div class="file-holder">
<div ref="header" class="file-title file-title-flex-parent">
- <div class="file-header-content ">
- <div class="d-inline-block cursor-pointer" @click="toggle()">
+ <div class="file-header-content d-flex align-content-center">
+ <div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
</div>
- <div class="d-inline-block append-right-4">
- <file-icon
- :file-name="filePath"
- :size="18"
- aria-hidden="true"
- css-classes="append-right-5"
- />
- <strong v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
- {{ filePath }}
- </strong>
- </div>
-
+ <file-icon
+ :file-name="filePath"
+ :size="18"
+ aria-hidden="true"
+ css-classes="append-right-5"
+ />
+ <strong
+ v-gl-tooltip
+ :title="filePath"
+ class="file-title-name d-inline-block overflow-hidden text-truncate"
+ :class="{ 'limited-width': !hasCode }"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
<clipboard-button
:title="__('Copy file path')"
:text="filePath"
- css-class="btn-default btn-transparent btn-clipboard"
+ css-class="btn-default btn-transparent btn-clipboard position-static"
/>
+ <span v-if="!hasCode" class="text-tertiary">{{ noCodeFn }}{{ noCodeLine }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
index 7d13439d721..a36c84dc28c 100644
--- a/app/assets/javascripts/error_tracking/store/details/getters.js
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -1,3 +1,6 @@
-export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse();
+export const stacktrace = state =>
+ state.stacktraceData.stack_trace_entries
+ ? state.stacktraceData.stack_trace_entries.reverse()
+ : [];
export default () => {};
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index a0272b148e3..d17c2f33adc 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -52,6 +52,11 @@ export default {
header: s__('PerformanceBar|Redis calls'),
keys: ['cmd'],
},
+ {
+ metric: 'total',
+ header: s__('PerformanceBar|Frontend resources'),
+ keys: ['name', 'size'],
+ },
],
data() {
return { currentRequestId: '' };
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 735c9d804ee..2ffe07500e0 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
@@ -53,12 +54,61 @@ export default ({ container }) =>
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data);
+
+ if (this.requestId === requestId) this.collectFrontendPerformanceMetrics();
})
.catch(() =>
// eslint-disable-next-line no-console
console.warn(`Error getting performance bar results for ${requestId}`),
);
},
+ collectFrontendPerformanceMetrics() {
+ if (performance) {
+ const navigationEntries = performance.getEntriesByType('navigation');
+ const paintEntries = performance.getEntriesByType('paint');
+ const resourceEntries = performance.getEntriesByType('resource');
+
+ let durationString = '';
+ if (navigationEntries.length > 0) {
+ durationString = `BE ${this.formatMs(navigationEntries[0].responseEnd)} / `;
+ durationString += `FCP ${this.formatMs(paintEntries[1].startTime)} / `;
+ durationString += `DOM ${this.formatMs(navigationEntries[0].domContentLoadedEventEnd)}`;
+ }
+
+ let newEntries = resourceEntries.map(this.transformResourceEntry);
+
+ this.updateFrontendPerformanceMetrics(durationString, newEntries);
+
+ if ('PerformanceObserver' in window) {
+ // We start observing for more incoming timings
+ const observer = new PerformanceObserver(list => {
+ newEntries = newEntries.concat(list.getEntries().map(this.transformResourceEntry));
+ this.updateFrontendPerformanceMetrics(durationString, newEntries);
+ });
+
+ observer.observe({ entryTypes: ['resource'] });
+ }
+ }
+ },
+ updateFrontendPerformanceMetrics(durationString, requestEntries) {
+ this.store.setRequestDetailsData(this.requestId, 'total', {
+ duration: durationString,
+ calls: requestEntries.length,
+ details: requestEntries,
+ });
+ },
+ transformResourceEntry(entry) {
+ const nf = new Intl.NumberFormat();
+ return {
+ name: entry.name.replace(document.location.origin, ''),
+ duration: Math.round(entry.duration),
+ size: entry.transferSize ? `${nf.format(entry.transferSize)} bytes` : 'cached',
+ };
+ },
+ formatMs(msValue) {
+ const nf = new Intl.NumberFormat();
+ return `${nf.format(Math.round(msValue))}ms`;
+ },
},
render(createElement) {
return createElement('performance-bar-app', {
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 12d0ee86218..6f443db47ed 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -32,6 +32,16 @@ export default class PerformanceBarStore {
return request;
}
+ setRequestDetailsData(requestId, metricKey, requestDetailsData) {
+ const selectedRequest = this.findRequest(requestId);
+ if (selectedRequest) {
+ selectedRequest.details = {
+ ...selectedRequest.details,
+ [metricKey]: requestDetailsData,
+ };
+ }
+ }
+
requestsWithDetails() {
return this.requests.filter(request => request.details);
}
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 7d6a725b30f..157d89a3a40 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -17,6 +17,7 @@ const handleUserPopoverMouseOut = event => {
renderedPopover.$destroy();
renderedPopover = null;
}
+ target.removeAttribute('aria-describedby');
};
/**
diff --git a/app/assets/stylesheets/pages/error_details.scss b/app/assets/stylesheets/pages/error_details.scss
index 0515db914e9..dcd25c126c4 100644
--- a/app/assets/stylesheets/pages/error_details.scss
+++ b/app/assets/stylesheets/pages/error_details.scss
@@ -12,6 +12,12 @@
}
}
+ .file-title-name {
+ &.limited-width {
+ max-width: 80%;
+ }
+ }
+
.line_content.old::before {
content: none !important;
}
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 6ad086211fb..c15d9edf7b2 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -17,11 +17,11 @@ module Clusters
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
+ alias_method :original_set_initial_status, :set_initial_status
def set_initial_status
- return unless not_installable?
- return unless verify_cluster?
+ return unless cluster&.platform_kubernetes_rbac?
- self.status = status_states[:installable]
+ original_set_initial_status
end
state_machine :status do
@@ -131,10 +131,6 @@ module Clusters
[Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)]
end
-
- def verify_cluster?
- cluster&.application_helm_available? && cluster&.platform_kubernetes_rbac?
- end
end
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 8ef49fc56c6..65f5cbf69c6 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -5,6 +5,7 @@ class Deployment < ApplicationRecord
include IidRoutes
include AfterCommitQueue
include UpdatedAtFilterable
+ include Gitlab::Utils::StrongMemoize
belongs_to :project, required: true
belongs_to :environment, required: true
@@ -126,6 +127,12 @@ class Deployment < ApplicationRecord
@scheduled_actions ||= deployable.try(:other_scheduled_actions)
end
+ def playable_build
+ strong_memoize(:playable_build) do
+ deployable.try(:playable?) ? deployable : nil
+ end
+ end
+
def includes_commit?(commit)
return false unless commit
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index d7dc64190d6..3eca8c91e40 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -78,7 +78,7 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments.available.map do |environment|
+ pipeline.environments.includes(:project).available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
diff --git a/app/models/project.rb b/app/models/project.rb
index 26392964ced..3177a5b83f9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2250,12 +2250,13 @@ class Project < ApplicationRecord
# Git objects are only poolable when the project is or has:
# - Hashed storage -> The object pool will have a remote to its members, using relative paths.
# If the repository path changes we would have to update the remote.
- # - Public -> User will be able to fetch Git objects that might not exist
- # in their own repository.
+ # - not private -> The visibility level or repository access level has to be greater than private
+ # to prevent fetching objects that might not exist
# - Repository -> Else the disk path will be empty, and there's nothing to pool
def git_objects_poolable?
hashed_storage?(:repository) &&
- public? &&
+ visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
+ repository_access_level > ProjectFeature::PRIVATE &&
repository_exists? &&
Gitlab::CurrentSettings.hashed_storage_enabled
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index e6421315b34..94773eeebd0 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -37,6 +37,9 @@ class DeploymentEntity < Grape::Entity
expose :commit, using: CommitEntity, if: -> (*) { include_details? }
expose :manual_actions, using: JobEntity, if: -> (*) { include_details? && can_create_deployment? }
expose :scheduled_actions, using: JobEntity, if: -> (*) { include_details? && can_create_deployment? }
+ expose :playable_build, expose_nil: false, if: -> (*) { include_details? && can_create_deployment? } do |deployment, options|
+ JobEntity.represent(deployment.playable_build, options.merge(only: [:play_path, :retry_path]))
+ end
expose :cluster, using: ClusterBasicEntity
@@ -47,7 +50,7 @@ class DeploymentEntity < Grape::Entity
end
def can_create_deployment?
- can?(request.current_user, :create_deployment, request.project)
+ can?(request.current_user, :create_deployment, project)
end
def can_read_deployables?
@@ -56,6 +59,10 @@ class DeploymentEntity < Grape::Entity
# because it triggers a policy evaluation that involves multiple
# Gitaly calls that might not be cached.
#
- can?(request.current_user, :read_build, request.project)
+ can?(request.current_user, :read_build, project)
+ end
+
+ def project
+ request.try(:project) || options[:project]
end
end
diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb
index 811cc2ad5af..40db23c143e 100644
--- a/app/serializers/environment_status_entity.rb
+++ b/app/serializers/environment_status_entity.rb
@@ -37,6 +37,10 @@ class EnvironmentStatusEntity < Grape::Entity
es.deployment.try(:formatted_deployment_time)
end
+ expose :deployment, as: :details do |es, options|
+ DeploymentEntity.represent(es.deployment, options.merge(project: es.project, only: [:playable_build]))
+ end
+
expose :changes
private
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 2a61187a856..028de38e42a 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-class MergeRequestPollWidgetEntity < IssuableEntity
+class MergeRequestPollWidgetEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :auto_merge_strategy
expose :available_auto_merge_strategies do |merge_request|
AutoMergeService.new(merge_request.project, current_user).available_strategies(merge_request) # rubocop: disable CodeReuse/ServiceClass
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 94e8b174f0f..cddb894fd64 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -77,6 +77,10 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
+ expose :failed_builds, if: -> (*) { can_retry? }, using: JobEntity do |pipeline|
+ pipeline.builds.failed
+ end
+
private
alias_method :pipeline, :object
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 48ed5afbc2a..974f7e598ca 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -36,3 +36,5 @@ module Issues
end
end
end
+
+Issues::BaseService.prepend_if_ee('EE::Issues::BaseService')
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 1fef43c0c37..be574155436 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -18,7 +18,7 @@
.col-lg-4
%h4.prepend-top-0= _('Notification events')
%p
- - notification_link = link_to _('notification emails'), help_page_path('workflow/notifications'), target: '_blank'
+ - notification_link = link_to _('notification emails'), help_page_path('user/profile/notifications'), target: '_blank'
- paragraph = _('Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.') % { notification_link: notification_link.html_safe }
#{ paragraph.html_safe }
.col-lg-8
diff --git a/changelogs/unreleased/33318-make-internal-projects-poolable.yml b/changelogs/unreleased/33318-make-internal-projects-poolable.yml
new file mode 100644
index 00000000000..c13794018d2
--- /dev/null
+++ b/changelogs/unreleased/33318-make-internal-projects-poolable.yml
@@ -0,0 +1,5 @@
+---
+title: Make internal projects poolable
+merge_request: 19295
+author: briankabiro
+type: changed
diff --git a/changelogs/unreleased/35458-expose-manual-actions-retry.yml b/changelogs/unreleased/35458-expose-manual-actions-retry.yml
new file mode 100644
index 00000000000..167dca796c4
--- /dev/null
+++ b/changelogs/unreleased/35458-expose-manual-actions-retry.yml
@@ -0,0 +1,5 @@
+---
+title: Exposed deployment build manual actions for merge request page
+merge_request: 20615
+author:
+type: changed
diff --git a/changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml b/changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml
new file mode 100644
index 00000000000..2d2450ebd68
--- /dev/null
+++ b/changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml
@@ -0,0 +1,5 @@
+---
+title: Update deploy instances color scheme
+merge_request: 20890
+author:
+type: changed
diff --git a/changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml b/changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml
new file mode 100644
index 00000000000..213517c0ec1
--- /dev/null
+++ b/changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml
@@ -0,0 +1,5 @@
+---
+title: Handle empty stacktrace and entries with no code
+merge_request: 20458
+author:
+type: fixed
diff --git a/changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml b/changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml
new file mode 100644
index 00000000000..b6e3f1af414
--- /dev/null
+++ b/changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Fix opening Sentry error details in new tab
+merge_request: 20611
+author:
+type: fixed
diff --git a/changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml
new file mode 100644
index 00000000000..549127c365b
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20875
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml
new file mode 100644
index 00000000000..330bf2493d6
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20876
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/fixes-35624.yml b/changelogs/unreleased/fixes-35624.yml
new file mode 100644
index 00000000000..855a4ad8e93
--- /dev/null
+++ b/changelogs/unreleased/fixes-35624.yml
@@ -0,0 +1,5 @@
+---
+title: Resets aria-describedby on mouseleave
+merge_request: 20092
+author: carolcarvalhosa
+type: fixed
diff --git a/changelogs/unreleased/tz-fe-timings-performancebar.yml b/changelogs/unreleased/tz-fe-timings-performancebar.yml
new file mode 100644
index 00000000000..6b6f2cc7ea0
--- /dev/null
+++ b/changelogs/unreleased/tz-fe-timings-performancebar.yml
@@ -0,0 +1,5 @@
+---
+title: Added Total/Frontend metrics to the performance bar
+merge_request: 20725
+author:
+type: added
diff --git a/doc/administration/monitoring/performance/img/performance_bar.png b/doc/administration/monitoring/performance/img/performance_bar.png
index 73f2ccbe4bb..e876e2f373b 100644
--- a/doc/administration/monitoring/performance/img/performance_bar.png
+++ b/doc/administration/monitoring/performance/img/performance_bar.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/performance_bar_frontend.png b/doc/administration/monitoring/performance/img/performance_bar_frontend.png
new file mode 100644
index 00000000000..489f855fe33
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/performance_bar_frontend.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index caddc87d8c1..e65fdfd028d 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -16,6 +16,12 @@ It allows you to see (from left to right):
![Rugged profiling using the Performance Bar](img/performance_bar_rugged_calls.png)
- time taken and number of Redis calls; click through for details of these calls
![Redis profiling using the Performance Bar](img/performance_bar_redis_calls.png)
+- total load timings of the page; click through for details of these calls
+ - BE = Backend - Time that the actual base page took to load
+ - FCP = [First Contentful Paint](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint) - Time until something was visible to the user
+ - DOM = [DomContentLoaded](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp) Event
+ - Number of Requests that the page loaded
+ ![Frontend requests using the Performance Bar](img/performance_bar_frontend.png)
- a link to add a request's details to the performance bar; the request can be
added by its full URL (authenticated as the current user), or by the value of
its `X-Request-Id` header
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 29f9cb40e41..fe551cfb397 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -627,7 +627,8 @@ POST /projects/:id/issues
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
| `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. |
-| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_id` **(ULTIMATE)** | integer | no | ID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -729,7 +730,8 @@ PUT /projects/:id/issues/:issue_iid
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. 0 |
| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
-| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_id` **(ULTIMATE)** | integer | no | ID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
diff --git a/doc/development/git_object_deduplication.md b/doc/development/git_object_deduplication.md
index 6d9eb90d482..938882ba5a2 100644
--- a/doc/development/git_object_deduplication.md
+++ b/doc/development/git_object_deduplication.md
@@ -111,7 +111,7 @@ are as follows:
contents of the pool repository are a Git clone of the source
project repository.
- The occasion for creating a pool is when an existing eligible
- (public, hashed storage, non-forked) GitLab project gets forked and
+ (non-private, hashed storage, non-forked) GitLab project gets forked and
this project does not belong to a pool repository yet. The fork
parent project becomes the source project of the new pool, and both
the fork parent and the fork child project become members of the new
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
index b14d7f821bb..98e9188ed9b 100644
--- a/doc/user/project/deploy_boards.md
+++ b/doc/user/project/deploy_boards.md
@@ -14,7 +14,7 @@ With Deploy Boards you can gain more insight into deploys with benefits such as:
- Following a deploy from the start, not just when it's done
- Watching the rollout of a build across multiple servers
-- Finer state detail (Waiting, Deploying, Finished, Unknown)
+- Finer state detail (Succeeded, Running, Failed, Pending, Unknown)
- See [Canary Deployments](canary_deployments.md)
Here's an example of a Deploy Board of the production environment.
diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb
index 7551ca50a7f..32a15381f27 100644
--- a/lib/api/helpers/common_helpers.rb
+++ b/lib/api/helpers/common_helpers.rb
@@ -15,3 +15,5 @@ module API
end
end
end
+
+API::Helpers::CommonHelpers.prepend_if_ee('EE::API::Helpers::CommonHelpers')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 15c05f1f37d..7832ee37ebd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5272,12 +5272,21 @@ msgstr ""
msgid "CycleAnalytics|All stages"
msgstr ""
+msgid "CycleAnalytics|Date"
+msgstr ""
+
+msgid "CycleAnalytics|Days to completion"
+msgstr ""
+
msgid "CycleAnalytics|No stages selected"
msgstr ""
msgid "CycleAnalytics|Stages"
msgstr ""
+msgid "CycleAnalytics|Total days to completion"
+msgstr ""
+
msgid "CycleAnalytics|group dropdown filter"
msgstr ""
@@ -12269,6 +12278,9 @@ msgstr ""
msgid "PerformanceBar|Download"
msgstr ""
+msgid "PerformanceBar|Frontend resources"
+msgstr ""
+
msgid "PerformanceBar|Gitaly calls"
msgstr ""
@@ -17642,6 +17654,9 @@ msgstr ""
msgid "There was an error while fetching cycle analytics data."
msgstr ""
+msgid "There was an error while fetching cycle analytics duration data."
+msgstr ""
+
msgid "There was an error while fetching cycle analytics summary data."
msgstr ""
@@ -20374,6 +20389,9 @@ msgstr ""
msgid "assign yourself"
msgstr ""
+msgid "at line %{errorLine}%{errorColumn}"
+msgstr ""
+
msgid "attach a new file"
msgstr ""
@@ -20854,6 +20872,9 @@ msgstr ""
msgid "importing"
msgstr ""
+msgid "in %{errorFn} "
+msgstr ""
+
msgid "in group %{link_to_group}"
msgstr ""
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 9f7fde2f0da..bcdff060350 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1226,9 +1226,9 @@ describe Projects::MergeRequestsController do
environment2 = create(:environment, project: forked)
create(:deployment, :succeed, environment: environment2, sha: sha, ref: 'master', deployable: build)
- # TODO address the last 5 queries
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/63952 (5 queries)
- leeway = 5
+ # TODO address the last 3 queries
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/63952 (3 queries)
+ leeway = 3
expect { get_ci_environments_status }.not_to exceed_all_query_limit(control_count + leeway)
end
end
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 4edc2a647c3..80f5b2ccb9f 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -9,6 +9,7 @@ import {
GlLink,
GlSearchBoxByClick,
} from '@gitlab/ui';
+import errorsList from './list_mock.json';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -18,11 +19,17 @@ describe('ErrorTrackingList', () => {
let wrapper;
let actions;
+ const findErrorListTable = () => wrapper.find('table');
+ const findErrorListRows = () => wrapper.findAll('tbody tr');
+ const findButton = () => wrapper.find(GlButton);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
function mountComponent({
errorTrackingEnabled = true,
userCanEnableErrorTracking = true,
stubs = {
'gl-link': GlLink,
+ 'gl-table': GlTable,
},
} = {}) {
wrapper = shallowMount(ErrorTrackingList, {
@@ -47,7 +54,7 @@ describe('ErrorTrackingList', () => {
};
const state = {
- errors: [],
+ errors: errorsList,
loading: true,
};
@@ -75,61 +82,74 @@ describe('ErrorTrackingList', () => {
});
it('shows spinner', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
- expect(wrapper.find(GlTable).exists()).toBeFalsy();
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findErrorListTable().exists()).toBe(false);
});
});
describe('results', () => {
beforeEach(() => {
store.state.list.loading = false;
-
mountComponent();
});
it('shows table', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
- expect(wrapper.find(GlTable).exists()).toBeTruthy();
- expect(wrapper.find(GlButton).exists()).toBeTruthy();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findErrorListTable().exists()).toBe(true);
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it('shows list of errors in a table', () => {
+ expect(findErrorListRows().length).toEqual(store.state.list.errors.length);
+ });
+
+ it('each error in a list should have a link to the error page', () => {
+ const errorTitle = wrapper.findAll('tbody tr a');
+
+ errorTitle.wrappers.forEach((_, index) => {
+ expect(errorTitle.at(index).attributes('href')).toEqual(
+ expect.stringMatching(/error_tracking\/\d+\/details$/),
+ );
+ });
});
describe('filtering', () => {
+ const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
+
it('shows search box', () => {
- expect(wrapper.find(GlSearchBoxByClick).exists()).toBeTruthy();
+ expect(findSearchBox().exists()).toBe(true);
});
it('makes network request on submit', () => {
expect(actions.startPolling).toHaveBeenCalledTimes(1);
-
- wrapper.find(GlSearchBoxByClick).vm.$emit('submit');
-
+ findSearchBox().vm.$emit('submit');
expect(actions.startPolling).toHaveBeenCalledTimes(2);
});
});
});
describe('no results', () => {
+ const findRefreshLink = () => wrapper.find('.js-try-again');
+
beforeEach(() => {
store.state.list.loading = false;
+ store.state.list.errors = [];
mountComponent();
});
it('shows empty table', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
- expect(wrapper.find(GlTable).exists()).toBeTruthy();
- expect(wrapper.find(GlButton).exists()).toBeTruthy();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findErrorListRows().length).toEqual(1);
+ expect(findButton().exists()).toBe(true);
});
it('shows a message prompting to refresh', () => {
- const refreshLink = wrapper.vm.$refs.empty.querySelector('a');
-
- expect(refreshLink.textContent.trim()).toContain('Check again');
+ expect(findRefreshLink().text()).toContain('Check again');
});
it('restarts polling', () => {
- wrapper.find('.js-try-again').trigger('click');
-
+ findRefreshLink().trigger('click');
expect(actions.restartPolling).toHaveBeenCalled();
});
});
@@ -140,10 +160,10 @@ describe('ErrorTrackingList', () => {
});
it('shows empty state', () => {
- expect(wrapper.find(GlEmptyState).exists()).toBeTruthy();
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
- expect(wrapper.find(GlTable).exists()).toBeFalsy();
- expect(wrapper.find(GlButton).exists()).toBeFalsy();
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findErrorListTable().exists()).toBe(false);
+ expect(findButton().exists()).toBe(false);
});
});
diff --git a/spec/frontend/error_tracking/components/list_mock.json b/spec/frontend/error_tracking/components/list_mock.json
new file mode 100644
index 00000000000..a6e94c1a026
--- /dev/null
+++ b/spec/frontend/error_tracking/components/list_mock.json
@@ -0,0 +1,29 @@
+[
+ {
+ "id": "1",
+ "title": "PG::ConnectionBad: FATAL",
+ "type": "error",
+ "userCount": 0,
+ "count": "52",
+ "firstSeen": "2019-05-30T07:21:46Z",
+ "lastSeen": "2019-11-06T03:21:39Z"
+ },
+ {
+ "id": "2",
+ "title": "ActiveRecord::StatementInvalid",
+ "type": "error",
+ "userCount": 0,
+ "count": "12",
+ "firstSeen": "2019-10-19T03:53:56Z",
+ "lastSeen": "2019-11-05T03:51:54Z"
+ },
+ {
+ "id": "3",
+ "title": "Command has failed",
+ "type": "default",
+ "userCount": 0,
+ "count": "275",
+ "firstSeen": "2019-02-12T07:22:36Z",
+ "lastSeen": "2019-10-22T03:20:48Z"
+ }
+] \ No newline at end of file
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index 95958408770..942585d5370 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -7,26 +7,23 @@ import Icon from '~/vue_shared/components/icon.vue';
describe('Stacktrace Entry', () => {
let wrapper;
+ const lines = [
+ [22, ' def safe_thread(name, \u0026block)\n'],
+ [23, ' Thread.new do\n'],
+ [24, " Thread.current['sidekiq_label'] = name\n"],
+ [25, ' watchdog(name, \u0026block)\n'],
+ ];
+
function mountComponent(props) {
wrapper = shallowMount(StackTraceEntry, {
propsData: {
filePath: 'sidekiq/util.rb',
- lines: [
- [22, ' def safe_thread(name, \u0026block)\n'],
- [23, ' Thread.new do\n'],
- [24, " Thread.current['sidekiq_label'] = name\n"],
- [25, ' watchdog(name, \u0026block)\n'],
- ],
errorLine: 24,
...props,
},
});
}
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -34,16 +31,47 @@ describe('Stacktrace Entry', () => {
});
it('should render stacktrace entry collapsed', () => {
+ mountComponent({ lines });
expect(wrapper.find(StackTraceEntry).exists()).toBe(true);
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
expect(wrapper.find(Icon).exists()).toBe(true);
expect(wrapper.find(FileIcon).exists()).toBe(true);
- expect(wrapper.element.querySelectorAll('table').length).toBe(0);
+ expect(wrapper.find('table').exists()).toBe(false);
});
it('should render stacktrace entry table expanded', () => {
- mountComponent({ expanded: true });
- expect(wrapper.element.querySelectorAll('tr.line_holder').length).toBe(4);
- expect(wrapper.element.querySelectorAll('.line_content.old').length).toBe(1);
+ mountComponent({ expanded: true, lines });
+ expect(wrapper.find('table').exists()).toBe(true);
+ expect(wrapper.findAll('tr.line_holder').length).toBe(4);
+ expect(wrapper.findAll('.line_content.old').length).toBe(1);
+ });
+
+ describe('no code block', () => {
+ const findFileHeaderContent = () => wrapper.find('.file-header-content').html();
+
+ it('should hide collapse icon and render error fn name and error line when there is no code block', () => {
+ const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 };
+ mountComponent({ expanded: false, lines: [], ...extraInfo });
+ expect(wrapper.find(Icon).exists()).toBe(false);
+ expect(findFileHeaderContent()).toContain(
+ `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`,
+ );
+ });
+
+ it('should render only lineNo:columnNO when there is no errorFn ', () => {
+ const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 };
+ mountComponent({ expanded: false, lines: [], ...extraInfo });
+ expect(findFileHeaderContent()).not.toContain(`in ${extraInfo.errorFn}`);
+ expect(findFileHeaderContent()).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`);
+ });
+
+ it('should render only lineNo when there is no errorColumn ', () => {
+ const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null };
+ mountComponent({ expanded: false, lines: [], ...extraInfo });
+ expect(findFileHeaderContent()).toContain(
+ `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}`,
+ );
+ expect(findFileHeaderContent()).not.toContain(`:${extraInfo.errorColumn}`);
+ });
});
});
diff --git a/spec/frontend/error_tracking/store/details/getters_spec.js b/spec/frontend/error_tracking/store/details/getters_spec.js
index ea57de5872b..aba080790da 100644
--- a/spec/frontend/error_tracking/store/details/getters_spec.js
+++ b/spec/frontend/error_tracking/store/details/getters_spec.js
@@ -1,12 +1,18 @@
import * as getters from '~/error_tracking/store/details/getters';
describe('Sentry error details store getters', () => {
- const state = {
- stacktraceData: { stack_trace_entries: [1, 2] },
- };
-
describe('stacktrace', () => {
+ it('should return empty stacktrace when there are no entries', () => {
+ const state = {
+ stacktraceData: { stack_trace_entries: null },
+ };
+ expect(getters.stacktrace(state)).toEqual([]);
+ });
+
it('should get stacktrace', () => {
+ const state = {
+ stacktraceData: { stack_trace_entries: [1, 2] },
+ };
expect(getters.stacktrace(state)).toEqual([2, 1]);
});
});
diff --git a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
index 686029a28a9..6b7893cb523 100644
--- a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
+++ b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
@@ -42,4 +42,21 @@ describe('PerformanceBarStore', () => {
expect(findUrl('id')).toEqual('html5-boilerplate');
});
});
+
+ describe('setRequestDetailsData', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PerformanceBarStore();
+ });
+
+ it('updates correctly specific details', () => {
+ store.addRequest('id', 'https://gitlab.com/');
+ store.setRequestDetailsData('id', 'test', {
+ calls: 123,
+ });
+
+ expect(store.findRequest('id').details.test.calls).toEqual(123);
+ });
+ });
});
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index ccb657e0df1..86a2a10b7a0 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import Board from '~/boards/components/board';
import List from '~/boards/models/list';
-import { mockBoardService } from '../mock_data';
describe('Board component', () => {
let vm;
@@ -35,13 +34,6 @@ describe('Board component', () => {
const setUpTests = (done, opts = {}) => {
loadFixtures('boards/show.html');
- gl.boardService = mockBoardService({
- boardsEndpoint: '/',
- listsEndpoint: '/',
- bulkUpdatePath: '/',
- boardId: 1,
- });
-
createComponent(opts);
Vue.nextTick(done);
@@ -61,15 +53,6 @@ describe('Board component', () => {
};
describe('List', () => {
- beforeEach(() => {
- gl.boardService = mockBoardService({
- boardsEndpoint: '/',
- listsEndpoint: '/',
- bulkUpdatePath: '/',
- boardId: 1,
- });
- });
-
it('board is expandable when list type is closed', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true);
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 05e6ea1394d..890a47c189a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -7,13 +7,13 @@ import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
-import { mockBoardService } from './mock_data';
+import { setMockEndpoints } from './mock_data';
describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = mockBoardService();
+ setMockEndpoints();
boardsStore.create();
issue = new ListIssue({
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
index c0d5ee9c446..e2fc359644d 100644
--- a/spec/javascripts/user_popovers_spec.js
+++ b/spec/javascripts/user_popovers_spec.js
@@ -38,6 +38,7 @@ describe('User Popovers', () => {
const shownPopover = document.querySelector('.popover');
expect(shownPopover).not.toBeNull();
+ expect(targetLink.getAttribute('aria-describedby')).not.toBeNull();
expect(shownPopover.innerHTML).toContain(dummyUser.name);
expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId.toString());
@@ -47,6 +48,7 @@ describe('User Popovers', () => {
setTimeout(() => {
// After Mouse leave it should be hidden now
expect(document.querySelector('.popover')).toBeNull();
+ expect(targetLink.getAttribute('aria-describedby')).toBeNull();
done();
});
}, 210); // We need to wait until the 200ms mouseover delay is over, only then the popover will be visible
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 268542c39c4..522a27954e2 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -442,4 +442,36 @@ describe Deployment do
expect(deploy2.previous_environment_deployment).to be_nil
end
end
+
+ describe '#playable_build' do
+ subject { deployment.playable_build }
+
+ context 'when there is a deployable build' do
+ let(:deployment) { create(:deployment, deployable: build) }
+
+ context 'when the deployable build is playable' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'returns that build' do
+ is_expected.to eq(build)
+ end
+ end
+
+ context 'when the deployable build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'when there is no deployable build' do
+ let(:deployment) { create(:deployment) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ac6ae5e1cc6..cf7bc59f89d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5088,12 +5088,24 @@ describe Project do
it { is_expected.not_to be_git_objects_poolable }
end
- context 'when the project is not public' do
+ context 'when the project is private' do
let(:project) { create(:project, :private) }
it { is_expected.not_to be_git_objects_poolable }
end
+ context 'when the project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it { is_expected.to be_git_objects_poolable }
+ end
+
+ context 'when the project is internal' do
+ let(:project) { create(:project, :repository, :internal) }
+
+ it { is_expected.to be_git_objects_poolable }
+ end
+
context 'when objects are poolable' do
let(:project) { create(:project, :repository, :public) }
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index d7816a3503d..2a57ea51b39 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -107,6 +107,36 @@ describe DeploymentEntity do
end
end
+ describe 'playable_build' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when the deployment has a playable deployable' do
+ context 'when this build is ready to be played' do
+ let(:build) { create(:ci_build, :playable, :scheduled, pipeline: pipeline) }
+
+ it 'exposes only the play_path' do
+ expect(subject[:playable_build].keys).to contain_exactly(:play_path)
+ end
+ end
+
+ context 'when this build has failed' do
+ let(:build) { create(:ci_build, :playable, :failed, pipeline: pipeline) }
+
+ it 'exposes the play_path and the retry_path' do
+ expect(subject[:playable_build].keys).to contain_exactly(:play_path, :retry_path)
+ end
+ end
+ end
+
+ context 'when the deployment does not have a playable deployable' do
+ let(:build) { create(:ci_build) }
+
+ it 'is not exposed' do
+ expect(subject[:playable_build]).to be_nil
+ end
+ end
+ end
+
context 'when deployment details serialization was disabled' do
include Gitlab::Routing
diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb
index 0687751fd67..bac590d44e9 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe EnvironmentStatusEntity do
let(:user) { create(:user) }
- let(:request) { double('request') }
+ let(:request) { double('request', project: project) }
let(:deployment) { create(:deployment, :succeed, :review_app) }
let(:environment) { deployment.environment }
@@ -28,6 +28,7 @@ describe EnvironmentStatusEntity do
it { is_expected.to include(:external_url_formatted) }
it { is_expected.to include(:deployed_at) }
it { is_expected.to include(:deployed_at_formatted) }
+ it { is_expected.to include(:details) }
it { is_expected.to include(:changes) }
it { is_expected.to include(:status) }
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 02c5b817ea4..d95aaf3d104 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -218,5 +218,28 @@ describe PipelineEntity do
expect(subject[:merge_request_event_type]).to be_present
end
end
+
+ context 'when pipeline has failed builds' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let_it_be(:failed_1) { create(:ci_build, :failed, pipeline: pipeline) }
+ let_it_be(:failed_2) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ context 'when the user can retry the pipeline' do
+ it 'exposes these failed builds' do
+ allow(entity).to receive(:can_retry?).and_return(true)
+
+ expect(subject[:failed_builds].map { |b| b[:id] }).to contain_exactly(failed_1.id, failed_2.id)
+ end
+ end
+
+ context 'when the user cannot retry the pipeline' do
+ it 'is nil' do
+ allow(entity).to receive(:can_retry?).and_return(false)
+
+ expect(subject[:failed_builds]).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/git_deduplication_service_spec.rb b/spec/services/projects/git_deduplication_service_spec.rb
index 3acbc46b473..9e6279da7de 100644
--- a/spec/services/projects/git_deduplication_service_spec.rb
+++ b/spec/services/projects/git_deduplication_service_spec.rb
@@ -58,6 +58,65 @@ describe Projects::GitDeduplicationService do
service.execute
end
+
+ context 'when visibility level of the project' do
+ before do
+ allow(pool.source_project).to receive(:repository_access_level).and_return(ProjectFeature::ENABLED)
+ end
+
+ context 'is private' do
+ it 'does not call fetch' do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::PRIVATE)
+ expect(pool.object_pool).not_to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ context 'is public' do
+ it 'calls fetch' do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::PUBLIC)
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ context 'is internal' do
+ it 'calls fetch' do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::INTERNAL)
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when the repository access level' do
+ before do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ context 'is private' do
+ it 'does not call fetch' do
+ allow(pool.source_project).to receive(:repository_access_level).and_return(ProjectFeature::PRIVATE)
+
+ expect(pool.object_pool).not_to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ context 'is greater than private' do
+ it 'calls fetch' do
+ allow(pool.source_project).to receive(:repository_access_level).and_return(ProjectFeature::PUBLIC)
+
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+ end
+ end
end
it 'links the repository to the object pool' do