Read about our upcoming Code of Conduct on this issue

Commit 8e3e2153 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent edd837bfdea9
......@@ -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 @@ class Knative < ApplicationRecord
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 @@ def delete_knative_istio_metrics
[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 @@ def scheduled_actions
@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 @@ def build_change(file)
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 @@ def object_pool_params
# 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 @@ def include_details?
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 @@ def can_read_deployables?
# 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 @@ def update_project_counter_caches?(issue)
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
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