Commit 6b13a226 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 2ac93cb8
......@@ -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">
......
......@@ -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>
......
<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>
......
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 () => {};
......@@ -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: '' };
......
/* 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', {
......
......@@ -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);
}
......
......@@ -17,6 +17,7 @@ const handleUserPopoverMouseOut = event => {
renderedPopover.$destroy();
renderedPopover = null;
}
target.removeAttribute('aria-describedby');
};
/**
......
......@@ -12,6 +12,12 @@
}
}
.file-title-name {
&.limited-width {
max-width: 80%;
}
}
.line_content.old::before {
content: none !important;
}
......
......@@ -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
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......@@ -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
......
# 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
......
......@@ -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
......
......@@ -36,3 +36,5 @@ module Issues
end
end
end
Issues::BaseService.prepend_if_ee('EE::Issues::BaseService')
......@@ -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
......
---
title: Make internal projects poolable
merge_request: 19295
author: briankabiro
type: changed
---
title: Exposed deployment build manual actions for merge request page
merge_request: 20615
author:
type: changed
---
title: Update deploy instances color scheme
merge_request: 20890
author:
type: changed
---
title: Handle empty stacktrace and entries with no code
merge_request: 20458
author:
type: fixed
---
title: Fix opening Sentry error details in new tab
merge_request: 20611
author:
type: fixed
---
title: removes references of BoardService
merge_request: 20875
author: nuwe1
type: other
---
title: removes references of BoardService
merge_request: 20876
author: nuwe1
type: other
---
title: Resets aria-describedby on mouseleave
merge_request: 20092
author: carolcarvalhosa
type: fixed
---
title: Added Total/Frontend metrics to the performance bar
merge_request: 20725
author:
type: added
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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.
......
......@@ -15,3 +15,5 @@ module API
end
end
end
API::Helpers::CommonHelpers.prepend_if_ee('EE::API::Helpers::CommonHelpers')
......@@ -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 ""
......
......@@ -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
......
......@@ -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);
});
});
......
[
{
"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
......@@ -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}`);
});
});
});
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]);
});
});
......
......@@ -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);
});
});
});
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);
});
......
......@@ -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({
......
......@@ -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
......
......@@ -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
......@@ -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) }
......
......@@ -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
......
......@@ -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) }
......
......@@ -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
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment