diff options
37 files changed, 843 insertions, 97 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d8c52461a..ee8af966567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 13.11.4 (2021-05-14) + +### Fixed (3 changes) + +- Fix N+1 SQL queries in PipelinesController#show. !60794 +- Omit trailing slash when proxying pre-authorized routes with no suffix. !61638 +- Omit trailing slash when checking allowed requests in the read-only middleware. !61641 + + ## 13.11.3 (2021-04-30) ### Fixed (1 change) diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue index 46108572a88..88a9f73258f 100644 --- a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue @@ -91,7 +91,9 @@ export default { data-testid="stuck-icon" /> - <div class="gl-display-flex gl-align-items-center"> + <div + class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end" + > <div v-if="jobRef" class="gl-max-w-15 gl-text-truncate"> <gl-icon v-if="createdByTag" diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue index 2736aa42684..4fe5bbf79cd 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue @@ -11,6 +11,8 @@ const defaultTableClasses = { tdClass: 'gl-p-5!', thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!', }; +// eslint-disable-next-line @gitlab/require-i18n-strings +const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`; export default { i18n: { @@ -56,7 +58,8 @@ export default { { key: 'coverage', label: __('Coverage'), - ...defaultTableClasses, + tdClass: coverageTdClasses, + thClass: defaultTableClasses.thClass, columnClass: 'gl-w-10p', }, { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 784f7cccf15..2cb1b6a195f 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -1,6 +1,7 @@ import { __ } from '~/locale'; export const DEBOUNCE_DELAY = 200; +export const MAX_RECENT_TOKENS_SIZE = 3; export const FILTER_NONE = 'None'; export const FILTER_ANY = 'Any'; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js index a15cf220ee5..e5c8d29e09b 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js @@ -1,6 +1,9 @@ -import { isEmpty } from 'lodash'; +import { isEmpty, uniqWith, isEqual } from 'lodash'; +import AccessorUtilities from '~/lib/utils/accessor'; import { queryToObject } from '~/lib/utils/url_utility'; +import { MAX_RECENT_TOKENS_SIZE } from './constants'; + /** * Strips enclosing quotations from a string if it has one. * @@ -162,3 +165,38 @@ export function urlQueryToFilter(query = '') { return { ...memo, [filterName]: { value, operator } }; }, {}); } + +/** + * Returns array of token values from localStorage + * based on provided recentTokenValuesStorageKey + * + * @param {String} recentTokenValuesStorageKey + * @returns + */ +export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) { + let recentlyUsedTokenValues = []; + if (AccessorUtilities.isLocalStorageAccessSafe()) { + recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || []; + } + return recentlyUsedTokenValues; +} + +/** + * Sets provided token value to recently used array + * within localStorage for provided recentTokenValuesStorageKey + * + * @param {String} recentTokenValuesStorageKey + * @param {Object} tokenValue + */ +export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) { + const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey); + + recentlyUsedTokenValues.splice(0, 0, { ...tokenValue }); + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem( + recentTokenValuesStorageKey, + JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)), + ); + } +} diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue new file mode 100644 index 00000000000..6ebc5431012 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -0,0 +1,167 @@ +<script> +import { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlDropdownSectionHeader, + GlLoadingIcon, +} from '@gitlab/ui'; + +import { DEBOUNCE_DELAY } from '../constants'; +import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; + +export default { + components: { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlDropdownSectionHeader, + GlLoadingIcon, + }, + props: { + tokenConfig: { + type: Object, + required: true, + }, + tokenValue: { + type: Object, + required: true, + }, + tokenActive: { + type: Boolean, + required: true, + }, + tokensListLoading: { + type: Boolean, + required: true, + }, + tokenValues: { + type: Array, + required: true, + }, + fnActiveTokenValue: { + type: Function, + required: true, + }, + defaultTokenValues: { + type: Array, + required: false, + default: () => [], + }, + recentTokenValuesStorageKey: { + type: String, + required: false, + default: '', + }, + valueIdentifier: { + type: String, + required: false, + default: 'id', + }, + fnCurrentTokenValue: { + type: Function, + required: false, + default: null, + }, + }, + data() { + return { + searchKey: '', + recentTokenValues: this.recentTokenValuesStorageKey + ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey) + : [], + loading: false, + }; + }, + computed: { + isRecentTokenValuesEnabled() { + return Boolean(this.recentTokenValuesStorageKey); + }, + recentTokenIds() { + return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name); + }, + currentTokenValue() { + if (this.fnCurrentTokenValue) { + return this.fnCurrentTokenValue(this.tokenValue.data); + } + return this.tokenValue.data.toLowerCase(); + }, + activeTokenValue() { + return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue); + }, + /** + * Return all the tokenValues when searchKey is present + * otherwise return only the tokenValues which aren't + * present in "Recently used" + */ + availableTokenValues() { + return this.searchKey + ? this.tokenValues + : this.tokenValues.filter( + (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]), + ); + }, + }, + watch: { + tokenActive: { + immediate: true, + handler(newValue) { + if (!newValue && !this.tokenValues.length) { + this.$emit('fetch-token-values', this.tokenValue.data); + } + }, + }, + }, + methods: { + handleInput({ data }) { + this.searchKey = data; + setTimeout(() => { + if (!this.tokensListLoading) this.$emit('fetch-token-values', data); + }, DEBOUNCE_DELAY); + }, + handleTokenValueSelected(activeTokenValue) { + if (this.isRecentTokenValuesEnabled) { + setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); + } + }, + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="tokenConfig" + v-bind="{ ...this.$parent.$props, ...this.$parent.$attrs }" + v-on="this.$parent.$listeners" + @input="handleInput" + @select="handleTokenValueSelected(activeTokenValue)" + > + <template #view-token="viewTokenProps"> + <slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> + </template> + <template #view="viewTokenProps"> + <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> + </template> + <template #suggestions> + <template v-if="defaultTokenValues.length"> + <gl-filtered-search-suggestion + v-for="token in defaultTokenValues" + :key="token.value" + :value="token.value" + > + {{ token.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider /> + </template> + <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey"> + <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header> + <slot name="token-values-list" :token-values="recentTokenValues"></slot> + <gl-dropdown-divider /> + </template> + <gl-loading-icon v-if="tokensListLoading" /> + <template v-else> + <slot name="token-values-list" :token-values="availableTokenValues"></slot> + </template> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 4b71f9b3065..d7ec2e699a0 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -148,6 +148,27 @@ module CommitsHelper end end + # This is used to calculate a cache key for the app/views/projects/commits/_commit.html.haml + # partial. It takes some of the same parameters as used in the partial and will hash the + # current pipeline status. + # + # This includes a keyed hash for values that can be nil, to prevent invalid cache entries + # being served if the order should change in future. + def commit_partial_cache_key(commit, ref:, merge_request:, request:) + [ + commit, + commit.author, + ref, + { + merge_request: merge_request, + pipeline_status: hashed_pipeline_status(commit, ref), + xhr: request.xhr?, + controller: controller.controller_path, + path: @path # referred to in #link_to_browse_code + } + ] + end + protected # Private: Returns a link to a person. If the person has a matching user and @@ -221,4 +242,14 @@ module CommitsHelper project_commit_path(project, commit) end end + + private + + def hashed_pipeline_status(commit, ref) + status = commit.status_for(ref) + + return if status.nil? + + Digest::SHA1.hexdigest(status.to_s) + end end diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb index 03da679cfdd..38c98776fdf 100644 --- a/app/helpers/notify_helper.rb +++ b/app/helpers/notify_helper.rb @@ -18,7 +18,7 @@ module NotifyHelper when "Developer" s_("InviteEmail|As a developer, you have full access to projects, so you can take an idea from concept to production.") when "Maintainer" - s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to master and deploy to production.") + s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to the default branch and deploy to production.") when "Owner" s_("InviteEmail|As an owner, you have full access to projects and can manage access to the group, including inviting new members.") when "Minimal Access" diff --git a/app/models/project.rb b/app/models/project.rb index 9d8bd2dbb36..6d8f46d9ea6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2539,7 +2539,7 @@ class Project < ApplicationRecord def default_branch_or_main return default_branch if default_branch - Gitlab::DefaultBranch.value(project: self) + Gitlab::DefaultBranch.value(object: self) end def ci_config_path_or_default diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 7beeb106735..68957dd6b22 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -315,7 +315,7 @@ class Snippet < ApplicationRecord override :default_branch def default_branch - super || Gitlab::DefaultBranch.value(project: project) + super || Gitlab::DefaultBranch.value(object: project) end def repository_storage diff --git a/app/views/admin/application_settings/_initial_branch_name.html.haml b/app/views/admin/application_settings/_initial_branch_name.html.haml index b5c178641df..f881808e51f 100644 --- a/app/views/admin/application_settings/_initial_branch_name.html.haml +++ b/app/views/admin/application_settings/_initial_branch_name.html.haml @@ -1,12 +1,12 @@ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) - - fallback_branch_name = '<code>master</code>' + - fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value}</code>" %fieldset .form-group = f.label :default_branch_name, _('Default initial branch name'), class: 'label-light' - = f.text_field :default_branch_name, placeholder: 'master', class: 'form-control gl-form-input' + = f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input' %span.form-text.text-muted = (_("Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name } ).html_safe diff --git a/app/views/groups/settings/repository/_initial_branch_name.html.haml b/app/views/groups/settings/repository/_initial_branch_name.html.haml index efe690a0c2d..23ac7d51e4f 100644 --- a/app/views/groups/settings/repository/_initial_branch_name.html.haml +++ b/app/views/groups/settings/repository/_initial_branch_name.html.haml @@ -9,12 +9,12 @@ .settings-content = form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f| = form_errors(@group) - - fallback_branch_name = '<code>master</code>' + - fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value(object: @group)}</code>" %fieldset .form-group = f.label :default_branch_name, _('Default initial branch name'), class: 'label-light' - = f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: 'master', class: 'form-control' + = f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: Gitlab::DefaultBranch.value(object: @group), class: 'form-control' %span.form-text.text-muted = (_("Changes affect new repositories only. If not specified, either the configured application-wide default or Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name }).html_safe diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index ceb312450be..bc0d14743b9 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -3,22 +3,24 @@ - `assets/javascripts/diffs/components/commit_item.vue` EXCEPTION WARNING - see above `.vue` file for de-sync drift --#----------------------------------------------------------------- -- view_details = local_assigns.fetch(:view_details, false) -- merge_request = local_assigns.fetch(:merge_request, nil) -- project = local_assigns.fetch(:project) { merge_request&.project } -- ref = local_assigns.fetch(:ref) { merge_request&.source_branch } -- commit = commit.present(current_user: current_user) -- commit_status = commit.status_for(ref) -- collapsible = local_assigns.fetch(:collapsible, true) -- link_data_attrs = local_assigns.fetch(:link_data_attrs, {}) - -- link = commit_path(project, commit, merge_request: merge_request) + WARNING: When introducing new content here, please consider what + changes may need to be made in the cache keys used to + wrap this view, found in + CommitsHelper#commit_partial_cache_key +-#----------------------------------------------------------------- +- view_details = local_assigns.fetch(:view_details, false) +- merge_request = local_assigns.fetch(:merge_request, nil) +- project = local_assigns.fetch(:project) { merge_request&.project } +- ref = local_assigns.fetch(:ref) { merge_request&.source_branch } +- commit = commit.present(current_user: current_user) +- commit_status = commit.status_for(ref) +- collapsible = local_assigns.fetch(:collapsible, true) +- link_data_attrs = local_assigns.fetch(:link_data_attrs, {}) +- link = commit_path(project, commit, merge_request: merge_request) - show_project_name = local_assigns.fetch(:show_project_name, false) %li{ class: ["commit flex-row", ("js-toggle-container" if collapsible)], id: "commit-#{commit.short_id}" } - .avatar-cell.d-none.d-sm-block = author_avatar(commit, size: 40, has_tooltip: false) diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 9e2dca3ad71..e6c9a7166a9 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,8 +3,8 @@ - ref = local_assigns.fetch(:ref) { merge_request&.source_branch } - can_update_merge_request = can?(current_user, :update_merge_request, @merge_request) -- commits = @commits -- context_commits = @context_commits +- commits = @commits&.map { |commit| commit.present(current_user: current_user) } +- context_commits = @context_commits&.map { |commit| commit.present(current_user: current_user) } - hidden = @hidden_commit_count - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits| @@ -14,7 +14,10 @@ %li.commits-row{ data: { day: day } } %ul.content-list.commit-list.flex-list - = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request } + - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml) + = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) } + - else + = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request } - if context_commits.present? %li.commit-header.js-commit-header @@ -25,7 +28,10 @@ %li.commits-row %ul.content-list.commit-list.flex-list - = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request } + - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml) + = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) } + - else + = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request } - if hidden > 0 %li.gl-alert.gl-alert-warning diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index e3ab184ec6f..426d022da26 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -4,11 +4,11 @@ %h3.page-title = _("Compare Git revisions") .sub-header-block - - example_master = capture do - %code.ref-name master + - example_branch = capture do + %code.ref-name= @project.default_branch_or_main - example_sha = capture do %code.ref-name 4eedf23 - = html_escape(_("Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { master: example_master.html_safe, sha: example_sha.html_safe } + = html_escape(_("Choose a branch/tag (e.g. %{branch}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { branch: example_branch.html_safe, sha: example_sha.html_safe } %br = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe } diff --git a/changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml b/changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml deleted file mode 100644 index 6e04471fa13..00000000000 --- a/changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Omit trailing slash when checking allowed requests in the read-only middleware -merge_request: 61641 -author: -type: fixed diff --git a/changelogs/unreleased/id-rename-main-to-master-in-views.yml b/changelogs/unreleased/id-rename-main-to-master-in-views.yml new file mode 100644 index 00000000000..7e3f45274f1 --- /dev/null +++ b/changelogs/unreleased/id-rename-main-to-master-in-views.yml @@ -0,0 +1,5 @@ +--- +title: Rename master to main in views placeholders +merge_request: 61252 +author: +type: changed diff --git a/changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml b/changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml deleted file mode 100644 index 3bea1874ff3..00000000000 --- a/changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Omit trailing slash when proxying pre-authorized routes with no suffix -merge_request: 61638 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml b/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml deleted file mode 100644 index ebaf2aee123..00000000000 --- a/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix N+1 SQL queries in PipelinesController#show -merge_request: 60794 -author: -type: fixed diff --git a/config/feature_flags/development/cached_commits.yml b/config/feature_flags/development/cached_commits.yml new file mode 100644 index 00000000000..0758f8a3f53 --- /dev/null +++ b/config/feature_flags/development/cached_commits.yml @@ -0,0 +1,8 @@ +--- +name: cached_commits +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61617 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330968 +milestone: '13.12' +type: development +group: group::source code +default_enabled: false diff --git a/config/helpers/vendor_dll_hash.js b/config/helpers/vendor_dll_hash.js index cdbaebc9789..9b99b4c4ae9 100644 --- a/config/helpers/vendor_dll_hash.js +++ b/config/helpers/vendor_dll_hash.js @@ -1,5 +1,5 @@ -const fs = require('fs'); const crypto = require('crypto'); +const fs = require('fs'); const path = require('path'); const CACHE_PATHS = [ diff --git a/config/karma.config.js b/config/karma.config.js index 1c2dd21c189..3e125759357 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -1,8 +1,8 @@ /* eslint-disable no-inner-declarations, no-param-reassign */ +const path = require('path'); const chalk = require('chalk'); const argumentsParser = require('commander'); const glob = require('glob'); -const path = require('path'); const webpack = require('webpack'); const IS_EE = require('./helpers/is_ee_env'); const webpackConfig = require('./webpack.config.js'); diff --git a/config/webpack.config.js b/config/webpack.config.js index d48b22f8024..d40d14e1158 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,11 +1,11 @@ const fs = require('fs'); +const path = require('path'); const SOURCEGRAPH_VERSION = require('@sourcegraph/code-host-integration/package.json').version; const CompressionPlugin = require('compression-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const glob = require('glob'); -const path = require('path'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const VUE_LOADER_VERSION = require('vue-loader/package.json').version; const VUE_VERSION = require('vue/package.json').version; diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md index 14555a05672..6e9197c223b 100644 --- a/doc/ci/jobs/job_control.md +++ b/doc/ci/jobs/job_control.md @@ -63,7 +63,7 @@ except `main` and branches that start with `release/`. ### `only: variables` / `except: variables` examples -You can use `except:variables` to exclude jobs based on a commit message: +You can use [`except:variables`](../yaml/README.md#onlyvariables--exceptvariables) to exclude jobs based on a commit message: ```yaml end-to-end: @@ -223,6 +223,48 @@ test: - "README.md" ``` +## Use predefined CI/CD variables to run jobs only in specific pipeline types + +You can use [predefined CI/CD variables](../variables/predefined_variables.md) to choose +which pipeline types jobs run in, with: + +- [`rules`](../yaml/README.md#rules) +- [`only:variables`](../yaml/README.md#onlyvariables--exceptvariables) +- [`except:variables`](../yaml/README.md#onlyvariables--exceptvariables) + +The following table lists some of the variables that you can use, and the pipeline +types the variables can control for: + +- Branch pipelines that run for Git `push` events to a branch, like new commits or tags. +- Tag pipelines that run only when a new Git tag is pushed to a branch. +- [Merge request pipelines](../merge_request_pipelines/index.md) that run for changes + to a merge request, like new commits or selecting the **Run pipeline** button + in a merge request's pipelines tab. +- [Scheduled pipelines](../pipelines/schedules.md). + +| Variables | Branch | Tag | Merge request | Scheduled | +|--------------------------------------------|--------|-----|---------------|-----------| +| `CI_COMMIT_BRANCH` | Yes | | | Yes | +| `CI_COMMIT_TAG` | | Yes | | Yes, if the scheduled pipeline is configured to run on a tag. | +| `CI_PIPELINE_SOURCE = push` | Yes | Yes | | | +| `CI_PIPELINE_SOURCE = scheduled` | | | | Yes | +| `CI_PIPELINE_SOURCE = merge_request_event` | | | Yes | | +| `CI_MERGE_REQUEST_IID` | | | Yes | | + +For example, to configure a job to run for merge request pipelines and scheduled pipelines, +but not branch or tag pipelines: + +```yaml +job1: + script: + - echo + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "scheduled" + - if: $CI_PIPELINE_SOURCE == "push" + when: never +``` + ## Regular expressions The `@` symbol denotes the beginning of a ref's repository path. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4b30d08ff10..1f323a311e1 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -36,14 +36,14 @@ The keywords available for jobs are: | [`coverage`](#coverage) | Code coverage settings for a given job. | | [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. | | [`environment`](#environment) | Name of an environment to which the job deploys. | -| [`except`](#only--except) | Control when jobs are not created. | +| [`except`](#only--except) | Control when jobs are not created. | | [`extends`](#extends) | Configuration entries that this job inherits from. | | [`image`](#image) | Use Docker images. | | [`include`](#include) | Include external YAML files. | | [`inherit`](#inherit) | Select which global defaults all jobs inherit. | | [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. | | [`needs`](#needs) | Execute jobs earlier than the stage ordering. | -| [`only`](#only--except) | Control when jobs are created. | +| [`only`](#only--except) | Control when jobs are created. | | [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. | | [`parallel`](#parallel) | How many instances of a job should be run in parallel. | | [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. | @@ -1615,7 +1615,7 @@ Four keywords can be used with `only` and `except`: - [`changes`](#onlychanges--exceptchanges) - [`kubernetes`](#onlykubernetes--exceptkubernetes) -See [control jobs with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except) +See [specify when jobs run with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except) for more details and examples. #### `only:refs` / `except:refs` @@ -3632,10 +3632,10 @@ failure. `artifacts:when` can be set to one of the following values: 1. `on_success` (default): Upload artifacts only when the job succeeds. -1. `on_failure`: Upload artifacts only when the job fails. Useful, for example, when +1. `on_failure`: Upload artifacts only when the job fails. +1. `always`: Always upload artifacts. Useful, for example, when [uploading artifacts](../unit_test_reports.md#viewing-junit-screenshots-on-gitlab) required to troubleshoot failing tests. -1. `always`: Always upload artifacts. For example, to upload artifacts only when a job fails: diff --git a/doc/development/stage_group_dashboards.md b/doc/development/stage_group_dashboards.md index 58e998e46a8..d6d607df2dd 100644 --- a/doc/development/stage_group_dashboards.md +++ b/doc/development/stage_group_dashboards.md @@ -57,7 +57,7 @@ component has 2 indicators: The calculation to a ratio then happens as follows: ```math -\frac {operations\_meeting\_apdex + (total\_operations - operations\_with_\errors)} {total\_apdex\_measurements + total\_operations} +\frac {operations\_meeting\_apdex + (total\_operations - operations\_with\_errors)} {total\_apdex\_measurements + total\_operations} ``` *Caveat:* Not all components are included, causing the diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md index e79cd127d5a..292e1256cb8 100644 --- a/doc/development/usage_ping/index.md +++ b/doc/development/usage_ping/index.md @@ -266,7 +266,7 @@ To remove a deprecated metric: [fixtures](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/spec/support/usage_data_helpers.rb#L540) used to test [`UsageDataController#create`](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/3760ef28/spec/controllers/usage_data_controller_spec.rb#L75) - endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload. + endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload. 1. Create an issue in the [GitLab Data Team project](https://gitlab.com/gitlab-data/analytics/-/issues). @@ -276,7 +276,7 @@ To remove a deprecated metric: This step can be skipped if verification done during [deprecation process](#3-deprecate-a-metric) reported that metric is not required by any data transformation in Snowflake data warehouse nor it is used by any of SiSense dashboards. - + 1. After you verify the metric can be safely removed, update the attributes of the metric's YAML definition: @@ -1024,7 +1024,13 @@ On GitLab.com, we have DangerBot setup to monitor Product Intelligence related f ### 10. Verify your metric -On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric. +On GitLab.com, the Product Intelligence team regularly [monitors Usage Ping](https://gitlab.com/groups/gitlab-org/-/epics/6000). +They may alert you that your metrics need further optimization to run quicker and with greater success. + +The Usage Ping JSON payload for GitLab.com is shared in the +[#g_product_intelligence](https://gitlab.slack.com/archives/CL3A7GFPF) Slack channel every week. + +You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric. ### Usage Ping local setup diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml new file mode 100644 index 00000000000..5216a46745c --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml @@ -0,0 +1,77 @@ +# Read more about the feature here: https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html + +browser_performance: + stage: performance + image: docker:19.03.12 + allow_failure: true + variables: + DOCKER_TLS_CERTDIR: "" + SITESPEED_IMAGE: sitespeedio/sitespeed.io + SITESPEED_VERSION: 14.1.0 + SITESPEED_OPTIONS: '' + services: + - docker:19.03.12-dind + script: + - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) + - mkdir gitlab-exporter + # Busybox wget does not support proxied HTTPS, get the real thing. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/287611. + - (env | grep -i _proxy= 2>&1 >/dev/null) && apk --no-cache add wget + - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js + - mkdir sitespeed-results + - | + function propagate_env_vars() { + CURRENT_ENV=$(printenv) + + for VAR_NAME; do + echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME " + done + } + - | + if [ -f .gitlab-urls.txt ] + then + sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt + docker run \ + $(propagate_env_vars \ + auto_proxy \ + https_proxy \ + http_proxy \ + no_proxy \ + AUTO_PROXY \ + HTTPS_PROXY \ + HTTP_PROXY \ + NO_PROXY \ + ) \ + --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS + else + docker run \ + $(propagate_env_vars \ + auto_proxy \ + https_proxy \ + http_proxy \ + no_proxy \ + AUTO_PROXY \ + HTTPS_PROXY \ + HTTP_PROXY \ + NO_PROXY \ + ) \ + --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS + fi + - mv sitespeed-results/data/performance.json browser-performance.json + artifacts: + paths: + - sitespeed-results/ + reports: + browser_performance: browser-performance.json + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$PERFORMANCE_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/lib/gitlab/default_branch.rb b/lib/gitlab/default_branch.rb index 73c9ac07753..6bd9a5675c4 100644 --- a/lib/gitlab/default_branch.rb +++ b/lib/gitlab/default_branch.rb @@ -3,8 +3,8 @@ # Class is used while we're migrating from master to main module Gitlab module DefaultBranch - def self.value(project: nil) - Feature.enabled?(:main_branch_over_master, project, default_enabled: :yaml) ? 'main' : 'master' + def self.value(object: nil) + Feature.enabled?(:main_branch_over_master, object, default_enabled: :yaml) ? 'main' : 'master' end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8087d3c3c32..01a5e40941a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6334,7 +6334,7 @@ msgstr "" msgid "Choose File..." msgstr "" -msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request." +msgid "Choose a branch/tag (e.g. %{branch}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request." msgstr "" msgid "Choose a file" @@ -17894,7 +17894,7 @@ msgstr "" msgid "InviteEmail|As a guest, you can view projects, leave comments, and create issues." msgstr "" -msgid "InviteEmail|As a maintainer, you have full access to projects. You can push commits to master and deploy to production." +msgid "InviteEmail|As a maintainer, you have full access to projects. You can push commits to the default branch and deploy to production." msgstr "" msgid "InviteEmail|As a reporter, you can view projects and reports, and leave comments on issues." diff --git a/package.json b/package.json index 57d229cd8e9..173f618e5e9 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@gitlab/eslint-plugin": "8.3.0", + "@gitlab/eslint-plugin": "8.4.0", "@gitlab/stylelint-config": "2.3.0", "@testing-library/dom": "^7.16.2", "@vue/test-utils": "1.1.2", @@ -201,7 +201,7 @@ "docdash": "^1.0.2", "eslint": "7.26.0", "eslint-import-resolver-jest": "3.0.0", - "eslint-import-resolver-webpack": "0.13.0", + "eslint-import-resolver-webpack": "0.13.1", "eslint-plugin-jasmine": "4.1.2", "eslint-plugin-no-jquery": "2.6.0", "gettext-extractor": "^3.5.3", diff --git a/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js b/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js index 22a4aac762b..4c439caa1da 100755 --- a/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js +++ b/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js @@ -8,8 +8,8 @@ if (process.env.RAILS_ENV !== 'production') { } const fs = require('fs'); -const glob = require('glob'); const path = require('path'); +const glob = require('glob'); const pjs = require('postcss'); const paths = glob.sync('public/assets/page_bundles/_mixins_and_variables_and_functions*.css', { diff --git a/scripts/frontend/merge_coverage_frontend.js b/scripts/frontend/merge_coverage_frontend.js index 6b3826ddac7..bd56b0d5868 100644 --- a/scripts/frontend/merge_coverage_frontend.js +++ b/scripts/frontend/merge_coverage_frontend.js @@ -1,8 +1,8 @@ +const { resolve } = require('path'); const { sync } = require('glob'); const { createCoverageMap } = require('istanbul-lib-coverage'); const { createContext } = require('istanbul-lib-report'); const { create } = require('istanbul-reports'); -const { resolve } = require('path'); const coverageMap = createCoverageMap(); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js index 9e96c154546..b2ed79cd75a 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js @@ -1,3 +1,6 @@ +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; + +import AccessorUtilities from '~/lib/utils/accessor'; import { stripQuotes, uniqueTokens, @@ -5,6 +8,8 @@ import { processFilters, filterToQueryObject, urlQueryToFilter, + getRecentlyUsedTokenValues, + setTokenValueToRecentlyUsed, } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { @@ -14,6 +19,12 @@ import { tokenValuePlain, } from './mock_data'; +const mockStorageKey = 'recent-tokens'; + +function setLocalStorageAvailability(isAvailable) { + jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(isAvailable); +} + describe('Filtered Search Utils', () => { describe('stripQuotes', () => { it.each` @@ -249,3 +260,79 @@ describe('urlQueryToFilter', () => { expect(res).toEqual(result); }); }); + +describe('getRecentlyUsedTokenValues', () => { + useLocalStorageSpy(); + + beforeEach(() => { + localStorage.removeItem(mockStorageKey); + }); + + it('returns array containing recently used token values from provided recentTokenValuesStorageKey', () => { + setLocalStorageAvailability(true); + + const mockExpectedArray = [{ foo: 'bar' }]; + localStorage.setItem(mockStorageKey, JSON.stringify(mockExpectedArray)); + + expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual(mockExpectedArray); + }); + + it('returns empty array when provided recentTokenValuesStorageKey does not have anything in localStorage', () => { + setLocalStorageAvailability(true); + + expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]); + }); + + it('returns empty array when when access to localStorage is not available', () => { + setLocalStorageAvailability(false); + + expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]); + }); +}); + +describe('setTokenValueToRecentlyUsed', () => { + const mockTokenValue1 = { foo: 'bar' }; + const mockTokenValue2 = { bar: 'baz' }; + useLocalStorageSpy(); + + beforeEach(() => { + localStorage.removeItem(mockStorageKey); + }); + + it('adds provided tokenValue to localStorage for recentTokenValuesStorageKey', () => { + setLocalStorageAvailability(true); + + setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); + + expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([mockTokenValue1]); + }); + + it('adds provided tokenValue to localStorage at the top of existing values (i.e. Stack order)', () => { + setLocalStorageAvailability(true); + + setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); + setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue2); + + expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([ + mockTokenValue2, + mockTokenValue1, + ]); + }); + + it('ensures that provided tokenValue is not added twice', () => { + setLocalStorageAvailability(true); + + setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); + setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); + + expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([mockTokenValue1]); + }); + + it('does not add any value when acess to localStorage is not available', () => { + setLocalStorageAvailability(false); + + setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); + + expect(JSON.parse(localStorage.getItem(mockStorageKey))).toBeNull(); + }); +}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js new file mode 100644 index 00000000000..0db47f1f189 --- /dev/null +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -0,0 +1,228 @@ +import { GlFilteredSearchToken } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { + mockRegularLabel, + mockLabels, +} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data'; + +import { DEFAULT_LABELS } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + getRecentlyUsedTokenValues, + setTokenValueToRecentlyUsed, +} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; + +import { mockLabelToken } from '../mock_data'; + +jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils'); + +const mockStorageKey = 'recent-tokens-label_name'; + +const defaultStubs = { + Portal: true, + GlFilteredSearchToken: { + template: ` + <div> + <slot name="view-token"></slot> + <slot name="view"></slot> + </div> + `, + }, + GlFilteredSearchSuggestionList: { + template: '<div></div>', + methods: { + getValue: () => '=', + }, + }, +}; + +const defaultSlots = { + 'view-token': ` + <div class="js-view-token">${mockRegularLabel.title}</div> + `, + view: ` + <div class="js-view">${mockRegularLabel.title}</div> + `, +}; + +const mockProps = { + tokenConfig: mockLabelToken, + tokenValue: { data: '' }, + tokenActive: false, + tokensListLoading: false, + tokenValues: [], + fnActiveTokenValue: jest.fn(), + defaultTokenValues: DEFAULT_LABELS, + recentTokenValuesStorageKey: mockStorageKey, + fnCurrentTokenValue: jest.fn(), +}; + +function createComponent({ + props = { ...mockProps }, + stubs = defaultStubs, + slots = defaultSlots, +} = {}) { + return mount(BaseToken, { + propsData: { + ...props, + }, + provide: { + portalName: 'fake target', + alignSuggestions: jest.fn(), + suggestionsListClass: 'custom-class', + }, + stubs, + slots, + }); +} + +describe('BaseToken', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent({ + props: { + ...mockProps, + tokenValue: { data: `"${mockRegularLabel.title}"` }, + tokenValues: mockLabels, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('data', () => { + it('calls `getRecentlyUsedTokenValues` to populate `recentTokenValues` when `recentTokenValuesStorageKey` is defined', () => { + expect(getRecentlyUsedTokenValues).toHaveBeenCalledWith(mockStorageKey); + }); + }); + + describe('computed', () => { + describe('currentTokenValue', () => { + it('calls `fnCurrentTokenValue` when it is provided', () => { + // We're disabling lint to trigger computed prop execution for this test. + // eslint-disable-next-line no-unused-vars + const { currentTokenValue } = wrapper.vm; + + expect(wrapper.vm.fnCurrentTokenValue).toHaveBeenCalledWith(`"${mockRegularLabel.title}"`); + }); + }); + + describe('activeTokenValue', () => { + it('calls `fnActiveTokenValue` when it is provided', async () => { + wrapper.setProps({ + fnCurrentTokenValue: undefined, + }); + + await wrapper.vm.$nextTick(); + + // We're disabling lint to trigger computed prop execution for this test. + // eslint-disable-next-line no-unused-vars + const { activeTokenValue } = wrapper.vm; + + expect(wrapper.vm.fnActiveTokenValue).toHaveBeenCalledWith( + mockLabels, + `"${mockRegularLabel.title.toLowerCase()}"`, + ); + }); + }); + }); + + describe('watch', () => { + describe('tokenActive', () => { + let wrapperWithTokenActive; + + beforeEach(() => { + wrapperWithTokenActive = createComponent({ + props: { + ...mockProps, + tokenActive: true, + tokenValue: { data: `"${mockRegularLabel.title}"` }, + }, + }); + }); + + afterEach(() => { + wrapperWithTokenActive.destroy(); + }); + + it('emits `fetch-token-values` event on the component when value of this prop is changed to false and `tokenValues` array is empty', async () => { + wrapperWithTokenActive.setProps({ + tokenActive: false, + }); + + await wrapperWithTokenActive.vm.$nextTick(); + + expect(wrapperWithTokenActive.emitted('fetch-token-values')).toBeTruthy(); + expect(wrapperWithTokenActive.emitted('fetch-token-values')).toEqual([ + [`"${mockRegularLabel.title}"`], + ]); + }); + }); + }); + + describe('methods', () => { + describe('handleTokenValueSelected', () => { + it('calls `setTokenValueToRecentlyUsed` when `recentTokenValuesStorageKey` is defined', () => { + const mockTokenValue = { + id: 1, + title: 'Foo', + }; + + wrapper.vm.handleTokenValueSelected(mockTokenValue); + + expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); + }); + }); + }); + + describe('template', () => { + it('renders gl-filtered-search-token component', () => { + const wrapperWithNoStubs = createComponent({ + stubs: {}, + }); + const glFilteredSearchToken = wrapperWithNoStubs.find(GlFilteredSearchToken); + + expect(glFilteredSearchToken.exists()).toBe(true); + expect(glFilteredSearchToken.props('config')).toBe(mockLabelToken); + + wrapperWithNoStubs.destroy(); + }); + + it('renders `view-token` slot when present', () => { + expect(wrapper.find('.js-view-token').exists()).toBe(true); + }); + + it('renders `view` slot when present', () => { + expect(wrapper.find('.js-view').exists()).toBe(true); + }); + + describe('events', () => { + let wrapperWithNoStubs; + + beforeEach(() => { + wrapperWithNoStubs = createComponent({ + stubs: { Portal: true }, + }); + }); + + afterEach(() => { + wrapperWithNoStubs.destroy(); + }); + + it('emits `fetch-token-values` event on component after a delay when component emits `input` event', async () => { + jest.useFakeTimers(); + + wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); + await wrapperWithNoStubs.vm.$nextTick(); + + jest.runAllTimers(); + + expect(wrapperWithNoStubs.emitted('fetch-token-values')).toBeTruthy(); + expect(wrapperWithNoStubs.emitted('fetch-token-values')[1]).toEqual(['foo']); + }); + }); + }); +}); diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index 53d204a697d..657ab8a81f0 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -289,4 +289,38 @@ RSpec.describe CommitsHelper do } end end + + describe "#commit_partial_cache_key" do + subject { helper.commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) } + + let(:commit) { create(:commit).present(current_user: user) } + let(:commit_status) { create(:commit_status) } + let(:user) { create(:user) } + let(:ref) { "master" } + let(:merge_request) { nil } + let(:request) { double(xhr?: true) } + let(:current_path) { "test" } + + before do + expect(commit).to receive(:status_for).with(ref).and_return(commit_status) + assign(:path, current_path) + end + + it { is_expected.to be_an(Array) } + it { is_expected.to include(commit) } + it { is_expected.to include(commit.author) } + it { is_expected.to include(ref) } + + it do + is_expected.to include( + { + merge_request: merge_request, + pipeline_status: Digest::SHA1.hexdigest(commit_status.to_s), + xhr: true, + controller: "commits", + path: current_path + } + ) + end + end end diff --git a/yarn.lock b/yarn.lock index 2353ff71ad5..c0d7f547a46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -867,10 +867,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== -"@gitlab/eslint-plugin@8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-8.3.0.tgz#156a375c6ab9e578ba39080932bca27006413486" - integrity sha512-AuJ6ddKVbfjVUd9DLaNLhpflThZKULWatpUuI+0RhcqyRTmcb1KL5YPxxKDlE1K+faeefgiWaGB+vSNmyNNPQQ== +"@gitlab/eslint-plugin@8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-8.4.0.tgz#094fa4d41676a71146f82e1b19257a7ceabefd88" + integrity sha512-VE/c1yIMrj2igJWAALQtAKpnXL8fN5wJ1uKteZfi8xYbWouoUK6hizXSPrrEUWiM2FqcBI4Igcpz2JlJzDlAnA== dependencies: babel-eslint "^10.0.3" eslint-config-airbnb-base "^14.2.1" @@ -881,6 +881,7 @@ eslint-plugin-jest "^23.8.2" eslint-plugin-promise "^4.2.1" eslint-plugin-vue "^7.5.0" + lodash "4.17.20" vue-eslint-parser "^7.0.0" "@gitlab/favicon-overlay@2.0.0": @@ -4142,10 +4143,10 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -4817,20 +4818,21 @@ eslint-import-resolver-node@^0.3.4: debug "^2.6.9" resolve "^1.13.1" -eslint-import-resolver-webpack@0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.0.tgz#5cb19cf4b6996c8a2514aeb10f909e2c70488dc3" - integrity sha512-hZWGcmjaJZK/WSCYGI/y4+FMGQZT+cwW/1E/P4rDwFj2PbanlQHISViw4ccDJ+2wxAqjgwBfxwy3seABbVKDEw== +eslint-import-resolver-webpack@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.1.tgz#6d2fb928091daf2da46efa1e568055555b2de902" + integrity sha512-O/8mG6AHmaKYSMb4lWxiXPpaARxOJ4rMQEHJ8vTgjS1MXooJA3KPgBPPAdOPoV17v5ML5120qod5FBLM+DtgEw== dependencies: array-find "^1.0.0" - debug "^2.6.9" + debug "^3.2.7" enhanced-resolve "^0.9.1" find-root "^1.1.0" has "^1.0.3" - interpret "^1.2.0" - lodash "^4.17.15" - node-libs-browser "^1.0.0 || ^2.0.0" - resolve "^1.13.1" + interpret "^1.4.0" + is-core-module "^2.4.0" + is-regex "^1.1.3" + lodash "^4.17.21" + resolve "^1.20.0" semver "^5.7.1" eslint-module-utils@^2.6.0: @@ -5896,10 +5898,10 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has-value@^0.3.1: version "0.3.1" @@ -6311,7 +6313,7 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@^1.2.0, interpret@^1.4.0: +interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== @@ -6409,6 +6411,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-core-module@^2.2.0, is-core-module@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" + integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -6581,13 +6590,13 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-regex@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== +is-regex@^1.1.1, is-regex@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" + integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== dependencies: call-bind "^1.0.2" - has-symbols "^1.0.1" + has-symbols "^1.0.2" is-regexp@^2.0.0: version "2.1.0" @@ -7866,6 +7875,11 @@ lodash.values@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= +lodash@4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -8592,7 +8606,7 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.2.1: +node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== @@ -10170,11 +10184,12 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== +resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: + is-core-module "^2.2.0" path-parse "^1.0.6" responselike@^1.0.2: |