diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue index a17378edafd..b0f19e5b585 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue @@ -214,7 +214,7 @@ export default { {{ s__('JiraService|Enable Jira transitions') }} @@ -232,7 +232,7 @@ export default { name="service[jira_issue_transition_automatic]" :value="issueTransitionOption.value" :disabled="isInheriting" - :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`" + :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`" > {{ issueTransitionOption.label }} diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 955f25a6f26..a509828815a 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -254,6 +254,37 @@ export const timeIntervalInWords = (intervalInSeconds) => { : secondsText; }; +/** + * Similar to `timeIntervalInWords`, but rounds the return value + * to 1/10th of the largest time unit. For example: + * + * 30 => 30 seconds + * 90 => 1.5 minutes + * 7200 => 2 hours + * 86400 => 1 day + * ... etc. + * + * The largest supported unit is "days". + * + * @param {Number} intervalInSeconds The time interval in seconds + * @returns {String} A humanized description of the time interval + */ +export const humanizeTimeInterval = (intervalInSeconds) => { + if (intervalInSeconds < 60 /* = 1 minute */) { + const seconds = Math.round(intervalInSeconds * 10) / 10; + return n__('%d second', '%d seconds', seconds); + } else if (intervalInSeconds < 3600 /* = 1 hour */) { + const minutes = Math.round(intervalInSeconds / 6) / 10; + return n__('%d minute', '%d minutes', minutes); + } else if (intervalInSeconds < 86400 /* = 1 day */) { + const hours = Math.round(intervalInSeconds / 360) / 10; + return n__('%d hour', '%d hours', hours); + } + + const days = Math.round(intervalInSeconds / 8640) / 10; + return n__('%d day', '%d days', days); +}; + export const dateInWords = (date, abbreviated = false, hideYear = false) => { if (!date) return date; diff --git a/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue b/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue new file mode 100644 index 00000000000..8de6e910bb6 --- /dev/null +++ b/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue @@ -0,0 +1,77 @@ + + + diff --git a/app/assets/javascripts/namespaces/cascading_settings/index.js b/app/assets/javascripts/namespaces/cascading_settings/index.js new file mode 100644 index 00000000000..3e44d1e9e2d --- /dev/null +++ b/app/assets/javascripts/namespaces/cascading_settings/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import LockPopovers from './components/lock_popovers.vue'; + +export const initCascadingSettingsLockPopovers = () => { + const el = document.querySelector('.js-cascading-settings-lock-popovers'); + + if (!el) return false; + + return new Vue({ + el, + render(createElement) { + return createElement(LockPopovers); + }, + }); +}; diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 176d2406751..49b9822795c 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import TransferDropdown from '~/groups/transfer_dropdown'; import groupsSelect from '~/groups_select'; +import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import projectSelect from '~/project_select'; import initSearchSettings from '~/search_settings'; @@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => { projectSelect(); initSearchSettings(); + initCascadingSettingsLockPopovers(); return new TransferDropdown(); }); diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 6ff7b356438..63048777724 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -54,6 +54,7 @@ export default { data() { return { hoveredJobName: '', + hoveredSourceJobName: '', highlightedJobs: [], measurements: { width: 0, @@ -93,6 +94,9 @@ export default { shouldHideLinks() { return this.isStageView; }, + shouldShowStageName() { + return !this.isStageView; + }, // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( @@ -148,6 +152,9 @@ export default { setJob(jobName) { this.hoveredJobName = jobName; }, + setSourceJob(jobName) { + this.hoveredSourceJobName = jobName; + }, slidePipelineContainer() { this.$refs.mainPipelineContainer.scrollBy({ left: ONE_COL_WIDTH, @@ -204,11 +211,13 @@ export default { -import { GlTooltipDirective } from '@gitlab/ui'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import JobItem from './job_item.vue'; @@ -11,12 +9,8 @@ import JobItem from './job_item.vue'; * */ export default { - directives: { - GlTooltip: GlTooltipDirective, - }, components: { JobItem, - CiIcon, }, props: { group: { @@ -28,6 +22,11 @@ export default { required: false, default: -1, }, + stageName: { + type: String, + required: false, + default: '', + }, }, computed: { computedJobId() { @@ -51,22 +50,21 @@ export default { - - - - - - -
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js index 6da141cb19a..3ac3a3a3611 100644 --- a/app/assets/javascripts/whats_new/index.js +++ b/app/assets/javascripts/whats_new/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { mapState } from 'vuex'; import App from './components/app.vue'; import store from './store'; -import { getStorageKey, setNotification } from './utils/notification'; +import { getVersionDigest, setNotification } from './utils/notification'; let whatsNewApp; @@ -27,9 +27,7 @@ export default (el) => { render(createElement) { return createElement('app', { props: { - storageKey: getStorageKey(el), - versions: JSON.parse(el.getAttribute('data-versions')), - gitlabDotCom: el.getAttribute('data-gitlab-dot-com'), + versionDigest: getVersionDigest(el), }, }); }, diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js index 4b3cfa55977..1dc92ea2606 100644 --- a/app/assets/javascripts/whats_new/store/actions.js +++ b/app/assets/javascripts/whats_new/store/actions.js @@ -1,19 +1,20 @@ import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { STORAGE_KEY } from '../utils/notification'; import * as types from './mutation_types'; export default { closeDrawer({ commit }) { commit(types.CLOSE_DRAWER); }, - openDrawer({ commit }, storageKey) { + openDrawer({ commit }, versionDigest) { commit(types.OPEN_DRAWER); - if (storageKey) { - localStorage.setItem(storageKey, JSON.stringify(false)); + if (versionDigest) { + localStorage.setItem(STORAGE_KEY, versionDigest); } }, - fetchItems({ commit, state }, { page, version } = { page: null, version: null }) { + fetchItems({ commit, state }, { page } = { page: null }) { if (state.fetching) { return false; } @@ -24,7 +25,6 @@ export default { .get('/-/whats_new', { params: { page, - version, }, }) .then(({ data, headers }) => { diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js index 52ca8058d1c..3d4326c4b3a 100644 --- a/app/assets/javascripts/whats_new/utils/notification.js +++ b/app/assets/javascripts/whats_new/utils/notification.js @@ -1,11 +1,18 @@ -export const getStorageKey = (appEl) => appEl.getAttribute('data-storage-key'); +export const STORAGE_KEY = 'display-whats-new-notification'; + +export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest'); export const setNotification = (appEl) => { - const storageKey = getStorageKey(appEl); + const versionDigest = getVersionDigest(appEl); const notificationEl = document.querySelector('.header-help'); let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count'); - if (JSON.parse(localStorage.getItem(storageKey)) === false) { + const legacyStorageKey = 'display-whats-new-notification-13.10'; + const localStoragePairs = [ + [legacyStorageKey, false], + [STORAGE_KEY, versionDigest], + ]; + if (localStoragePairs.some((pair) => localStorage.getItem(pair[0]) === pair[1].toString())) { notificationEl.classList.remove('with-notifications'); if (notificationCountEl) { notificationCountEl.parentElement.removeChild(notificationCountEl); diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss index a005daef2e4..2f3cf889549 100644 --- a/app/assets/stylesheets/page_bundles/pipeline.scss +++ b/app/assets/stylesheets/page_bundles/pipeline.scss @@ -148,7 +148,19 @@ } .gl-build-content { - @include build-content(); + display: inline-block; + padding: 8px 10px 9px; + width: 100%; + border: 1px solid var(--border-color, $border-color); + border-radius: 30px; + background-color: var(--white, $white); + + &:hover, + &:focus { + background-color: var(--gray-50, $gray-50); + border: 1px solid $dropdown-toggle-active-border-color; + color: var(--gl-text-color, $gl-text-color); + } } .gl-ci-action-icon-container { diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index e8e681ce649..7bfcda67aa2 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -5,19 +5,23 @@ module CreatesCommit include Gitlab::Utils::StrongMemoize # rubocop:disable Gitlab/ModuleWithInstanceVariables - def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) - if user_access(@project).can_push_to_branch?(branch_name_or_ref) - @project_to_commit_into = @project + def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil, target_project: nil) + target_project ||= @project + + if user_access(target_project).can_push_to_branch?(branch_name_or_ref) + @project_to_commit_into = target_project @branch_name ||= @ref else - @project_to_commit_into = current_user.fork_of(@project) + @project_to_commit_into = current_user.fork_of(target_project) @branch_name ||= @project_to_commit_into.repository.next_branch('patch') end @start_branch ||= @ref || @branch_name + start_project = Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml) ? @project_to_commit_into : @project + commit_params = @commit_params.merge( - start_project: @project, + start_project: start_project, start_branch: @start_branch, branch_name: @branch_name ) @@ -27,7 +31,7 @@ module CreatesCommit if result[:status] == :success update_flash_notice(success_notice) - success_path = final_success_path(success_path) + success_path = final_success_path(success_path, target_project) respond_to do |format| format.html { redirect_to success_path } @@ -79,9 +83,9 @@ module CreatesCommit end end - def final_success_path(success_path) + def final_success_path(success_path, target_project) if create_merge_request? - merge_request_exists? ? existing_merge_request_path : new_merge_request_path + merge_request_exists? ? existing_merge_request_path : new_merge_request_path(target_project) else success_path = success_path.call if success_path.respond_to?(:call) @@ -90,12 +94,12 @@ module CreatesCommit end # rubocop:disable Gitlab/ModuleWithInstanceVariables - def new_merge_request_path + def new_merge_request_path(target_project) project_new_merge_request_path( @project_to_commit_into, merge_request: { source_project_id: @project_to_commit_into.id, - target_project_id: @project.id, + target_project_id: target_project.id, source_branch: @branch_name, target_branch: @start_branch } diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb index 826fae834fa..4ea07c814ef 100644 --- a/app/controllers/concerns/renders_commits.rb +++ b/app/controllers/concerns/renders_commits.rb @@ -17,12 +17,13 @@ module RendersCommits def set_commits_for_rendering(commits, commits_count: nil) @total_commit_count = commits_count || commits.size limited, @hidden_commit_count = limited_commits(commits, @total_commit_count) - commits.each(&:lazy_author) # preload authors prepare_commits_for_rendering(limited) end # rubocop: enable Gitlab/ModuleWithInstanceVariables def prepare_commits_for_rendering(commits) + commits.each(&:lazy_author) # preload commits' authors + Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables commits diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index fdf66340cbb..1e65974a3cd 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -114,7 +114,7 @@ class Projects::CommitController < Projects::ApplicationController @branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", - success_path: -> { successful_change_path }, failure_path: failed_change_path) + success_path: -> { successful_change_path(@project) }, failure_path: failed_change_path) end def cherry_pick @@ -122,10 +122,15 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @start_branch.blank? + target_project = find_cherry_pick_target_project + return render_404 unless target_project + @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked into #{@branch_name}.", - success_path: -> { successful_change_path }, failure_path: failed_change_path) + success_path: -> { successful_change_path(target_project) }, + failure_path: failed_change_path, + target_project: target_project) end private @@ -138,8 +143,8 @@ class Projects::CommitController < Projects::ApplicationController params[:create_merge_request].present? || !can?(current_user, :push_code, @project) end - def successful_change_path - referenced_merge_request_url || project_commits_url(@project, @branch_name) + def successful_change_path(target_project) + referenced_merge_request_url || project_commits_url(target_project, @branch_name) end def failed_change_path @@ -218,4 +223,14 @@ class Projects::CommitController < Projects::ApplicationController @start_branch = params[:start_branch] @commit_params = { commit: @commit } end + + def find_cherry_pick_target_project + return @project if params[:target_project_id].blank? + return @project unless Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml) + + MergeRequestTargetProjectFinder + .new(current_user: current_user, source_project: @project, project_feature: :repository) + .execute + .find_by_id(params[:target_project_id]) + end end diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb index 12a52f30bd0..e24b0bbc7bb 100644 --- a/app/controllers/whats_new_controller.rb +++ b/app/controllers/whats_new_controller.rb @@ -5,7 +5,7 @@ class WhatsNewController < ApplicationController skip_before_action :authenticate_user! - before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? } + before_action :check_valid_page_param, :set_pagination_headers feature_category :navigation @@ -29,19 +29,11 @@ class WhatsNewController < ApplicationController def highlights strong_memoize(:highlights) do - if has_version_param? - ReleaseHighlight.for_version(version: params[:version]) - else - ReleaseHighlight.paginated(page: current_page) - end + ReleaseHighlight.paginated(page: current_page) end end def set_pagination_headers response.set_header('X-Next-Page', highlights.next_page) end - - def has_version_param? - params[:version].present? - end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 652ba9950bc..e7a81eb5629 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -139,7 +139,7 @@ module CommitsHelper def cherry_pick_projects_data(project) return [] unless Feature.enabled?(:pick_into_project, project, default_enabled: :yaml) - target_projects(project).map do |project| + [project, project.forked_from_project].compact.map do |project| { id: project.id.to_s, name: project.full_path, diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 8cf5cd49322..a4521541bf9 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -56,6 +56,33 @@ module NamespacesHelper namespaces_options(selected, **options) end + def cascading_namespace_settings_enabled? + NamespaceSetting.cascading_settings_feature_enabled? + end + + def cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) + locked_by_ancestor = group.namespace_settings.public_send("#{attribute}_locked_by_ancestor?") # rubocop:disable GitlabSecurity/PublicSend + + popover_data = { + locked_by_application_setting: group.namespace_settings.public_send("#{attribute}_locked_by_application_setting?"), # rubocop:disable GitlabSecurity/PublicSend + locked_by_ancestor: locked_by_ancestor + } + + if locked_by_ancestor + ancestor_namespace = group.namespace_settings.public_send("#{attribute}_locked_ancestor").namespace # rubocop:disable GitlabSecurity/PublicSend + + popover_data[:ancestor_namespace] = { + full_name: ancestor_namespace.full_name, + path: settings_path_helper.call(ancestor_namespace) + } + end + + { + popover_data: popover_data.to_json, + testid: 'cascading-settings-lock-icon' + } + end + private # Many importers create a temporary Group, so use the real diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb index bbf5bde5904..23ed2fc987c 100644 --- a/app/helpers/whats_new_helper.rb +++ b/app/helpers/whats_new_helper.rb @@ -5,15 +5,7 @@ module WhatsNewHelper ReleaseHighlight.most_recent_item_count end - def whats_new_storage_key - most_recent_version = ReleaseHighlight.versions&.first - - return unless most_recent_version - - ['display-whats-new-notification', most_recent_version].join('-') - end - - def whats_new_versions - ReleaseHighlight.versions + def whats_new_version_digest + ReleaseHighlight.most_recent_version_digest end end diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb index 1efba6380e9..98d9899a349 100644 --- a/app/models/release_highlight.rb +++ b/app/models/release_highlight.rb @@ -3,17 +3,6 @@ class ReleaseHighlight CACHE_DURATION = 1.hour FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') - RELEASE_VERSIONS_IN_A_YEAR = 12 - - def self.for_version(version:) - index = self.versions.index(version) - - return if index.nil? - - page = index + 1 - - self.paginated(page: page) - end def self.paginated(page: 1) key = self.cache_key("items:page-#{page}") @@ -82,15 +71,15 @@ class ReleaseHighlight end end - def self.versions - key = self.cache_key('versions') + def self.most_recent_version_digest + key = self.cache_key('most_recent_version_digest') Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do - versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path| - /\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".") - end + version = self.paginated&.items&.first&.[]('release')&.to_s - versions.uniq + next if version.nil? + + Digest::SHA256.hexdigest(version) end end diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index a14d342bc14..9dce33bf037 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -2,10 +2,9 @@ - page_title _("Broadcast Messages") %h3.page-title - Broadcast Messages + = _('Broadcast Messages') %p.light - Broadcast messages are displayed for every user and can be used to notify - users about scheduled maintenance, recent upgrades and more. + = _('Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.') = render 'form' @@ -15,12 +14,12 @@ %table.table.table-responsive %thead %tr - %th Status - %th Preview - %th Starts - %th Ends - %th Target Path - %th Type + %th= _('Status') + %th= _('Preview') + %th= _('Starts') + %th= _('Ends') + %th= _(' Target Path') + %th= _(' Type') %th   %tbody - @broadcast_messages.each do |message| @@ -38,7 +37,7 @@ %td = message.broadcast_type.capitalize %td.gl-white-space-nowrap.gl-display-flex - = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-icon gl-button' - = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2' + = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button' + = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2' = paginate @broadcast_messages, theme: 'gitlab' diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 57c0801074b..90a49e4bbe3 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -18,11 +18,11 @@ = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do = _("Your projects") - %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count) + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count) = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do = _("Starred projects") - %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count) + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count) = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, data: {placement: 'right'} do = _("Explore projects") diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml index 27ef586d90f..9d469ff6e7b 100644 --- a/app/views/devise/mailer/_confirmation_instructions_account.html.haml +++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml @@ -2,15 +2,15 @@ - if @resource.unconfirmed_email.present? || !@resource.created_recently? #content = email_default_heading(@resource.unconfirmed_email || @resource.email) - %p Click the link below to confirm your email address. + %p= _('Click the link below to confirm your email address.') #cta - = link_to 'Confirm your email address', confirmation_link + = link_to _('Confirm your email address'), confirmation_link - else #content - if Gitlab.com? - = email_default_heading('Thanks for signing up to GitLab!') + = email_default_heading(_('Thanks for signing up to GitLab!')) - else - = email_default_heading("Welcome, #{@resource.name}!") - %p To get started, click the link below to confirm your account. + = email_default_heading(_("Welcome, %{name}!") % { name: @resource.name }) + %p= _("To get started, click the link below to confirm your account.") #cta - = link_to 'Confirm your account', confirmation_link + = link_to _('Confirm your account'), confirmation_link diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 71e61436410..ee9eed7e6f6 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -3,6 +3,7 @@ - @content_class = "limit-container-width" unless fluid_layout - expanded = expanded_by_default? += render 'shared/namespaces/cascading_settings/lock_popovers' %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') } .settings-header diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 3e29e42126e..fcfe70bd694 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -8,27 +8,27 @@ = render 'shared/allow_request_access', form: f .form-group.gl-mb-3 - .form-check - = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' - = f.label :share_with_group_lock, class: 'form-check-label' do - %span.d-block + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input' + = f.label :share_with_group_lock, class: 'custom-control-label' do + %span - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %span.js-descr.text-muted= share_with_group_lock_help_text(@group) + %p.js-descr.help-text= share_with_group_lock_help_text(@group) .form-group.gl-mb-3 - .form-check - = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input' - = f.label :emails_disabled, class: 'form-check-label' do - %span.d-block= s_('GroupSettings|Disable email notifications') - %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input' + = f.label :emails_disabled, class: 'custom-control-label' do + %span= s_('GroupSettings|Disable email notifications') + %p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') .form-group.gl-mb-3 - .form-check - = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input' - = f.label :mentions_disabled, class: 'form-check-label' do - %span.d-block= s_('GroupSettings|Disable group mentions') - %span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.') + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input' + = f.label :mentions_disabled, class: 'custom-control-label' do + %span= s_('GroupSettings|Disable group mentions') + %p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.') = render 'groups/settings/project_access_token_creation', f: f, group: @group = render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group diff --git a/app/views/groups/settings/_project_access_token_creation.html.haml b/app/views/groups/settings/_project_access_token_creation.html.haml index ac0ebfbd7f5..8be17c6cc30 100644 --- a/app/views/groups/settings/_project_access_token_creation.html.haml +++ b/app/views/groups/settings/_project_access_token_creation.html.haml @@ -1,10 +1,10 @@ - return unless render_setting_to_allow_project_access_token_creation?(group) .form-group.gl-mb-3 - .form-check - = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'form-check-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } - = f.label :resource_access_token_creation_allowed, class: 'form-check-label' do - %span.gl-display-block= s_('GroupSettings|Allow project access token creation') + .gl-form-checkbox.custom-control.custom-checkbox + = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'custom-control-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } + = f.label :resource_access_token_creation_allowed, class: 'custom-control-label' do + %span= s_('GroupSettings|Allow project access token creation') - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens') - link_start = ''.html_safe % { url: project_access_tokens_link } - %span.text-muted= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: ''.html_safe } + %p.help-text= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: ''.html_safe } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7203919bc85..3225dad5d57 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -120,7 +120,7 @@ = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') -#whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } } +#whats-new-app{ data: { version_digest: whats_new_version_digest } } - if can?(current_user, :update_user_status, current_user) .js-set-status-modal-wrapper{ data: user_status_data } diff --git a/app/views/layouts/header/_whats_new_dropdown_item.html.haml b/app/views/layouts/header/_whats_new_dropdown_item.html.haml index f79b741ced0..61fe2f1e711 100644 --- a/app/views/layouts/header/_whats_new_dropdown_item.html.haml +++ b/app/views/layouts/header/_whats_new_dropdown_item.html.haml @@ -1,5 +1,5 @@ %li - %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', data: { storage_key: whats_new_storage_key }, class: 'gl-display-flex!' } + %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' } = _("What's new") %span.js-whats-new-notification-count.whats-new-notification-count = whats_new_most_recent_release_items_count diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml new file mode 100644 index 00000000000..1e9aa4ec5ff --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml @@ -0,0 +1,14 @@ +- attribute = local_assigns.fetch(:attribute, nil) +- group = local_assigns.fetch(:group, nil) +- form = local_assigns.fetch(:form, nil) + +- return unless attribute && group && form && cascading_namespace_settings_enabled? +- return if group.namespace_settings.public_send("#{attribute}_locked?") + +- lock_attribute = "lock_#{attribute}" + +.gl-form-checkbox.custom-control.custom-checkbox + = form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' } + = form.label lock_attribute, class: 'custom-control-label' do + %span= s_('CascadingSettings|Enforce for all subgroups') + %p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.') diff --git a/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml new file mode 100644 index 00000000000..91458bf180b --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml @@ -0,0 +1 @@ +.js-cascading-settings-lock-popovers diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml new file mode 100644 index 00000000000..6596ce2bc73 --- /dev/null +++ b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml @@ -0,0 +1,21 @@ +- attribute = local_assigns.fetch(:attribute, nil) +- group = local_assigns.fetch(:group, nil) +- form = local_assigns.fetch(:form, nil) +- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil) +- help_text = local_assigns.fetch(:help_text, nil) + +- return unless attribute && group && form && settings_path_helper + +- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?") + += form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do + %span.position-relative.gl-pr-6.gl-display-inline-flex + = yield + - if setting_locked + %button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!', + type: 'button', + data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) } + = sprite_icon('lock', size: 16) + - if help_text + %p.help-text + = help_text diff --git a/changelogs/unreleased/327006-fix-search-commits-n-plus-1.yml b/changelogs/unreleased/327006-fix-search-commits-n-plus-1.yml new file mode 100644 index 00000000000..b20f054370a --- /dev/null +++ b/changelogs/unreleased/327006-fix-search-commits-n-plus-1.yml @@ -0,0 +1,5 @@ +--- +title: Fix N+1 for searching commits +merge_request: 58867 +author: +type: performance diff --git a/changelogs/unreleased/Externalise-strings-in-broadcast_messages-index-html-haml.yml b/changelogs/unreleased/Externalise-strings-in-broadcast_messages-index-html-haml.yml new file mode 100644 index 00000000000..0ce42710333 --- /dev/null +++ b/changelogs/unreleased/Externalise-strings-in-broadcast_messages-index-html-haml.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings in broadcast_messages/index.html.haml +merge_request: 58146 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/Externalise-strings-in_confirmation_instructions_account-html-haml.yml b/changelogs/unreleased/Externalise-strings-in_confirmation_instructions_account-html-haml.yml new file mode 100644 index 00000000000..a0db84bcfe8 --- /dev/null +++ b/changelogs/unreleased/Externalise-strings-in_confirmation_instructions_account-html-haml.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings in _confirmation_instructions_account.html.haml +merge_request: 58214 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/gl-badge-dashboard.yml b/changelogs/unreleased/gl-badge-dashboard.yml new file mode 100644 index 00000000000..f5087769429 --- /dev/null +++ b/changelogs/unreleased/gl-badge-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add gl-badge for badges in dashboard nav +merge_request: 57936 +author: Yogi (@yo) +type: changed diff --git a/doc/api/packages/go_proxy.md b/doc/api/packages/go_proxy.md new file mode 100644 index 00000000000..2f81435db42 --- /dev/null +++ b/doc/api/packages/go_proxy.md @@ -0,0 +1,133 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Go Proxy API + +This is the API documentation for [Go Packages](../../user/packages/go_proxy/index.md). +This API is behind a feature flag that is disabled by default. GitLab administrators with access to +the GitLab Rails console can [enable](../../administration/feature_flags.md) +this API for your GitLab instance. + +WARNING: +This API is used by the [Go client](https://maven.apache.org/) +and is generally not meant for manual consumption. + +For instructions on how to work with the Go Proxy, see the [Go Proxy package documentation](../../user/packages/go_proxy/index.md). + +NOTE: +These endpoints do not adhere to the standard API authentication methods. +See the [Go Proxy package documentation](../../user/packages/go_proxy/index.md) +for details on which headers and token types are supported. + +## List + +> Introduced in GitLab 13.1. + +Get all tagged versions for a given Go module: + +```plaintext +GET projects/:id/packages/go/:module_name/@v/list +``` + +| Attribute | Type | Required | Description | +| -------------- | ------ | -------- | ----------- | +| `id` | string | yes | The project ID or full path of a project. | +| `module_name` | string | yes | The name of the Go module. | + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/list" +``` + +Example output: + +```shell +"v1.0.0\nv1.0.1\nv1.3.8\n2.0.0\n2.1.0\n3.0.0" +``` + +## Version metadata + +> Introduced in GitLab 13.1. + +Get all tagged versions for a given Go module: + +```plaintext +GET projects/:id/packages/go/:module_name/@v/:module_version.info +``` + +| Attribute | Type | Required | Description | +| ----------------- | ------ | -------- | ----------- | +| `id` | string | yes | The project ID or full path of a project. | +| `module_name` | string | yes | The name of the Go module. | +| `module_version` | string | yes | The version of the Go module. | + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.info" +``` + +Example output: + +```json +{ + "Version": "v1.0.0", + "Time": "1617822312 -0600" +} +``` + +## Download module file + +> Introduced in GitLab 13.1. + +Fetch the `.mod` module file: + +```plaintext +GET projects/:id/packages/go/:module_name/@v/:module_version.mod +``` + +| Attribute | Type | Required | Description | +| ----------------- | ------ | -------- | ----------- | +| `id` | string | yes | The project ID or full path of a project. | +| `module_name` | string | yes | The name of the Go module. | +| `module_version` | string | yes | The version of the Go module. | + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.mod" +``` + +Write to a file: + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.mod" >> foo.mod +``` + +This writes to `foo.mod` in the current directory. + +## Download module source + +> Introduced in GitLab 13.1. + +Fetch the `.zip` of the module source: + +```plaintext +GET projects/:id/packages/go/:module_name/@v/:module_version.zip +``` + +| Attribute | Type | Required | Description | +| ----------------- | ------ | -------- | ----------- | +| `id` | string | yes | The project ID or full path of a project. | +| `module_name` | string | yes | The name of the Go module. | +| `module_version` | string | yes | The version of the Go module. | + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.zip" +``` + +Write to a file: + +```shell +curl --header "Private-Token: " "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.zip" >> foo.zip +``` + +This writes to `foo.zip` in the current directory. diff --git a/doc/development/snowplow.md b/doc/development/snowplow.md index 6803737cd79..80bcad9e244 100644 --- a/doc/development/snowplow.md +++ b/doc/development/snowplow.md @@ -110,21 +110,66 @@ The current method provides several attributes that are sent on each click event | property | text | false | Any additional property of the element, or object being acted on. | | value | decimal | false | Describes a numeric value or something directly related to the event. This could be the value of an input (e.g. `10` when clicking `internal` visibility). | +### Examples + +| category* | label | action | property** | value | +|-------------|------------------|-----------------------|----------|:-----:| +| [root:index] | main_navigation | click_navigation_link | `[link_label]` | - | +| [groups:boards:show] | toggle_swimlanes | click_toggle_button | - | `[is_active]` | +| [projects:registry:index] | registry_delete | click_button | - | - | +| [projects:registry:index] | registry_delete | confirm_deletion | - | - | +| [projects:blob:show] | congratulate_first_pipeline | click_button | `[human_access]` | - | +| [projects:clusters:new] | chart_options | generate_link | `[chart_link]` | - | +| [projects:clusters:new] | chart_options | click_add_label_button | `[label_id]` | - | + +_* It's ok to omit the category, and use the default._
+_** Property is usually the best place for variable strings._ + +### Reference SQL + +#### Last 20 `reply_comment_button` events + +```sql +SELECT + event_id, + v_tracker, + event_label, + event_action, + event_property, + event_value, + event_category, + contexts +FROM legacy.snowplow_structured_events_all +WHERE + event_label = 'reply_comment_button' + AND event_action = 'click_button' + -- AND event_category = 'projects:issues:show' + -- AND event_value = 1 +ORDER BY collector_tstamp DESC +LIMIT 20 +``` + ### Web-specific parameters Snowplow JS adds many [web-specific parameters](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/#Web-specific_parameters) to all web events by default. ## Implementing Snowplow JS (Frontend) tracking -GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. There are a few ways to use tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Structured event taxonomy](#structured-event-taxonomy). +GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. The simplest way to use it is to add `data-` attributes to clickable elements and dropdowns. There is also a Vue mixin (exposing a `track` method), and the static method `Tracking.event`. Each of these requires at minimum a `category` and an `action`. Additional data can be provided that adheres to our [Structured event taxonomy](#structured-event-taxonomy). | field | type | default value | description | |:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `category` | string | `document.body.dataset.page` | Page or subsection of a page that events are being captured within. | -| `action` | string | 'generic' | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. | +| `action` | string | generic | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. | | `data` | object | `{}` | Additional data such as `label`, `property`, `value`, and `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). | -### Tracking in HAML (or Vue Templates) +### Usage recommendations + +- Use [data attributes](#tracking-with-data-attributes) on HTML elements that emits either the `click`, `show.bs.dropdown`, or `hide.bs.dropdown` events. +- Use the [Vue mixin](#tracking-within-vue-components) when tracking custom events, or if the supported events for data attributes are not propagating. +- Use the [Tracking class directly](#tracking-in-raw-javascript) when tracking on raw JS files. + +### Tracking with data attributes When working within HAML (or Vue templates) we can add `data-track-*` attributes to elements of interest. All elements that have a `data-track-action` attribute automatically have event tracking bound on clicks. @@ -142,7 +187,7 @@ Below is an example of `data-track-*` attributes assigned to a button: /> ``` -Event listeners are bound at the document level to handle click events on or within elements with these data attributes. This allows them to be properly handled on re-rendering and changes to the DOM. Note that because of the way these events are bound, click events should not be stopped from propagating up the DOM tree. If for any reason click events are being stopped from propagating, you need to implement your own listeners and follow the instructions in [Tracking in raw JavaScript](#tracking-in-raw-javascript). +Event listeners are bound at the document level to handle click events on or within elements with these data attributes. This allows them to be properly handled on re-rendering and changes to the DOM. Note that because of the way these events are bound, click events should not be stopped from propagating up the DOM tree. If for any reason click events are being stopped from propagating, you need to implement your own listeners and follow the instructions in [Tracking within Vue components](#tracking-within-vue-components) or [Tracking in raw JavaScript](#tracking-in-raw-javascript). Below is a list of supported `data-track-*` attributes: @@ -154,16 +199,29 @@ Below is a list of supported `data-track-*` attributes: | `data-track-value` | false | The `value` as described in our [Structured event taxonomy](#structured-event-taxonomy). If omitted, this is the element's `value` property or an empty string. For checkboxes, the default value is the element's checked attribute or `false` when unchecked. | | `data-track-context` | false | The `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). | +#### Available helpers + +```ruby +tracking_attrs(label, action, property) # { data: { track_label... } } + +%button{ **tracking_attrs('main_navigation', 'click_button', 'navigation') } +``` + #### Caveats When using the GitLab helper method [`nav_link`](https://gitlab.com/gitlab-org/gitlab/-/blob/898b286de322e5df6a38d257b10c94974d580df8/app/helpers/tab_helper.rb#L69) be sure to wrap `html_options` under the `html_options` keyword argument. Be careful, as this behavior can be confused with the `ActionView` helper method [`link_to`](https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) that does not require additional wrapping of `html_options` -`nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: "groups_dropdown", track_action: "click_dropdown" } })` +```ruby +# Bad += nav_link(controller: ['dashboard/groups', 'explore/groups'], data: { track_label: "explore_groups", track_action: "click_button" }) -vs +# Good += nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: "explore_groups", track_action: "click_button" } }) -`link_to assigned_issues_dashboard_path, title: _('Issues'), data: { track_label: 'main_navigation', track_action: 'click_issues_link' }` +# Good (other helpers) += link_to explore_groups_path, title: _("Explore"), data: { track_label: "explore_groups", track_action: "click_button" } +``` ### Tracking within Vue components @@ -186,17 +244,19 @@ export default { return { expanded: false, tracking: { - label: 'left_sidebar' - } + label: 'left_sidebar', + }, }; }, -} +}; ``` -The mixin provides a `track` method that can be called within the template, or from component methods. An example of the whole implementation might look like the following. +The mixin provides a `track` method that can be called within the template, +or from component methods. An example of the whole implementation might look like this: ```javascript export default { + name: 'RightSidebar', mixins: [Tracking.mixin({ label: 'right_sidebar' })], data() { return { @@ -206,26 +266,84 @@ export default { methods: { toggle() { this.expanded = !this.expanded; - this.track('click_toggle', { value: this.expanded }) + // Additional data will be merged, like `value` below + this.track('click_toggle', { value: Number(this.expanded) }); } } }; ``` -And if needed within the template, you can use the `track` method directly as well. +The event data can be provided with a `tracking` object, declared in the `data` function, +or as a `computed property`. + +```javascript +export default { + name: 'RightSidebar', + mixins: [Tracking.mixin()], + data() { + return { + tracking: { + label: 'right_sidebar', + // category: '', + // property: '', + // value: '', + }, + }; + }, +}; +``` + +The event data can be provided directly in the `track` function as well. +This object will merge with any previously provided options. + +```javascript +this.track('click_button', { + label: 'right_sidebar', +}); +``` + +Lastly, if needed within the template, you can use the `track` method directly as well. ```html ``` +#### Testing example + +```javascript +import { mockTracking } from 'helpers/tracking_helper'; +// mockTracking(category, documentOverride, spyMethod) + +describe('RightSidebar.vue', () => { + let trackingSpy; + let wrapper; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + const findToggle = () => wrapper.find('[data-testid="toggle"]'); + + it('tracks turning off toggle', () => { + findToggle().trigger('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', { + label: 'right_sidebar', + value: 0, + }); + }); +}); +``` + ### Tracking in raw JavaScript Custom event tracking and instrumentation can be added by directly calling the `Tracking.event` static function. The following example demonstrates tracking a click on a button by calling `Tracking.event` manually. @@ -234,64 +352,35 @@ Custom event tracking and instrumentation can be added by directly calling the ` import Tracking from '~/tracking'; const button = document.getElementById('create_from_template_button'); + button.addEventListener('click', () => { Tracking.event('dashboard:projects:index', 'click_button', { label: 'create_from_template', property: 'template_preview', - value: 'rails', - }); -}) -``` - -### Tests and test helpers - -In Jest particularly in Vue tests, you can use the following: - -```javascript -import { mockTracking } from 'helpers/tracking_helper'; - -describe('MyTracking', () => { - let spy; - - beforeEach(() => { - spy = mockTracking('_category_', wrapper.element, jest.spyOn); - }); - - it('tracks an event when clicked on feedback', () => { - wrapper.find('.discover-feedback-icon').trigger('click'); - - expect(spy).toHaveBeenCalledWith('_category_', 'click_button', { - label: 'security-discover-feedback-cta', - property: '0', - }); }); }); ``` -In obsolete Karma tests it's used as below: +#### Testing example ```javascript -import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper'; +import Tracking from '~/tracking'; -describe('my component', () => { - let trackingSpy; +describe('MyTracking', () => { + let wrapper; beforeEach(() => { - trackingSpy = mockTracking('_category_', vm.$el, spyOn); + jest.spyOn(Tracking, 'event'); }); - const triggerEvent = () => { - // action which should trigger a event - }; + const findButton = () => wrapper.find('[data-testid="create_from_template"]'); - it('tracks an event when toggled', () => { - expect(trackingSpy).not.toHaveBeenCalled(); + it('tracks event', () => { + findButton().trigger('click'); - triggerEvent('a.toggle'); - - expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_edit_button', { - label: 'right_sidebar', - property: 'confidentiality', + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + label: 'create_from_template', + property: 'template_preview', }); }); }); @@ -355,12 +444,6 @@ There are several tools for developing and testing Snowplow Event **{check-circle}** Available, **{status_preparing}** In progress, **{dotted-circle}** Not Planned -### Preparing your MR for Review - -1. For frontend events, in the MR description section, add a screenshot of the event's relevant section using the [Snowplow Analytics Debugger](https://chrome.google.com/webstore/detail/snowplow-analytics-debugg/jbnlcgeengmijcghameodeaenefieedm) Chrome browser extension. -1. For backend events, please use Snowplow Micro and add the output of the Snowplow Micro good events `GET http://localhost:9090/micro/good`. -1. Include a member of the Product Intelligence team as a reviewer of your MR. Mention `@gitlab-org/growth/product_intelligence/engineers` in your MR to request a review. - ### Snowplow Analytics Debugger Chrome Extension Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging and local development environments. diff --git a/doc/development/usage_ping/product_intelligence_review.md b/doc/development/usage_ping/product_intelligence_review.md index c667bc8354c..e90e93a2c81 100644 --- a/doc/development/usage_ping/product_intelligence_review.md +++ b/doc/development/usage_ping/product_intelligence_review.md @@ -34,7 +34,7 @@ Product Intelligence files. ### Roles and process -The merge request **author** should: +#### The merge request **author** should - Decide whether a Product Intelligence review is needed. - If a Product Intelligence review is needed, add the labels @@ -48,7 +48,15 @@ The merge request **author** should: [Metrics Dictionary](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/usage_ping/dictionary.md) if it is needed. - Add a changelog [according to guidelines](../changelog.md). -The Product Intelligence **reviewer** should: +##### When adding or modifiying Snowplow events + +- For frontend events, when relevant, add a screenshot of the event in + the [testing tool](../snowplow.md#developing-and-testing-snowplow) used. +- For backend events, when relevant, add the output of the Snowplow Micro + good events `GET http://localhost:9090/micro/good` (it might be a good idea + to reset with `GET http://localhost:9090/micro/reset` first). + +#### The Product Intelligence **reviewer** should - Perform a first-pass review on the merge request and suggest improvements to the author. - Approve the MR, and relabel the MR with `~"product intelligence::approved"`. @@ -71,6 +79,9 @@ Any of the Product Intelligence engineers can be assigned for the Product Intell - For tracking using Redis HLL (HyperLogLog): - Check the Redis slot. - Check if a [feature flag is needed](index.md#recommendations). +- For tracking with Snowplow: + - Check that the [event taxonomy](../snowplow.md#structured-event-taxonomy) is correct. + - Check the [usage recomendations](../snowplow.md#usage-recommendations). - Metrics YAML definitions: - Check the metric `description`. - Check the metrics `key_path`. diff --git a/doc/user/application_security/policies/index.md b/doc/user/application_security/policies/index.md index 33d792001e3..208fbdfa5f3 100644 --- a/doc/user/application_security/policies/index.md +++ b/doc/user/application_security/policies/index.md @@ -55,29 +55,46 @@ Feature.disable(:security_orchestration_policies_configuration, Project.find(