diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-16 12:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-16 12:09:36 +0300 |
commit | a57cec4bb89b61d210d4e413571b1d85d76179f6 (patch) | |
tree | c7868456df33c1849a7ff5037351bfd014a80584 /app | |
parent | 1de9854406851f7f1b599dd3311189f16db422f3 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
23 files changed, 291 insertions, 58 deletions
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 0b920ba8e7a..cc2cf787a8f 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -16,6 +16,50 @@ function decodeUrlParameter(val) { return decodeURIComponent(val.replace(/\+/g, '%20')); } +/** + * Safely encodes a string to be used as a path + * + * Note: This function DOES encode typical URL parts like ?, =, &, #, and + + * If you need to use search parameters or URL fragments, they should be + * added AFTER calling this function, not before. + * + * Usecase: An image filename is stored verbatim, and you need to load it in + * the browser. + * + * Example: /some_path/file #1.jpg ==> /some_path/file%20%231.jpg + * Example: /some-path/file! Final!.jpg ==> /some-path/file%21%20Final%21.jpg + * + * Essentially, if a character *could* present a problem in a URL, it's escaped + * to the hexadecimal representation instead. This means it's a bit more + * aggressive than encodeURIComponent: that built-in function doesn't + * encode some characters that *could* be problematic, so this function + * adds them (#, !, ~, *, ', (, and )). + * Additionally, encodeURIComponent *does* encode `/`, but we want safer + * URLs, not non-functional URLs, so this function DEcodes slashes ('%2F'). + * + * @param {String} potentiallyUnsafePath + * @returns {String} + */ +export function encodeSaferUrl(potentiallyUnsafePath) { + const unencode = ['%2F']; + const encode = ['#', '!', '~', '\\*', "'", '\\(', '\\)']; + let saferPath = encodeURIComponent(potentiallyUnsafePath); + + unencode.forEach((code) => { + saferPath = saferPath.replace(new RegExp(code, 'g'), decodeURIComponent(code)); + }); + encode.forEach((code) => { + const encodedValue = code + .codePointAt(code.length - 1) + .toString(16) + .toUpperCase(); + + saferPath = saferPath.replace(new RegExp(code, 'g'), `%${encodedValue}`); + }); + + return saferPath; +} + export function cleanLeadingSeparator(path) { return path.replace(PATH_SEPARATOR_LEADING_REGEX, ''); } diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue new file mode 100644 index 00000000000..0393793bfe1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue @@ -0,0 +1,27 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { ACTION_TEXT } from '../constants'; + +export default { + components: { GlLink }, + i18n: { + ACTION_TEXT, + }, + props: { + actions: { + required: true, + type: Object, + }, + }, +}; +</script> +<template> + <ul> + <li v-for="(value, action) in actions" :key="action"> + <span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span> + <span v-else> + <gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link> + </span> + </li> + </ul> +</template> diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue new file mode 100644 index 00000000000..0393793bfe1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue @@ -0,0 +1,27 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { ACTION_TEXT } from '../constants'; + +export default { + components: { GlLink }, + i18n: { + ACTION_TEXT, + }, + props: { + actions: { + required: true, + type: Object, + }, + }, +}; +</script> +<template> + <ul> + <li v-for="(value, action) in actions" :key="action"> + <span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span> + <span v-else> + <gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link> + </span> + </li> + </ul> +</template> diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js new file mode 100644 index 00000000000..8606af29785 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js @@ -0,0 +1,12 @@ +import { s__ } from '~/locale'; + +export const ACTION_TEXT = { + gitWrite: s__('LearnGitLab|Create a repository'), + userAdded: s__('LearnGitLab|Invite your colleagues'), + pipelineCreated: s__('LearnGitLab|Set-up CI/CD'), + trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'), + codeOwnersEnabled: s__('LearnGitLab|Add code owners'), + requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'), + mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'), + securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'), +}; diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js new file mode 100644 index 00000000000..c4dec89b984 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import LearnGitlabA from '../components/learn_gitlab_a.vue'; +import LearnGitlabB from '../components/learn_gitlab_b.vue'; + +function initLearnGitlab() { + const el = document.getElementById('js-learn-gitlab-app'); + + if (!el) { + return false; + } + + const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); + + const { learnGitlabA } = gon.experiments; + + return new Vue({ + el, + render(createElement) { + return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } }); + }, + }); +} + +initLearnGitlab(); diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js index 209ae9927cf..a5e53ee3927 100644 --- a/app/assets/javascripts/projects/settings/access_dropdown.js +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -11,7 +11,6 @@ export default class AccessDropdown { const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options; this.options = options; this.hasLicense = hasLicense; - this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches; this.groups = []; this.accessLevel = accessLevel; this.accessLevelsData = accessLevelsData.roles; @@ -330,11 +329,7 @@ export default class AccessDropdown { ); }) .catch(() => { - if (this.deployKeysOnProtectedBranchesEnabled) { - createFlash({ message: __('Failed to load groups, users and deploy keys.') }); - } else { - createFlash({ message: __('Failed to load groups & users.') }); - } + createFlash({ message: __('Failed to load groups, users and deploy keys.') }); }); } else { this.getDeployKeys(query) @@ -445,35 +440,33 @@ export default class AccessDropdown { } } - if (this.deployKeysOnProtectedBranchesEnabled) { - const deployKeys = deployKeysResponse.map((response) => { - const { - id, - fingerprint, - title, - owner: { avatar_url, name, username }, - } = response; - - const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`; - - return { - id, - title: title.concat(' ', shortFingerprint), - avatar_url, - fullname: name, - username, - type: LEVEL_TYPES.DEPLOY_KEY, - }; - }); + const deployKeys = deployKeysResponse.map((response) => { + const { + id, + fingerprint, + title, + owner: { avatar_url, name, username }, + } = response; + + const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`; + + return { + id, + title: title.concat(' ', shortFingerprint), + avatar_url, + fullname: name, + username, + type: LEVEL_TYPES.DEPLOY_KEY, + }; + }); - if (this.accessLevel === ACCESS_LEVELS.PUSH) { - if (deployKeys.length) { - consolidatedData = consolidatedData.concat( - [{ type: 'divider' }], - [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], - deployKeys, - ); - } + if (this.accessLevel === ACCESS_LEVELS.PUSH) { + if (deployKeys.length) { + consolidatedData = consolidatedData.concat( + [{ type: 'divider' }], + [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], + deployKeys, + ); } } @@ -501,19 +494,15 @@ export default class AccessDropdown { } getDeployKeys(query) { - if (this.deployKeysOnProtectedBranchesEnabled) { - return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), { - params: { - search: query, - per_page: 20, - active: true, - project_id: gon.current_project_id, - push_code: true, - }, - }); - } - - return Promise.resolve({ data: [] }); + return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), { + params: { + search: query, + per_page: 20, + active: true, + project_id: gon.current_project_id, + push_code: true, + }, + }); } buildUrl(urlRoot, url) { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index 9ece6a52805..a49eb7fd611 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -1,6 +1,7 @@ <script> import { throttle } from 'lodash'; -import { numberToHumanSize } from '../../../../lib/utils/number_utils'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { encodeSaferUrl } from '~/lib/utils/url_utility'; export default { props: { @@ -43,6 +44,9 @@ export default { hasDimensions() { return this.width && this.height; }, + safePath() { + return encodeSaferUrl(this.path); + }, }, beforeDestroy() { window.removeEventListener('resize', this.resizeThrottled, false); @@ -84,7 +88,7 @@ export default { <template> <div data-testid="image-viewer" data-qa-selector="image_viewer_container"> <div :class="innerCssClasses" class="position-relative"> - <img ref="contentImg" :src="path" @load="onImgLoad" /> + <img ref="contentImg" :src="safePath" @load="onImgLoad" /> <slot name="image-overlay" :rendered-width="renderedWidth" diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index d8bc1320db4..6e6686f225c 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -66,7 +66,11 @@ module BoardsResponses end def respond_with_board - respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop:disable Gitlab/ModuleWithInstanceVariables + return render_404 unless @board + + respond_with(@board) + # rubocop:enable Gitlab/ModuleWithInstanceVariables end def serialize_as_json(resource) diff --git a/app/controllers/projects/learn_gitlab_controller.rb b/app/controllers/projects/learn_gitlab_controller.rb new file mode 100644 index 00000000000..162ba9bd5cb --- /dev/null +++ b/app/controllers/projects/learn_gitlab_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Projects::LearnGitlabController < Projects::ApplicationController + before_action :authenticate_user! + before_action :check_experiment_enabled? + + feature_category :users + + def index + push_frontend_experiment(:learn_gitlab_a, subject: current_user) + push_frontend_experiment(:learn_gitlab_b, subject: current_user) + end + + private + + def check_experiment_enabled? + return access_denied! unless helpers.learn_gitlab_experiment_enabled?(project) + end +end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index dd50ab1bc7a..821560e32ba 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -7,7 +7,6 @@ module Projects before_action :define_variables, only: [:create_deploy_token] before_action do push_frontend_feature_flag(:ajax_new_deploy_token, @project) - push_frontend_feature_flag(:deploy_keys_on_protected_branches, @project) end feature_category :source_code_management, [:show, :cleanup] diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb new file mode 100644 index 00000000000..e72a9c83fc9 --- /dev/null +++ b/app/helpers/learn_gitlab_helper.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module LearnGitlabHelper + def learn_gitlab_experiment_enabled?(project) + return false unless current_user + return false unless experiment_enabled_for_user? + + learn_gitlab_onboarding_available?(project) + end + + def onboarding_actions_data(project) + attributes = onboarding_progress(project).attributes.symbolize_keys + + action_urls.map do |action, url| + [ + action, + url: url, + completed: attributes[OnboardingProgress.column_name(action)].present? + ] + end.to_h + end + + private + + ACTION_ISSUE_IDS = { + git_write: 2, + pipeline_created: 4, + merge_request_created: 6, + user_added: 7, + trial_started: 13, + required_mr_approvals_enabled: 15, + code_owners_enabled: 16 + }.freeze + + ACTION_DOC_URLS = { + security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports' + }.freeze + + def action_urls + ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS) + end + + def learn_gitlab_project + @learn_gitlab_project ||= LearnGitlab.new(current_user).project + end + + def onboarding_progress(project) + OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord + end + + def experiment_enabled_for_user? + Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) || + Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user) + end + + def learn_gitlab_onboarding_available?(project) + OnboardingProgress.onboarding?(project.namespace) && + LearnGitlab.new(current_user).available? + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a2e9952f350..f5cd89d96b4 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -433,6 +433,8 @@ module ProjectsHelper nav_tabs += package_nav_tabs(project, current_user) + nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project) + nav_tabs end # rubocop:enable Metrics/CyclomaticComplexity diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 8560826928a..2a051233de2 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class CertManager < ApplicationRecord VERSION = 'v0.10.1' CRD_VERSION = '0.10' diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb index 2b1a86706a4..07378b4e8dc 100644 --- a/app/models/clusters/applications/crossplane.rb +++ b/app/models/clusters/applications/crossplane.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Crossplane < ApplicationRecord VERSION = '0.4.1' diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 36324e7f3e0..e7d4d737b8e 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Ingress < ApplicationRecord VERSION = '1.40.2' INGRESS_CONTAINER_NAME = 'nginx-ingress-controller' diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ff907c6847f..8d7d9c20bfa 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -4,6 +4,8 @@ require 'securerandom' module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Jupyter < ApplicationRecord VERSION = '0.9.0' diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 7c131e031c1..6867d7b6934 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -2,6 +2,8 @@ module Clusters module Applications + # DEPRECATED for removal in %14.0 + # See https://gitlab.com/groups/gitlab-org/-/epics/4280 class Knative < ApplicationRecord VERSION = '0.10.0' REPOSITORY = 'https://charts.gitlab.io' diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb index 38a9489a3ad..8a444f8934e 100644 --- a/app/models/onboarding_progress.rb +++ b/app/models/onboarding_progress.rb @@ -47,6 +47,10 @@ class OnboardingProgress < ApplicationRecord safe_find_or_create_by(namespace: namespace) end + def onboarding?(namespace) + where(namespace: namespace).any? + end + def register(namespace, action) return unless root_namespace?(namespace) && ACTIONS.include?(action) diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index f28440f2444..ea51dca8a42 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -19,7 +19,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord end def check_access(user) - if Feature.enabled?(:deploy_keys_on_protected_branches, project) && user && deploy_key.present? + if user && deploy_key.present? return true if user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user) end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 2576caefdd4..8bb009bfd17 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -33,6 +33,13 @@ = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do %span= _('Releases') + - if project_nav_tab? :learn_gitlab + = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do + = link_to project_learn_gitlab_path(@project) do + .nav-icon-container + = sprite_icon('home') + %span.nav-item-name + = _('Learn GitLab') - if project_nav_tab? :files = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do diff --git a/app/views/projects/learn_gitlab/index.html.haml b/app/views/projects/learn_gitlab/index.html.haml new file mode 100644 index 00000000000..d5fdbc10eb4 --- /dev/null +++ b/app/views/projects/learn_gitlab/index.html.haml @@ -0,0 +1,4 @@ +- breadcrumb_title _("Learn GitLab") +- page_title _("Learn GitLab") + +#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } } diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index 33be875d9a6..20cd45be6da 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -1,5 +1,3 @@ -- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, @project) ? 'js-multiselect' : '' - - content_for :merge_access_levels do .merge_access_levels-container = dropdown_tag('Select', @@ -9,7 +7,7 @@ - content_for :push_access_levels do .push_access_levels-container = dropdown_tag('Select', - options: { toggle_class: "js-allowed-to-push qa-allowed-to-push-select #{select_mode_for_dropdown} wide", + options: { toggle_class: "js-allowed-to-push qa-allowed-to-push-select js-multiselect wide", dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown rspec-allowed-to-push-dropdown capitalize-header', data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) diff --git a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml index cb954c20b48..d1b32df7139 100644 --- a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml +++ b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml @@ -1,5 +1,3 @@ -- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, protected_branch.project) ? 'js-multiselect' : '' - - merge_access_levels = protected_branch.merge_access_levels.for_role - push_access_levels = protected_branch.push_access_levels.for_role @@ -25,7 +23,7 @@ %td.push_access_levels-container = hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level = dropdown_tag( (push_access_levels.first&.humanize || 'Select') , - options: { toggle_class: "js-allowed-to-push #{select_mode_for_dropdown}", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', + options: { toggle_class: "js-allowed-to-push js-multiselect", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }}) - if user_push_access_levels.any? %p.small |