diff options
Diffstat (limited to 'app')
13 files changed, 211 insertions, 19 deletions
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 218bf41cd58..6ebbeecae1d 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -44,6 +44,7 @@ class DueDateSelect { this.$selectbox.hide(); this.$value.css('display', ''); }, + shouldPropagate: false, }); } diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 751565ad542..be70bfc7399 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -1,5 +1,6 @@ <script> import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; +import { joinPaths } from '~/lib/utils/url_utility'; import { __ } from '../../locale'; import Icon from '../../vue_shared/components/icon.vue'; import getRefMixin from '../mixins/get_ref'; @@ -102,12 +103,12 @@ export default { .filter(p => p !== '') .reduce( (acc, name, i) => { - const path = `${i > 0 ? acc[i].path : ''}/${name}`; + const path = joinPaths(i > 0 ? acc[i].path : '', encodeURIComponent(name)); return acc.concat({ name, path, - to: `/-/tree/${escape(this.ref)}${escape(path)}`, + to: `/-/tree/${joinPaths(escape(this.ref), path)}`, }); }, [ diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index c905c39bbba..b81e6a38b4c 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -91,7 +91,9 @@ export default { }, computed: { routerLinkTo() { - return this.isFolder ? { path: `/-/tree/${escape(this.ref)}/${escape(this.path)}` } : null; + return this.isFolder + ? { path: `/-/tree/${escape(this.ref)}/${encodeURIComponent(this.path)}` } + : null; }, iconName() { return `fa-${getIconName(this.type, this.path)}`; @@ -141,6 +143,7 @@ export default { <i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i> <component :is="linkComponent" + ref="link" :to="routerLinkTo" :href="url" class="str-truncated" diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index 192e410b36f..ade92cc92e0 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -27,7 +27,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) { fetchpromise = axios .get( - `${gon.relative_url_root}/${projectPath}/-/refs/${escape(ref)}/logs_tree/${escape( + `${gon.relative_url_root}/${projectPath}/-/refs/${escape(ref)}/logs_tree/${encodeURIComponent( path.replace(/^\//, ''), )}`, { diff --git a/app/assets/javascripts/vue_shared/components/pagination/constants.js b/app/assets/javascripts/vue_shared/components/pagination/constants.js index 229132c0e33..748ad178c70 100644 --- a/app/assets/javascripts/vue_shared/components/pagination/constants.js +++ b/app/assets/javascripts/vue_shared/components/pagination/constants.js @@ -3,8 +3,8 @@ import { s__ } from '~/locale'; export const PAGINATION_UI_BUTTON_LIMIT = 4; export const UI_LIMIT = 6; export const SPREAD = '...'; -export const PREV = s__('Pagination|‹ Prev'); -export const NEXT = s__('Pagination|Next ›'); +export const PREV = s__('Pagination|Prev'); +export const NEXT = s__('Pagination|Next'); export const FIRST = s__('Pagination|« First'); export const LAST = s__('Pagination|Last »'); export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page'); diff --git a/app/controllers/projects/performance_monitoring/dashboards_controller.rb b/app/controllers/projects/performance_monitoring/dashboards_controller.rb index 2d872b78096..ec5a33f5dd6 100644 --- a/app/controllers/projects/performance_monitoring/dashboards_controller.rb +++ b/app/controllers/projects/performance_monitoring/dashboards_controller.rb @@ -22,6 +22,16 @@ module Projects end end + def update + result = ::Metrics::Dashboard::UpdateDashboardService.new(project, current_user, dashboard_params.merge(file_content_params)).execute + + if result[:status] == :success + respond_update_success(result) + else + respond_error(result) + end + end + private def respond_success(result) @@ -43,6 +53,19 @@ module Projects flash[:notice] = message.html_safe end + def respond_update_success(result) + set_web_ide_link_update_notice(result.dig(:dashboard, :path)) + respond_to do |format| + format.json { render status: result.delete(:http_status), json: result } + end + end + + def set_web_ide_link_update_notice(new_dashboard_path) + web_ide_link_start = "<a href=\"#{ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path)}\">" + message = _("Your dashboard has been updated. You can %{web_ide_link_start}edit it here%{web_ide_link_end}.") % { web_ide_link_start: web_ide_link_start, web_ide_link_end: "</a>" } + flash[:notice] = message.html_safe + end + def validate_required_params! params.require(%i(branch file_name dashboard commit_message)) end @@ -54,6 +77,31 @@ module Projects def dashboard_params params.permit(%i(branch file_name dashboard commit_message)).to_h end + + def file_content_params + params.permit( + file_content: [ + :dashboard, + panel_groups: [ + :group, + :priority, + panels: [ + :type, + :title, + :y_label, + :weight, + metrics: [ + :id, + :unit, + :label, + :query, + :query_range + ] + ] + ] + ] + ) + end end end end diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 6c080582cae..55518f32316 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -90,6 +90,12 @@ module Ci end end + def needs_attributes + strong_memoize(:needs_attributes) do + needs.map { |need| need.attributes.except('id', 'build_id') } + end + end + private def validate_scheduling_type? diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 010e0018414..4b472cfdf45 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -52,6 +52,13 @@ module ReactiveCaching end end + def with_reactive_cache_set(resource, opts, &blk) + data = with_reactive_cache(resource, opts, &blk) + save_keys_in_set(resource, opts) if data + + data + end + # This method is used for debugging purposes and should not be used otherwise. def without_reactive_cache(*args, &blk) return with_reactive_cache(*args, &blk) unless Rails.env.development? @@ -65,6 +72,12 @@ module ReactiveCaching Rails.cache.delete(alive_reactive_cache_key(*args)) end + def clear_reactive_cache_set!(*args) + cache_key = full_reactive_cache_key(args) + + reactive_set_cache.clear_cache!(cache_key) + end + def exclusively_update_reactive_cache!(*args) locking_reactive_cache(*args) do key = full_reactive_cache_key(*args) @@ -86,6 +99,16 @@ module ReactiveCaching private + def save_keys_in_set(resource, opts) + cache_key = full_reactive_cache_key(resource) + + reactive_set_cache.write(cache_key, "#{cache_key}:#{opts}") + end + + def reactive_set_cache + Gitlab::ReactiveCacheSetCache.new(expires_in: reactive_cache_lifetime) + end + def refresh_reactive_cache!(*args) clear_reactive_cache!(*args) keep_alive_reactive_cache!(*args) diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index d328a609439..133850b6ab6 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -85,7 +85,7 @@ module ErrorTracking end def list_sentry_issues(opts = {}) - with_reactive_cache('list_issues', opts.stringify_keys) do |result| + with_reactive_cache_set('list_issues', opts.stringify_keys) do |result| result end end @@ -130,6 +130,10 @@ module ErrorTracking end end + def expire_issues_cache + clear_reactive_cache_set!('list_issues') + end + # http://HOST/api/0/projects/ORG/PROJECT # -> # http://HOST/ORG/PROJECT diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb index e633dc7f633..3aa2b16bc73 100644 --- a/app/services/ci/create_job_artifacts_service.rb +++ b/app/services/ci/create_job_artifacts_service.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true module Ci - class CreateJobArtifactsService + class CreateJobArtifactsService < ::BaseService ArtifactsExistError = Class.new(StandardError) + OBJECT_STORAGE_ERRORS = [ + Errno::EIO, + Google::Apis::ServerError, + Signet::RemoteServerError + ].freeze def execute(job, artifacts_file, params, metadata_file: nil) expire_in = params['expire_in'] || @@ -26,18 +31,20 @@ module Ci expire_in: expire_in) end - job.update(artifacts_expire_in: expire_in) - rescue ActiveRecord::RecordNotUnique => error - return true if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file) + if job.update(artifacts_expire_in: expire_in) + success + else + error(job.errors.messages, :bad_request) + end - Gitlab::ErrorTracking.track_exception(error, - job_id: job.id, - project_id: job.project_id, - uploading_type: params['artifact_type'] - ) + rescue ActiveRecord::RecordNotUnique => error + return success if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file) - job.errors.add(:base, 'another artifact of the same type already exists') - false + track_exception(error, job, params) + error('another artifact of the same type already exists', :bad_request) + rescue *OBJECT_STORAGE_ERRORS => error + track_exception(error, job, params) + error(error.message, :service_unavailable) end private @@ -48,5 +55,13 @@ module Ci existing_artifact.file_sha256 == artifacts_file.sha256 end + + def track_exception(error, job, params) + Gitlab::ErrorTracking.track_exception(error, + job_id: job.id, + project_id: job.project_id, + uploading_type: params['artifact_type'] + ) + end end end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 838ed789155..87f818ad497 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -5,7 +5,8 @@ module Ci CLONE_ACCESSORS = %i[pipeline project ref tag options name allow_failure stage stage_id stage_idx trigger_request yaml_variables when environment coverage_regex - description tag_list protected needs resource_group scheduling_type].freeze + description tag_list protected needs_attributes + resource_group scheduling_type].freeze def execute(build) reprocess!(build).tap do |new_build| diff --git a/app/services/error_tracking/issue_update_service.rb b/app/services/error_tracking/issue_update_service.rb index e516ac95138..b8235678d1d 100644 --- a/app/services/error_tracking/issue_update_service.rb +++ b/app/services/error_tracking/issue_update_service.rb @@ -11,6 +11,7 @@ module ErrorTracking ) compose_response(response) do + project_error_tracking_setting.expire_issues_cache response[:closed_issue_iid] = update_related_issue&.iid end end diff --git a/app/services/metrics/dashboard/update_dashboard_service.rb b/app/services/metrics/dashboard/update_dashboard_service.rb new file mode 100644 index 00000000000..316563ecbe7 --- /dev/null +++ b/app/services/metrics/dashboard/update_dashboard_service.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# Updates the content of a specified dashboard in .yml file inside `.gitlab/dashboards` +module Metrics + module Dashboard + class UpdateDashboardService < ::BaseService + ALLOWED_FILE_TYPE = '.yml' + USER_DASHBOARDS_DIR = ::Metrics::Dashboard::ProjectDashboardService::DASHBOARD_ROOT + + def execute + catch(:error) do + throw(:error, error(_(%q(You can't commit to this project)), :forbidden)) unless push_authorized? + + result = ::Files::UpdateService.new(project, current_user, dashboard_attrs).execute + throw(:error, result.merge(http_status: :bad_request)) unless result[:status] == :success + + success(result.merge(http_status: :created, dashboard: dashboard_details)) + end + end + + private + + def dashboard_attrs + { + commit_message: params[:commit_message], + file_path: update_dashboard_path, + file_content: update_dashboard_content, + encoding: 'text', + branch_name: branch, + start_branch: repository.branch_exists?(branch) ? branch : project.default_branch + } + end + + def dashboard_details + { + path: update_dashboard_path, + display_name: ::Metrics::Dashboard::ProjectDashboardService.name_for_path(update_dashboard_path), + default: false, + system_dashboard: false + } + end + + def push_authorized? + Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) + end + + def branch + @branch ||= begin + throw(:error, error(_('There was an error updating the dashboard, branch name is invalid.'), :bad_request)) unless valid_branch_name? + throw(:error, error(_('There was an error updating the dashboard, branch named: %{branch} already exists.') % { branch: params[:branch] }, :bad_request)) unless new_or_default_branch? + + params[:branch] + end + end + + def new_or_default_branch? + !repository.branch_exists?(params[:branch]) || project.default_branch == params[:branch] + end + + def valid_branch_name? + Gitlab::GitRefValidator.validate(params[:branch]) + end + + def update_dashboard_path + File.join(USER_DASHBOARDS_DIR, file_name) + end + + def target_file_type_valid? + File.extname(params[:file_name]) == ALLOWED_FILE_TYPE + end + + def file_name + @file_name ||= begin + throw(:error, error(_('The file name should have a .yml extension'), :bad_request)) unless target_file_type_valid? + + File.basename(CGI.unescape(params[:file_name])) + end + end + + def update_dashboard_content + ::PerformanceMonitoring::PrometheusDashboard.from_json(params[:file_content]).to_yaml + end + + def repository + @repository ||= project.repository + end + end + end +end |