diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 38bcea0df1f..42e883369a3 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -9,6 +9,9 @@ .rspec-base: extends: .rails-job-base stage: test + variables: + RUBY_GC_MALLOC_LIMIT: 67108864 + RUBY_GC_MALLOC_LIMIT_MAX: 134217728 needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"] script: # Only install knapsack after bundle install! Otherwise oddly some native diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 0fd2c01bb04..c8624e812e0 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -81,10 +81,9 @@ review-deploy: # Run seed-dast-test-data.sh only when DAST_RUN is set to true. This is to pupulate review app with data for DAST scan. # Set DAST_RUN to true when jobs are manually scheduled. - if [ "$DAST_RUN" == "true" ]; then source scripts/review_apps/seed-dast-test-data.sh; TRACE=1 trigger_proj_user_creation; fi - artifacts: paths: [environment_url.txt] - expire_in: 2 days + expire_in: 7 days when: always .review-stop-base: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index a8e0e1ccaaa..ab49f557ec0 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -132,7 +132,10 @@ .db-patterns: &db-patterns - "{,ee/}{,spec/}{db,migrations}/**/*" + - "{,ee/}{,spec/}lib/{,ee/}gitlab/database/**/*" + - "{,ee/}{,spec/}lib/{,ee/}gitlab/database{,_spec}.rb" - "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration/**/*" + - "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration{,_spec}.rb" - "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer - "{,ee/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 71f6ed34613..c37e923a787 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -65cd98f93c072f3a536021462c56e686cb2f8c7b +0fc40ef439ae4bbf91da2a5b454dfad5cb815a17 diff --git a/Gemfile b/Gemfile index 842fd630897..28e44577710 100644 --- a/Gemfile +++ b/Gemfile @@ -111,7 +111,7 @@ gem 'hamlit', '~> 2.11.0' # Files attachments gem 'carrierwave', '~> 1.3' -gem 'mini_magick' +gem 'mini_magick', '~> 4.10.1' # for backups gem 'fog-aws', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index efc95728f18..9202998b5ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -684,7 +684,7 @@ GEM mime-types-data (3.2020.0512) mimemagic (0.3.5) mini_histogram (0.1.3) - mini_magick (4.9.5) + mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.11.3) @@ -1384,7 +1384,7 @@ DEPENDENCIES memory_profiler (~> 0.9) method_source (~> 1.0) mimemagic (~> 0.3.2) - mini_magick + mini_magick (~> 4.10.1) minitest (~> 5.11.0) multi_json (~> 1.14.1) nakayoshi_fork (~> 0.0.4) diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index d61797b7ae4..3e9d77cdf6b 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,5 +1,5 @@ import Autosize from 'autosize'; -import { waitForCSSLoaded } from '../helpers/startup_css_helper'; +import { waitForCSSLoaded } from '~/helpers/startup_css_helper'; document.addEventListener('DOMContentLoaded', () => { waitForCSSLoaded(() => { diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 061c9ffe8d4..25f5483c58b 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -72,7 +72,7 @@ export default { -

+

{{ s__('Environments|Environments') }} / {{ folderName }}

diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js index 0a032eacf05..55a1ac9a8a2 100644 --- a/app/assets/javascripts/groups/members/index.js +++ b/app/assets/javascripts/groups/members/index.js @@ -11,7 +11,7 @@ export const initGroupMembersApp = (el, tableFields) => { Vue.use(Vuex); - const { members, groupId } = el.dataset; + const { members, groupId, memberPath } = el.dataset; const store = new Vuex.Store({ ...membersModule({ @@ -19,6 +19,7 @@ export const initGroupMembersApp = (el, tableFields) => { sourceId: parseInt(groupId, 10), currentUserId: gon.current_user_id || null, tableFields, + memberPath, }), }); diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 252706b3647..6dd4018f87a 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -77,13 +77,19 @@ export default class Members { $expiresInText.text(sprintf(__('Expires in %{expires_at}'), { expires_at: expiresIn })); - const { expires_soon: expiresSoon } = data; + const { expires_soon: expiresSoon, expires_at_formatted: expiresAtFormatted } = data; if (expiresSoon) { $expiresInText.addClass('text-warning'); } else { $expiresInText.removeClass('text-warning'); } + + // Update tooltip + if (expiresAtFormatted) { + $expiresInText.attr('title', expiresAtFormatted); + $expiresInText.attr('data-original-title', expiresAtFormatted); + } } else { $expiresIn.addClass('gl-display-none'); } diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 878a748e99a..3d6c9e6b297 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -55,7 +55,7 @@ export default {
diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue index 0204169214b..ab00ccdc09b 100644 --- a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue +++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue @@ -1,6 +1,11 @@ diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 983062c79f1..93fe38831be 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -1,16 +1,15 @@ diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js index 1d559dc6e41..6e68114e04b 100644 --- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import deleteMilestoneModal from './components/delete_milestone_modal.vue'; +import DeleteMilestoneModal from './components/delete_milestone_modal.vue'; import eventHub from './event_hub'; export default () => { @@ -18,6 +18,8 @@ export default () => { button.querySelector('.js-loading-icon').classList.add('hidden'); }; + const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); + const onRequestStarted = milestoneUrl => { const button = document.querySelector( `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, @@ -27,35 +29,8 @@ export default () => { eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = event => { - const button = event.currentTarget; - const modalProps = { - milestoneId: parseInt(button.dataset.milestoneId, 10), - milestoneTitle: button.dataset.milestoneTitle, - milestoneUrl: button.dataset.milestoneUrl, - issueCount: parseInt(button.dataset.milestoneIssueCount, 10), - mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), - }; - eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); - eventHub.$emit('deleteMilestoneModal.props', modalProps); - }; - - const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - deleteMilestoneButtons.forEach(button => { - button.addEventListener('click', onDeleteButtonClick); - }); - - eventHub.$once('deleteMilestoneModal.mounted', () => { - deleteMilestoneButtons.forEach(button => { - button.removeAttribute('disabled'); - }); - }); - return new Vue({ - el: '#delete-milestone-modal', - components: { - deleteMilestoneModal, - }, + el: '#js-delete-milestone-modal', data() { return { modalProps: { @@ -69,10 +44,21 @@ export default () => { }, mounted() { eventHub.$on('deleteMilestoneModal.props', this.setModalProps); - eventHub.$emit('deleteMilestoneModal.mounted'); - }, - beforeDestroy() { - eventHub.$off('deleteMilestoneModal.props', this.setModalProps); + deleteMilestoneButtons.forEach(button => { + button.removeAttribute('disabled'); + button.addEventListener('click', () => { + this.$root.$emit('bv::show::modal', 'delete-milestone-modal'); + eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); + + this.setModalProps({ + milestoneId: parseInt(button.dataset.milestoneId, 10), + milestoneTitle: button.dataset.milestoneTitle, + milestoneUrl: button.dataset.milestoneUrl, + issueCount: parseInt(button.dataset.milestoneIssueCount, 10), + mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), + }); + }); + }); }, methods: { setModalProps(modalProps) { @@ -80,7 +66,7 @@ export default () => { }, }, render(createElement) { - return createElement(deleteMilestoneModal, { + return createElement(DeleteMilestoneModal, { props: this.modalProps, }); }, diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 384216f29eb..74abd1f67a5 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,6 +1,6 @@ -import Vue from 'vue'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper'; +import Vue from 'vue'; +import { waitForCSSLoaded } from '~/helpers/startup_css_helper'; import { __ } from '~/locale'; import CodeCoverage from '../components/code_coverage.vue'; import SeriesDataMixin from './series_data_mixin'; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index e691f675e59..e582d5c3e47 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -1,16 +1,15 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue index 0688c5d3c9d..aeb2de8e2b3 100644 --- a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue +++ b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue @@ -38,12 +38,18 @@ export default { isCurrentUser() { return this.member.user?.id === this.currentUserId; }, + canRemove() { + return this.isDirectMember && this.member.canRemove; + }, }, render() { return this.$scopedSlots.default({ memberType: this.memberType, isDirectMember: this.isDirectMember, isCurrentUser: this.isCurrentUser, + permissions: { + canRemove: this.canRemove, + }, }); }, }; diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/vuex_shared/modules/members/state.js index 244a54d74d5..64e794518a2 100644 --- a/app/assets/javascripts/vuex_shared/modules/members/state.js +++ b/app/assets/javascripts/vuex_shared/modules/members/state.js @@ -1,6 +1,7 @@ -export default ({ members, sourceId, currentUserId, tableFields }) => ({ +export default ({ members, sourceId, currentUserId, tableFields, memberPath }) => ({ members, sourceId, currentUserId, tableFields, + memberPath, }); diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index ce533625d06..d13112903a5 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -12,7 +12,6 @@ @import './pages/diff'; @import './pages/editor'; @import './pages/environment_logs'; -@import './pages/environments'; @import './pages/error_details'; @import './pages/error_list'; @import './pages/error_tracking_list'; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a8cc685d880..94d0f7c999f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -244,20 +244,15 @@ } &.btn-text-field { + color: $gray-500; + justify-content: start; width: 100%; text-align: left; - padding: 6px 16px; - border-color: $border-color; - color: $gray-darkest; - background-color: $white; &:hover, &:active, &:focus { cursor: text; - box-shadow: none; - border-color: lighten($blue-300, 20%); - color: $gray-darkest; } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/page_bundles/environments.scss similarity index 91% rename from app/assets/stylesheets/pages/environments.scss rename to app/assets/stylesheets/page_bundles/environments.scss index 5ce500dad1d..8f65a626e5f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/page_bundles/environments.scss @@ -1,13 +1,4 @@ -@include media-breakpoint-down(md) { - .deployments-container { - width: 100%; - overflow: auto; - } -} - -.environments-folder-name { - font-weight: $gl-font-weight-normal; -} +@import 'page_bundles/mixins_and_variables_and_functions'; .environments-container { .ci-table { diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 8c7f156f7f8..d521267c50c 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -22,10 +22,14 @@ module MembershipActions .new(current_user, update_params) .execute(member) - member = present_members([member]).first - - respond_to do |format| - format.js { render 'shared/members/update', locals: { member: member } } + if member.expires? + render json: { + expires_in: helpers.distance_of_time_in_words_to_now(member.expires_at), + expires_soon: member.expires_soon?, + expires_at_formatted: member.expires_at.to_time.in_time_zone.to_s(:medium) + } + else + render json: {} end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 632e8db9796..49840e847f2 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -14,10 +14,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_admin_snippet!, only: [:destroy] - before_action do - push_frontend_feature_flag(:snippet_multiple_files, current_user) - end - def index @snippet_counts = ::Snippets::CountService .new(current_user, project: @project) diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 486c7f1d028..e68b821459d 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -17,10 +17,6 @@ class SnippetsController < Snippets::ApplicationController layout 'snippets' - before_action do - push_frontend_feature_flag(:snippet_multiple_files, current_user) - end - def index if params[:username].present? @user = UserFinder.new(params[:username]).find_by_username! diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 1cf3097861c..7d2229140b8 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -19,7 +19,6 @@ class Snippet < ApplicationRecord extend ::Gitlab::Utils::Override MAX_FILE_COUNT = 10 - MAX_SINGLE_FILE_COUNT = 1 cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description @@ -175,8 +174,8 @@ class Snippet < ApplicationRecord Snippet.find_by(id: id, project: project) end - def self.max_file_limit(user) - Feature.enabled?(:snippet_multiple_files, user) ? MAX_FILE_COUNT : MAX_SINGLE_FILE_COUNT + def self.max_file_limit + MAX_FILE_COUNT end def initialize(attributes = {}) diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb index abe95f5c44d..665b3657623 100644 --- a/app/presenters/snippet_blob_presenter.rb +++ b/app/presenters/snippet_blob_presenter.rb @@ -25,10 +25,6 @@ class SnippetBlobPresenter < BlobPresenter private - def snippet_multiple_files? - blob.container.repository_exists? && Feature.enabled?(:snippet_multiple_files, current_user) - end - def snippet blob.container end @@ -52,8 +48,6 @@ class SnippetBlobPresenter < BlobPresenter end def snippet_blob_raw_route(only_path: false) - return gitlab_raw_snippet_blob_url(snippet, blob.path, only_path: only_path) if snippet_multiple_files? - - gitlab_raw_snippet_url(snippet, only_path: only_path) + gitlab_raw_snippet_blob_url(snippet, blob.path, only_path: only_path) end end diff --git a/app/services/snippets/repository_validation_service.rb b/app/services/snippets/repository_validation_service.rb index 5bf5e692ef4..7e9b2eded16 100644 --- a/app/services/snippets/repository_validation_service.rb +++ b/app/services/snippets/repository_validation_service.rb @@ -52,7 +52,7 @@ module Snippets def check_file_count! file_count = repository.ls_files(snippet.default_branch).size - limit = Snippet.max_file_limit(current_user) + limit = Snippet.max_file_limit if file_count > limit raise RepositoryValidationError, _('Repository files count over the limit') diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index ed7b201323a..ad9fd250210 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -69,7 +69,7 @@ = label_tag :sort_by, _('Sort by'), class: form_item_label_css_class = render 'shared/members/sort_dropdown' - if vue_members_list_enabled - .js-group-members-list{ data: { members: members_data_json(@group, @members), **data_attributes } } + .js-group-members-list{ data: { members: members_data_json(@group, @members), member_path: group_group_member_path(id: ':id'), **data_attributes } } - else %ul.content-list.members-list{ data: { qa_selector: 'members_list' } } = render partial: 'shared/members/member', collection: @members, as: :member @@ -95,7 +95,7 @@ = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do = render 'shared/members/search_field', name: 'search_invited' - if vue_members_list_enabled - .js-group-invited-members-list{ data: { members: members_data_json(@group, @invited_members), **data_attributes } } + .js-group-invited-members-list{ data: { members: members_data_json(@group, @invited_members), member_path: group_group_member_path(id: ':id'), **data_attributes } } - else %ul.content-list.members-list = render partial: 'shared/members/member', collection: @invited_members, as: :member @@ -107,7 +107,7 @@ = render 'groups/group_members/tab_pane/title' do = html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: ''.html_safe, strong_end: ''.html_safe } - if vue_members_list_enabled - .js-group-access-requests-list{ data: { members: members_data_json(@group, @requesters), **data_attributes } } + .js-group-access-requests-list{ data: { members: members_data_json(@group, @requesters), member_path: group_group_member_path(id: ':id'), **data_attributes } } - else %ul.content-list.members-list = render partial: 'shared/members/member', collection: @requesters, as: :member diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml index 56af252d785..c4e2c1eb63d 100644 --- a/app/views/projects/environments/edit.html.haml +++ b/app/views/projects/environments/edit.html.haml @@ -1,4 +1,5 @@ - page_title _("Edit"), @environment.name, _("Environments") +- add_page_specific_style 'page_bundles/environments' %h3.page-title = _('Edit environment') diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index 554cb4323f7..2b4d19a0e1d 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -1,5 +1,6 @@ - add_to_breadcrumbs _("Environments"), project_environments_path(@project) - breadcrumb_title _("Folder/%{name}") % { name: @folder } - page_title _("Environments in %{name}") % { name: @folder } +- add_page_specific_style 'page_bundles/environments' #environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, project_path: @project.full_path } } diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 9abc1a5a925..067c987e721 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -1,4 +1,5 @@ - page_title _("Environments") +- add_page_specific_style 'page_bundles/environments' #environments-list-view{ data: { environments_data: environments_list_data, "can-read-environment" => can?(current_user, :read_environment, @project).to_s, diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 96edd3f0bd7..6b0ccc1dcc7 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,5 +1,6 @@ - breadcrumb_title _("Environments") - page_title _("New Environment") +- add_page_specific_style 'page_bundles/environments' %h3.page-title = _("New environment") diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index a774e3b61cc..5b1556c9f52 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -2,6 +2,7 @@ - breadcrumb_title @environment.name - page_title _("Environments") - add_page_specific_style 'page_bundles/xterm' +- add_page_specific_style 'page_bundles/environments' #environments-detail-view{ data: { name: @environment.name, id: @environment.id, delete_path: environment_delete_path(@environment)} } - if @environment.available? && can?(current_user, :stop_environment, @environment) diff --git a/app/views/projects/issues/export_csv/_button.html.haml b/app/views/projects/issues/export_csv/_button.html.haml index ef3fb438641..e5710fcdb60 100644 --- a/app/views/projects/issues/export_csv/_button.html.haml +++ b/app/views/projects/issues/export_csv/_button.html.haml @@ -1,4 +1,4 @@ - if current_user - %button.csv_download_link.btn.has-tooltip{ title: _('Export as CSV'), + %button.csv_download_link.btn.gl-button.has-tooltip{ title: _('Export as CSV'), data: { toggle: 'modal', target: '.issues-export-modal', qa_selector: 'export_as_csv_button' } } = sprite_icon('export') diff --git a/app/views/projects/issues/export_csv/_modal.html.haml b/app/views/projects/issues/export_csv/_modal.html.haml index 793e43da935..a73122f8181 100644 --- a/app/views/projects/issues/export_csv/_modal.html.haml +++ b/app/views/projects/issues/export_csv/_modal.html.haml @@ -18,4 +18,4 @@ .modal-text = html_escape(_('The CSV export will be created in the background. Once finished, it will be sent to %{strong_open}%{email}%{strong_close} in an attachment.')) % { email: @current_user.notification_email, strong_open: ''.html_safe, strong_close: ''.html_safe } .modal-footer - = link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" } + = link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn gl-button btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" } diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 6e81058df2a..2c52d2a5fbc 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -12,7 +12,7 @@ = _('New milestone') .milestones - #delete-milestone-modal + #js-delete-milestone-modal #promote-milestone-modal %ul.content-list diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 679a460eeb3..164d38986ec 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -40,10 +40,11 @@ = _("Requested %{time_ago}").html_safe % { time_ago: time_ago_with_tooltip(member.requested_at) } - else = _("Given access %{time_ago}").html_safe % { time_ago: time_ago_with_tooltip(member.created_at) } - - if member.expires? - ยท - %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } - = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) } + %span.js-expires-in{ class: ('gl-display-none' unless member.expires?) } + · + %span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_s(:medium) if member.expires?) } + - if member.expires? + = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) } - else = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '' diff --git a/app/views/shared/members/update.js.haml b/app/views/shared/members/update.js.haml deleted file mode 100644 index 55050bd8a15..00000000000 --- a/app/views/shared/members/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- member = local_assigns.fetch(:member) - -:plain - var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}'); - $("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); - gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}")); diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml index e00a10398d3..7a813e110c4 100644 --- a/app/views/shared/milestones/_delete_button.html.haml +++ b/app/views/shared/milestones/_delete_button.html.haml @@ -1,8 +1,6 @@ - milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone) -%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal', - target: '#delete-milestone-modal', - milestone_id: @milestone.id, +%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { milestone_id: @milestone.id, milestone_title: markdown_field(@milestone, :title), milestone_url: milestone_url, milestone_issue_count: @milestone.issues.count, @@ -11,4 +9,4 @@ = _('Delete') .spinner.js-loading-icon.hidden -#delete-milestone-modal +#js-delete-milestone-modal diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml index 5fef56f7fc3..27a22689b84 100644 --- a/app/views/sherlock/file_samples/show.html.haml +++ b/app/views/sherlock/file_samples/show.html.haml @@ -5,7 +5,7 @@ .row-content-block .float-right - = link_to(sherlock_transaction_path(@transaction), class: 'btn') do + = link_to(sherlock_transaction_path(@transaction), class: 'btn gl-button') do = sprite_icon('arrow-left') = t('sherlock.transaction') .oneline diff --git a/changelogs/unreleased/-231200-projects-issues-export_csv.yml b/changelogs/unreleased/-231200-projects-issues-export_csv.yml new file mode 100644 index 00000000000..90f30207926 --- /dev/null +++ b/changelogs/unreleased/-231200-projects-issues-export_csv.yml @@ -0,0 +1,5 @@ +--- +title: Apply gl-button class to projects/issues/export_csv directory +merge_request: 44106 +author: Lakshit +type: other diff --git a/changelogs/unreleased/-231208-sherlock-file_samples.yml b/changelogs/unreleased/-231208-sherlock-file_samples.yml new file mode 100644 index 00000000000..69402e37ae0 --- /dev/null +++ b/changelogs/unreleased/-231208-sherlock-file_samples.yml @@ -0,0 +1,5 @@ +--- +title: Apply GitLab UI button styles to buttons in app/views/sherlock/file_samples +merge_request: 44109 +author: Lakshit +type: other diff --git a/changelogs/unreleased/217809-fj-remove-snippet-multiple-files-ff.yml b/changelogs/unreleased/217809-fj-remove-snippet-multiple-files-ff.yml new file mode 100644 index 00000000000..1ce966770a1 --- /dev/null +++ b/changelogs/unreleased/217809-fj-remove-snippet-multiple-files-ff.yml @@ -0,0 +1,5 @@ +--- +title: Enable snippet multiple files +merge_request: 43246 +author: +type: added diff --git a/changelogs/unreleased/241058-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-app-assets-javas.yml b/changelogs/unreleased/241058-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-app-assets-javas.yml new file mode 100644 index 00000000000..cb99207fffe --- /dev/null +++ b/changelogs/unreleased/241058-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-app-assets-javas.yml @@ -0,0 +1,6 @@ +--- +title: Replacing deprecated Bootstrap button with GlButton and updating btn-text-field + class to align with styles +merge_request: 41430 +author: +type: other diff --git a/changelogs/unreleased/258206-enable-gitpod-flag-by-default.yml b/changelogs/unreleased/258206-enable-gitpod-flag-by-default.yml new file mode 100644 index 00000000000..a55fac23c32 --- /dev/null +++ b/changelogs/unreleased/258206-enable-gitpod-flag-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable Gitpod button on file tree view +merge_request: 43961 +author: +type: added diff --git a/changelogs/unreleased/Migrate-deprecated-delete-milestone-modal-ab.yml b/changelogs/unreleased/Migrate-deprecated-delete-milestone-modal-ab.yml new file mode 100644 index 00000000000..2bc425b2e12 --- /dev/null +++ b/changelogs/unreleased/Migrate-deprecated-delete-milestone-modal-ab.yml @@ -0,0 +1,5 @@ +--- +title: Migrate DeprecatedModal to GitLab UI Modal +merge_request: 42113 +author: +type: changed diff --git a/changelogs/unreleased/id-update-mini-magick.yml b/changelogs/unreleased/id-update-mini-magick.yml new file mode 100644 index 00000000000..71d2503478b --- /dev/null +++ b/changelogs/unreleased/id-update-mini-magick.yml @@ -0,0 +1,5 @@ +--- +title: Bump mini_magick gem version +merge_request: 44450 +author: +type: other diff --git a/config/application.rb b/config/application.rb index 3b9f7d4e553..abc215eb5c2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require_relative 'boot' # Based on https://github.com/rails/rails/blob/v6.0.1/railties/lib/rails/all.rb @@ -188,6 +189,7 @@ module Gitlab config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css" config.assets.precompile << "page_bundles/boards.css" config.assets.precompile << "page_bundles/cycle_analytics.css" + config.assets.precompile << "page_bundles/environments.css" config.assets.precompile << "page_bundles/ide.css" config.assets.precompile << "page_bundles/issues_list.css" config.assets.precompile << "page_bundles/jira_connect.css" diff --git a/config/feature_flags/development/gitpod.yml b/config/feature_flags/development/gitpod.yml index 148ea7294ba..d1d9f35d4d4 100644 --- a/config/feature_flags/development/gitpod.yml +++ b/config/feature_flags/development/gitpod.yml @@ -1,7 +1,7 @@ --- name: gitpod introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37985 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258206 group: group::editor type: development -default_enabled: false \ No newline at end of file +default_enabled: true diff --git a/config/feature_flags/development/snippet_multiple_files.yml b/config/feature_flags/development/snippet_multiple_files.yml deleted file mode 100644 index f495c3d4e64..00000000000 --- a/config/feature_flags/development/snippet_multiple_files.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: snippet_multiple_files -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32416 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/217809 -group: group::editor -type: development -default_enabled: false diff --git a/config/object_store_settings.rb b/config/object_store_settings.rb index 0d346135463..767fcd7579c 100644 --- a/config/object_store_settings.rb +++ b/config/object_store_settings.rb @@ -1,8 +1,12 @@ # Set default values for object_store settings class ObjectStoreSettings - SUPPORTED_TYPES = %w(artifacts external_diffs lfs uploads packages dependency_proxy terraform_state).freeze + SUPPORTED_TYPES = %w(artifacts external_diffs lfs uploads packages dependency_proxy terraform_state pages).freeze ALLOWED_OBJECT_STORE_OVERRIDES = %w(bucket enabled proxy_download).freeze + # pages may be enabled but use legacy disk storage + # we don't need to raise an error in that case + ALLOWED_INCOMPLETE_TYPES = %w(pages).freeze + attr_accessor :settings # Legacy parser @@ -115,7 +119,9 @@ class ObjectStoreSettings next unless section - raise "Object storage for #{store_type} must have a bucket specified" if section['enabled'] && target_config['bucket'].blank? + if section['enabled'] && target_config['bucket'].blank? + missing_bucket_for(store_type) + end # Map bucket (external name) -> remote_directory (internal representation) target_config['remote_directory'] = target_config.delete('bucket') @@ -152,4 +158,14 @@ class ObjectStoreSettings true end + + def missing_bucket_for(store_type) + message = "Object storage for #{store_type} must have a bucket specified" + + if ALLOWED_INCOMPLETE_TYPES.include?(store_type) + warn "[WARNING] #{message}" + else + raise message + end + end end diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index d48a47e9645..25bfc3c132d 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -1,77 +1,75 @@ --- -stage: Verify -group: Continuous Integration +stage: Enablement +group: Distribution 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/#designated-technical-writers type: reference --- -# Environment Variables +# Environment variables GitLab exposes certain environment variables which can be used to override their defaults values. -People usually configure GitLab via `/etc/gitlab/gitlab.rb` for Omnibus +People usually configure GitLab with `/etc/gitlab/gitlab.rb` for Omnibus installations, or `gitlab.yml` for installations from source. -Below you will find the supported environment variables which you can use to -override certain values. +You can use the following environment variables to override certain values: ## Supported environment variables -Variable | Type | Description --------- | ---- | ----------- -`ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable) -`GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (e.g. `//mycdnsubdomain.fictional-cdn.com`) -`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation -`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) -`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` -`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` -`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab -`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer -`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer -`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for runners -`UNSTRUCTURED_RAILS_LOG` | string | Enables the unstructured log in addition to JSON logs (defaults to `true`) +| Variable | Type | Description | +|--------------------------------------------|---------|---------------------------------------------------------------------------------------------------------| +| `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`. | +| `ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable). | +| `GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (for example, `//mycdnsubdomain.fictional-cdn.com`). | +| `GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the **From** field in emails sent by GitLab. | +| `GITLAB_EMAIL_FROM` | string | The email address used in the **From** field in emails sent by GitLab. | +| `GITLAB_EMAIL_REPLY_TO` | string | The email address used in the **Reply-To** field in emails sent by GitLab. | +| `GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The email subject suffix used in emails sent by GitLab. | +| `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`). | +| `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation. | +| `GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for runners. | +| `GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). | +| `GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). | +| `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging`, or `test`. | +| `UNSTRUCTURED_RAILS_LOG` | string | Enables the unstructured log in addition to JSON logs (defaults to `true`). | ## Complete database variables -The recommended way of specifying your database connection information is to set -the `DATABASE_URL` environment variable. This variable only holds connection -information (`adapter`, `database`, `username`, `password`, `host` and `port`), -but not behavior information (`encoding`, `pool`). If you don't want to use -`DATABASE_URL` and/or want to set database behavior information, you will have -to either: +The recommended method for specifying your database connection information is +to set the `DATABASE_URL` environment variable. This variable contains +connection information (`adapter`, `database`, `username`, `password`, `host`, +and `port`), but no behavior information (`encoding` or `pool`). If you don't +want to use `DATABASE_URL`, or want to set database behavior information, +either: -- copy our template file: `cp config/database.yml.env config/database.yml`, or -- set a value for some `GITLAB_DATABASE_XXX` variables +- Copy the template file, `cp config/database.yml.env config/database.yml`. +- Set a value for some `GITLAB_DATABASE_XXX` variables. The list of `GITLAB_DATABASE_XXX` variables that you can set is: -Variable | Default value | Overridden by `DATABASE_URL`? --------- | ------------- | ----------------------------- -`GITLAB_DATABASE_ADAPTER` | `postgresql` | Yes -`GITLAB_DATABASE_DATABASE` | `gitlab_#{ENV['RAILS_ENV']` | Yes -`GITLAB_DATABASE_USERNAME` | `root` | Yes -`GITLAB_DATABASE_PASSWORD` | None | Yes -`GITLAB_DATABASE_HOST` | `localhost` | Yes -`GITLAB_DATABASE_PORT` | `5432` | Yes -`GITLAB_DATABASE_ENCODING` | `unicode` | No -`GITLAB_DATABASE_POOL` | `10` | No +| Variable | Default value | Overridden by `DATABASE_URL`? | +|-----------------------------|--------------------------------|-------------------------------| +| `GITLAB_DATABASE_ADAPTER` | `postgresql` | **{check-circle}** Yes | +| `GITLAB_DATABASE_DATABASE` | `gitlab_#{ENV['RAILS_ENV']` | **{check-circle}** Yes | +| `GITLAB_DATABASE_ENCODING` | `unicode` | **{dotted-circle}** No | +| `GITLAB_DATABASE_HOST` | `localhost` | **{check-circle}** Yes | +| `GITLAB_DATABASE_PASSWORD` | _none_ | **{check-circle}** Yes | +| `GITLAB_DATABASE_POOL` | `10` | **{dotted-circle}** No | +| `GITLAB_DATABASE_PORT` | `5432` | **{check-circle}** Yes | +| `GITLAB_DATABASE_USERNAME` | `root` | **{check-circle}** Yes | ## Adding more variables -We welcome merge requests to make more settings configurable via variables. -Please make changes in the `config/initializers/1_settings.rb` file and stick -to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. +We welcome merge requests to make more settings configurable by using variables. +Make changes to the `config/initializers/1_settings.rb` file, and use the +naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. ## Omnibus configuration -To set environment variables, follow [these -instructions](https://docs.gitlab.com/omnibus/settings/environment-variables.html). +To set environment variables, follow [these instructions](https://docs.gitlab.com/omnibus/settings/environment-variables.html). It's possible to preconfigure the GitLab Docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. -For more information see the [Pre-configure Docker container](https://docs.gitlab.com/omnibus/docker/#pre-configure-docker-container) -section in the Omnibus documentation. +For more information, see the [Pre-configure Docker container](https://docs.gitlab.com/omnibus/docker/#pre-configure-docker-container) +section of the Omnibus GitLab documentation. diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index e262a2c4fbe..3b01752c373 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -495,7 +495,7 @@ addresses and names, do use: - **Email addresses**: Use an email address ending in `example.com`. - **Names**: Use strings like `example_username`. Alternatively, use diverse or non-gendered names with common surnames, such as `Sidney Jones`, `Zhang Wei`, - or `Maria Garcia`. + or `Alex Garcia`. ### Fake URLs diff --git a/doc/development/telemetry/snowplow.md b/doc/development/telemetry/snowplow.md index d6e3f0de2c8..f84cea1124d 100644 --- a/doc/development/telemetry/snowplow.md +++ b/doc/development/telemetry/snowplow.md @@ -40,7 +40,7 @@ Snowplow is an enterprise-grade marketing and product analytics platform which h We have many definitions of Snowplow's schema. We have an active issue to [standardize this schema](https://gitlab.com/gitlab-org/gitlab/-/issues/207930) including the following definitions: - Frontend and backend taxonomy as listed below -- [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy) +- [Structured event taxonomy](#structured-event-taxonomy) - [Self describing events](https://github.com/snowplow/snowplow/wiki/Custom-events#self-describing-events) - [Iglu schema](https://gitlab.com/gitlab-org/iglu/) - [Snowplow authored events](https://github.com/snowplow/snowplow/wiki/Snowplow-authored-events) @@ -96,15 +96,29 @@ sequenceDiagram Snowflake DW->>Sisense Dashboards: Data available for querying ``` +## Structured event taxonomy + +When adding new click events, we should add them in a way that's internally consistent. If we don't, it'll be very painful to perform analysis across features since each feature will be capturing events differently. + +The current method provides several attributes that are sent on each click event. Please try to follow these guidelines when specifying events to capture: + +| attribute | type | required | description | +| --------- | ------- | -------- | ----------- | +| category | text | true | The page or backend area of the application. Unless infeasible, please use the Rails page attribute by default in the frontend, and namespace + classname on the backend. | +| action | text | true | The action the user is taking, or aspect that's being instrumented. The first word should always describe the action or aspect: clicks should be `click`, activations should be `activate`, creations should be `create`, etc. Use underscores to describe what was acted on; for example, activating a form field would be `activate_form_input`. An interface action like clicking on a dropdown would be `click_dropdown`, while a behavior like creating a project record from the backend would be `create_project` | +| label | text | false | The specific element, or object that's being acted on. This is either the label of the element (e.g. a tab labeled 'Create from template' may be `create_from_template`) or a unique identifier if no text is available (e.g. closing the Groups dropdown in the top navbar might be `groups_dropdown_close`), or it could be the name or title attribute of a record being created. | +| 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). | + ## 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 utilize tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). +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 utilize 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). | 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`. | -| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). | +| `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) @@ -131,10 +145,10 @@ Below is a list of supported `data-track-*` attributes: | attribute | required | description | |:----------------------|:---------|:------------| | `data-track-event` | true | Action the user is taking. Clicks must be prepended with `click` and activations must be prepended with `activate`. For example, focusing a form field would be `activate_form_input` and clicking a button would be `click_button`. | -| `data-track-label` | false | The `label` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). | -| `data-track-property` | false | The `property` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). | -| `data-track-value` | false | The `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=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 Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). | +| `data-track-label` | false | The `label` as described in our [Structured event taxonomy](#structured-event-taxonomy). | +| `data-track-property` | false | The `property` as described in our [Structured event taxonomy](#structured-event-taxonomy). | +| `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). | ### Tracking within Vue components @@ -278,7 +292,7 @@ Custom event tracking and instrumentation can be added by directly calling the ` |:-----------|:-------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `category` | string | 'application' | Area or aspect of the application. This could be `HealthCheckController` or `Lfs::FileTransformer` for instance. | | `action` | string | 'generic' | The action being taken, which can be anything from a controller action like `create` to something like an Active Record callback. | -| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described in [Instrumentation at GitLab](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#sts=Taxonomy). These are set as empty strings if you don't provide them. | +| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described in [Structured event taxonomy](#structured-event-taxonomy). These are set as empty strings if you don't provide them. | Tracking can be viewed as either tracking user behavior, or can be utilized for instrumentation to monitor and visualize performance over time in an area or aspect of code. diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 53aa84cffcb..6b298c2e791 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -484,17 +484,22 @@ This will result in only one `Project`, `User`, and `ProjectMember` created for is handled automatically using a transaction rollback. Note that if you modify an object defined inside a `let_it_be` block, -then you will need to reload the object as needed, or specify the `reload` -option to reload for every example. +then you must do one of the following: + +- Reload the object as needed. +- Use the `let_it_be_with_reload` alias. +- Specify the `reload` option to reload for every example. ```ruby +let_it_be_with_reload(:project) { create(:project) } let_it_be(:project, reload: true) { create(:project) } ``` -You can also specify the `refind` option as well to completely load a -new object. +You can also use the `let_it_be_with_refind` alias, or specify the `refind` +option as well to completely load a new object. ```ruby +let_it_be_with_refind(:project) { create(:project) } let_it_be(:project, refind: true) { create(:project) } ``` diff --git a/doc/integration/gitpod.md b/doc/integration/gitpod.md index bd038eaf47b..83fb1e10d04 100644 --- a/doc/integration/gitpod.md +++ b/doc/integration/gitpod.md @@ -8,7 +8,8 @@ info: "To determine the technical writer assigned to the Stage/Group associated # Gitpod Integration > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228893) in GitLab 13.4. -> - It's [deployed behind a feature flag](#enable-or-disable-the-gitpod-integration), disabled by default. +> - It was [deployed behind a feature flag](#enable-or-disable-the-gitpod-integration), disabled by default. +> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/258206) in GitLab 13.5. > - It's enabled on GitLab.com. > - It's recommended for production use. > - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#configure-your-gitlab-instance-with-gitpod). **(CORE ONLY)** @@ -57,19 +58,18 @@ and get your instance up and running. ## Enable or disable the Gitpod integration **(CORE ONLY)** -The Gitpod integration is under development and not ready for production use. It is deployed behind a -feature flag that is **disabled by default**. +The Gitpod integration is deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) -can enable it. - -To enable it: - -```ruby -Feature.enable(:gitpod) -``` +can enable or disable it. To disable it: ```ruby Feature.disable(:gitpod) ``` + +To enable it: + +```ruby +Feature.enable(:gitpod) +``` diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index 45083dfeb64..bd6be48ecef 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -391,6 +391,7 @@ The following table lists variables used to disable jobs. | `REVIEW_DISABLED` | From GitLab 11.0, used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs won't be created. | | `SAST_DISABLED` | From GitLab 11.0, used to disable the `sast` job. If the variable is present, the job won't be created. | | `TEST_DISABLED` | From GitLab 11.0, used to disable the `test` job. If the variable is present, the job won't be created. | +| `SECRET_DETECTION_DISABLED` | From GitLab 13.1, used to disable the `secret_detection` job. If the variable is present, the job won't be created. | ### Application secret variables diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 769894278cb..1a6956284ab 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -1126,7 +1126,7 @@ to how much it can scale, and as it is a single instance deployment, you will ex when upgrading the Vault application. To optimally use Vault in a production environment, it's ideal to have a good understanding -of the internals of Vault and how to configure it. This can be done by reading the +of the internals of Vault and how to configure it. This can be done by reading the [Vault Configuration guide](../../ci/secrets/#configure-your-vault-server), [the Vault documentation](https://www.vaultproject.io/docs/internals) as well as the Vault Helm chart [`values.yaml` file](https://github.com/hashicorp/vault-helm/blob/v0.3.3/values.yaml). diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index c1b2b16e39d..edd097d04c2 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -356,6 +356,13 @@ with your personal access token or deploy token): //gitlab.com/api/v4/projects/:_authToken= ``` +You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically: + +```shell +yarn config set '//gitlab.com/api/v4/projects//packages/npm/:_authToken' "" +yarn config set '//gitlab.com/api/v4/packages/npm/:_authToken' "" +``` + ### `npm publish` targets default NPM registry (`registry.npmjs.org`) Ensure that your package scope is set consistently in your `package.json` and `.npmrc` files. diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md index 21f216aa50a..d961e4bafa3 100644 --- a/doc/user/project/clusters/add_remove_clusters.md +++ b/doc/user/project/clusters/add_remove_clusters.md @@ -19,9 +19,12 @@ and learn how to spin up a Kubernetes cluster managed by Google Cloud Platform ( in a few clicks. TIP: **Tip:** -Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial), -and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's -Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form) and apply for credit. +Every new Google Cloud Platform (GCP) account receives +[$300 in credit upon sign up](https://console.cloud.google.com/freetrial). +In partnership with Google, GitLab is able to offer an additional $200 for new GCP +accounts to get started with GitLab's Google Kubernetes Engine Integration. +[Follow this link](https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form) +to apply for credit. ## Before you begin @@ -30,7 +33,7 @@ Before [adding a Kubernetes cluster](#create-new-cluster) using GitLab, you need - GitLab itself. Either: - A [GitLab.com account](https://about.gitlab.com/pricing/#gitlab-com). - A [self-managed installation](https://about.gitlab.com/pricing/#self-managed) with GitLab version - 12.5 or later. This will ensure the GitLab UI can be used for cluster creation. + 12.5 or later. This ensures the GitLab UI can be used for cluster creation. - The following GitLab access: - [Maintainer access to a project](../../permissions.md#project-members-permissions) for a project-level cluster. @@ -41,14 +44,12 @@ Before [adding a Kubernetes cluster](#create-new-cluster) using GitLab, you need ## Access controls -When creating a cluster in GitLab, you will be asked if you would like to create either: +When creating a cluster in GitLab, you are asked if you would like to create either: -- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) cluster. +- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) + cluster, which is the GitLab default and recommended option. - An [Attribute-based access control (ABAC)](https://kubernetes.io/docs/reference/access-authn-authz/abac/) cluster. -NOTE: **Note:** -[RBAC](#rbac-cluster-resources) is recommended and the GitLab default. - GitLab creates the necessary service accounts and privileges to install and run [GitLab managed applications](index.md#installing-applications). When GitLab creates the cluster, a `gitlab` service account with `cluster-admin` privileges is created in the `default` namespace @@ -59,10 +60,10 @@ Restricted service account for deployment was [introduced](https://gitlab.com/gi The first time you install an application into your cluster, the `tiller` service account is created with `cluster-admin` privileges in the -`gitlab-managed-apps` namespace. This service account will be used by Helm to +`gitlab-managed-apps` namespace. This service account is used by Helm to install and run [GitLab managed applications](index.md#installing-applications). -Helm will also create additional service accounts and other resources for each +Helm also creates additional service accounts and other resources for each installed application. Consult the documentation of the Helm charts for each application for details. @@ -77,7 +78,7 @@ Note the following about access controls: - Environment-specific resources are only created if your cluster is [managed by GitLab](index.md#gitlab-managed-clusters). -- If your cluster was created before GitLab 12.2, it will use a single namespace for all project +- If your cluster was created before GitLab 12.2, it uses a single namespace for all project environments. ### RBAC cluster resources @@ -181,7 +182,7 @@ To add a Kubernetes cluster to your project, group, or instance: kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}' ``` - 1. **CA certificate** (required) - A valid Kubernetes certificate is needed to authenticate to the cluster. We will use the certificate created by default. + 1. **CA certificate** (required) - A valid Kubernetes certificate is needed to authenticate to the cluster. We use the certificate created by default. 1. List the secrets with `kubectl get secrets`, and one should be named similar to `default-token-xxxxx`. Copy that token name for use below. 1. Get the certificate by running this command: @@ -193,17 +194,17 @@ To add a Kubernetes cluster to your project, group, or instance: NOTE: **Note:** If the command returns the entire certificate chain, you must copy the Root CA certificate and any intermediate certificates at the bottom of the chain. - A chain file has following structure: + A chain file has following structure: ```plaintext - -----BEGIN MY CERTIFICATE----- - -----END MY CERTIFICATE----- - -----BEGIN INTERMEDIATE CERTIFICATE----- - -----END INTERMEDIATE CERTIFICATE----- - -----BEGIN INTERMEDIATE CERTIFICATE----- - -----END INTERMEDIATE CERTIFICATE----- - -----BEGIN ROOT CERTIFICATE----- - -----END ROOT CERTIFICATE----- + -----BEGIN MY CERTIFICATE----- + -----END MY CERTIFICATE----- + -----BEGIN INTERMEDIATE CERTIFICATE----- + -----END INTERMEDIATE CERTIFICATE----- + -----BEGIN INTERMEDIATE CERTIFICATE----- + -----END INTERMEDIATE CERTIFICATE----- + -----BEGIN ROOT CERTIFICATE----- + -----END ROOT CERTIFICATE----- ``` 1. **Token** - @@ -241,10 +242,10 @@ To add a Kubernetes cluster to your project, group, or instance: kubectl apply -f gitlab-admin-service-account.yaml ``` - You will need the `container.clusterRoleBindings.create` permission + You need the `container.clusterRoleBindings.create` permission to create cluster-level roles. If you do not have this permission, you can alternatively enable Basic Authentication and then run the - `kubectl apply` command as an admin: + `kubectl apply` command as an administrator: ```shell kubectl apply -f gitlab-admin-service-account.yaml --username=admin --password= @@ -286,7 +287,7 @@ To add a Kubernetes cluster to your project, group, or instance: ``` NOTE: **Note:** - For GKE clusters, you will need the + For GKE clusters, you need the `container.clusterRoleBindings.create` permission to create a cluster role binding. You can follow the [Google Cloud documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access) @@ -295,7 +296,7 @@ To add a Kubernetes cluster to your project, group, or instance: 1. **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster. See the [Managed clusters section](index.md#gitlab-managed-clusters) for more information. 1. **Project namespace** (optional) - You don't have to fill it in; by leaving - it blank, GitLab will create one for you. Also: + it blank, GitLab creates one for you. Also: - Each project should have a unique namespace. - The project namespace is not necessarily the namespace of the secret, if you're using a secret with broader permissions, like the secret from `default`. @@ -306,19 +307,19 @@ To add a Kubernetes cluster to your project, group, or instance: 1. Finally, click the **Create Kubernetes cluster** button. -After a couple of minutes, your cluster will be ready to go. You can now proceed +After a couple of minutes, your cluster is ready. You can now proceed to install some [pre-defined applications](index.md#installing-applications). #### Disable Role-Based Access Control (RBAC) (optional) When connecting a cluster via GitLab integration, you may specify whether the -cluster is RBAC-enabled or not. This will affect how GitLab interacts with the +cluster is RBAC-enabled or not. This affects how GitLab interacts with the cluster for certain operations. If you did *not* check the **RBAC-enabled cluster** -checkbox at creation time, GitLab will assume RBAC is disabled for your cluster +checkbox at creation time, GitLab assumes RBAC is disabled for your cluster when interacting with it. If so, you must disable RBAC on your cluster for the integration to work properly. -![rbac](img/rbac_v13_1.png) +![RBAC](img/rbac_v13_1.png) NOTE: **Note:** Disabling RBAC means that any application running in the cluster, @@ -368,3 +369,12 @@ When removing the cluster integration, note: To learn more on automatically deploying your applications, read about [Auto DevOps](../../../topics/autodevops/index.md). + +## Troubleshooting + +### There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid + +If you encounter this error while adding a Kubernetes cluster, ensure you're +properly pasting the service token. Some shells may add a line break to the +service token, making it invalid. Ensure that there are no line breaks by +pasting your token into an editor and removing any additional spaces. diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb index 40488eb882d..85148c03d18 100644 --- a/lib/api/entities/snippet.rb +++ b/lib/api/entities/snippet.rb @@ -17,7 +17,7 @@ module API expose :file_name do |snippet| snippet.file_name_on_repo || snippet.file_name end - expose :files, if: ->(snippet, options) { snippet_multiple_files?(snippet, options[:current_user]) } do |snippet, options| + expose :files do |snippet, options| snippet.list_files.map do |file| { path: file, @@ -25,10 +25,6 @@ module API } end end - - def snippet_multiple_files?(snippet, current_user) - ::Feature.enabled?(:snippet_multiple_files, current_user) && snippet.repository_exists? - end end end end diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb index 9224381735f..42f56680ded 100644 --- a/lib/api/helpers/snippets_helpers.rb +++ b/lib/api/helpers/snippets_helpers.rb @@ -93,7 +93,7 @@ module API def validate_params_for_multiple_files(snippet) return unless params[:content] || params[:file_name] - if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files? + if snippet.multiple_files? render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400) end end diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb index 0450c043831..710e2ce90ec 100644 --- a/lib/gitlab/git_access_snippet.rb +++ b/lib/gitlab/git_access_snippet.rb @@ -115,7 +115,7 @@ module Gitlab override :check_single_change_access def check_single_change_access(change, _skip_lfs_integrity_check: false) Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate! - Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate! + Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit, logger: logger).validate! rescue Checks::TimedLogger::TimeoutError raise TimeoutError, logger.full_message end diff --git a/lib/gitlab/gitpod.rb b/lib/gitlab/gitpod.rb index 11b54db72ea..e35fb8fed02 100644 --- a/lib/gitlab/gitpod.rb +++ b/lib/gitlab/gitpod.rb @@ -3,17 +3,13 @@ module Gitlab class Gitpod class << self - def feature_conditional? - feature.conditional? - end - def feature_available? # The gitpod_bundle feature could be conditionally applied, so check if `!off?` - !feature.off? + !feature.off? || feature_enabled? end def feature_enabled?(actor = nil) - feature.enabled?(actor) + Feature.enabled?(:gitpod, actor, default_enabled: true) end def feature_and_settings_enabled?(actor = nil) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a5c0abf29c4..2b918a14d33 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4338,6 +4338,9 @@ msgstr "" msgid "Burndown chart" msgstr "" +msgid "Burndown charts are now fixed. This means that removing issues from a milestone after it has expired won't affect the chart. You can view the old chart using the %{strongStart}Legacy burndown chart%{strongEnd} button." +msgstr "" + msgid "BurndownChartLabel|Open issue weight" msgstr "" @@ -11347,6 +11350,9 @@ msgstr "" msgid "First seen" msgstr "" +msgid "Fixed burndown chart" +msgstr "" + msgid "Fixed date" msgstr "" @@ -15013,6 +15019,9 @@ msgstr "" msgid "Leave zen mode" msgstr "" +msgid "Legacy burndown chart" +msgstr "" + msgid "Let's Encrypt does not accept emails on example.com" msgstr "" @@ -15840,6 +15849,21 @@ msgstr "" msgid "Members|%{time} by %{user}" msgstr "" +msgid "Members|Are you sure you want to deny %{usersName}'s request to join \"%{source}\"" +msgstr "" + +msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\"" +msgstr "" + +msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\"" +msgstr "" + +msgid "Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join \"%{source}\"" +msgstr "" + +msgid "Members|Are you sure you want to withdraw your access request for \"%{source}\"" +msgstr "" + msgid "Members|Expired" msgstr "" @@ -15849,6 +15873,15 @@ msgstr "" msgid "Members|in %{time}" msgstr "" +msgid "Member|Deny access" +msgstr "" + +msgid "Member|Remove member" +msgstr "" + +msgid "Member|Revoke invite" +msgstr "" + msgid "Memory Usage" msgstr "" @@ -16008,7 +16041,7 @@ msgstr "" msgid "MergeRequests|Jump to next unresolved thread" msgstr "" -msgid "MergeRequests|Reply..." +msgid "MergeRequests|Reply" msgstr "" msgid "MergeRequests|Resolve this thread in a new issue" diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb index 376ce51273e..50f2f4789fa 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb @@ -1,16 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :requires_admin do + RSpec.describe 'Create' do describe 'Multiple file snippet' do - before do - Runtime::Feature.enable('snippet_multiple_files') - end - - after do - Runtime::Feature.disable('snippet_multiple_files') - end - it 'creates a personal snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/842' do Flow::Login.sign_in diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb index 36938c74afa..430ba1205cb 100644 --- a/spec/config/object_store_settings_spec.rb +++ b/spec/config/object_store_settings_spec.rb @@ -24,6 +24,7 @@ RSpec.describe ObjectStoreSettings do 'lfs' => { 'enabled' => true }, 'artifacts' => { 'enabled' => true }, 'external_diffs' => { 'enabled' => false }, + 'pages' => { 'enabled' => true }, 'object_store' => { 'enabled' => true, 'connection' => connection, @@ -39,6 +40,9 @@ RSpec.describe ObjectStoreSettings do 'external_diffs' => { 'bucket' => 'external_diffs', 'enabled' => false + }, + 'pages' => { + 'bucket' => 'pages' } } } @@ -64,6 +68,11 @@ RSpec.describe ObjectStoreSettings do expect(settings.lfs['object_store']['proxy_download']).to be true expect(settings.lfs['object_store']['remote_directory']).to eq('lfs-objects') + expect(settings.pages['enabled']).to be true + expect(settings.pages['object_store']['enabled']).to be true + expect(settings.pages['object_store']['connection']).to eq(connection) + expect(settings.pages['object_store']['remote_directory']).to eq('pages') + expect(settings.external_diffs['enabled']).to be false expect(settings.external_diffs['object_store']['enabled']).to be false expect(settings.external_diffs['object_store']['remote_directory']).to eq('external_diffs') @@ -75,6 +84,12 @@ RSpec.describe ObjectStoreSettings do expect { subject }.to raise_error(/Object storage for lfs must have a bucket specified/) end + it 'does not raise error if pages bucket is missing' do + config['object_store']['objects']['pages'].delete('bucket') + + expect { subject }.not_to raise_error + end + context 'with legacy config' do let(:legacy_settings) do { diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 4b9dd3629f1..5425a437c80 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -233,6 +233,42 @@ RSpec.describe Groups::GroupMembersController do end end end + + context 'expiration date' do + let(:expiry_date) { 1.month.from_now.to_date } + + before do + travel_to Time.now.utc.beginning_of_day + + put( + :update, + params: { + group_member: { expires_at: expiry_date }, + group_id: group, + id: requester + }, + format: :json + ) + end + + context 'when `expires_at` is set' do + it 'returns correct json response' do + expect(json_response).to eq({ + "expires_in" => "about 1 month", + "expires_soon" => false, + "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium) + }) + end + end + + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } + + it 'returns empty json response' do + expect(json_response).to be_empty + end + end + end end describe 'DELETE destroy' do @@ -441,7 +477,7 @@ RSpec.describe Groups::GroupMembersController do group_id: group, id: membership }, - format: :js + format: :json expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index ae05e2d2631..4755dab9967 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -228,6 +228,43 @@ RSpec.describe Projects::ProjectMembersController do end end end + + context 'expiration date' do + let(:expiry_date) { 1.month.from_now.to_date } + + before do + travel_to Time.now.utc.beginning_of_day + + put( + :update, + params: { + project_member: { expires_at: expiry_date }, + namespace_id: project.namespace, + project_id: project, + id: requester + }, + format: :json + ) + end + + context 'when `expires_at` is set' do + it 'returns correct json response' do + expect(json_response).to eq({ + "expires_in" => "about 1 month", + "expires_soon" => false, + "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium) + }) + end + end + + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } + + it 'returns empty json response' do + expect(json_response).to be_empty + end + end + end end describe 'DELETE destroy' do diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb index d94cc85f411..dd708c243a8 100644 --- a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb @@ -6,65 +6,66 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js include Select2Helper include ActiveSupport::Testing::TimeHelpers - let(:user1) { create(:user, name: 'John Doe') } - let!(:new_member) { create(:user, name: 'Mary Jane') } - let(:group) { create(:group) } + let_it_be(:user1) { create(:user, name: 'John Doe') } + let_it_be(:group) { create(:group) } + let(:new_member) { create(:user, name: 'Mary Jane') } before do stub_feature_flags(vue_group_members_list: false) + travel_to Time.now.utc.beginning_of_day + group.add_owner(user1) sign_in(user1) end it 'expiration date is displayed in the members list' do - travel_to Time.zone.parse('2016-08-06 08:00') do - date = 4.days.from_now - visit group_group_members_path(group) + visit group_group_members_path(group) - page.within '.invite-users-form' do - select2(new_member.id, from: '#user_ids', multiple: true) - fill_in 'expires_at', with: date.to_s(:medium) + "\n" - click_on 'Invite' - end + page.within '.invite-users-form' do + select2(new_member.id, from: '#user_ids', multiple: true) - page.within "#group_member_#{group_member_id(new_member)}" do - expect(page).to have_content('Expires in 4 days') - end + fill_in 'expires_at', with: 3.days.from_now.to_date + find_field('expires_at').native.send_keys :enter + + click_on 'Invite' + end + + page.within "#group_member_#{group_member_id}" do + expect(page).to have_content('Expires in 3 days') end end - it 'change expiration date' do - travel_to Time.zone.parse('2016-08-06 08:00') do - date = 3.days.from_now - group.add_developer(new_member) + it 'changes expiration date' do + group.add_developer(new_member) + visit group_group_members_path(group) - visit group_group_members_path(group) + page.within "#group_member_#{group_member_id}" do + fill_in 'Expiration date', with: 3.days.from_now.to_date + find_field('Expiration date').native.send_keys :enter - page.within "#group_member_#{group_member_id(new_member)}" do - find('.js-access-expiration-date').set date.to_s(:medium) + "\n" - wait_for_requests - expect(page).to have_content('Expires in 3 days') - end + wait_for_requests + + expect(page).to have_content('Expires in 3 days') end end - it 'remove expiration date' do - travel_to Time.zone.parse('2016-08-06 08:00') do - date = 3.days.from_now - group_member = create(:group_member, :developer, user: new_member, group: group, expires_at: date.to_s(:medium)) + it 'clears expiration date' do + create(:group_member, :developer, user: new_member, group: group, expires_at: 3.days.from_now.to_date) + visit group_group_members_path(group) - visit group_group_members_path(group) + page.within "#group_member_#{group_member_id}" do + expect(page).to have_content('Expires in 3 days') - page.within "#group_member_#{group_member.id}" do - find('.js-clear-input').click - wait_for_requests - expect(page).not_to have_content('Expires in 3 days') - end + find('.js-clear-input').click + + wait_for_requests + + expect(page).not_to have_content('Expires in') end end - def group_member_id(user) + def group_member_id group.members.find_by(user_id: new_member).id end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index ff78b9e608f..321e214df1c 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -834,7 +834,7 @@ RSpec.describe 'GFM autocomplete', :js do end def start_and_cancel_discussion - click_button('Reply...') + click_button('Reply') fill_in('note_note', with: 'Whoops!') diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index c8fc23bebf9..f1f04c47bd8 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -223,7 +223,7 @@ end def write_reply_to_discussion(button_text: 'Start a review', text: 'Line is wrong', resolve: false, unresolve: false) page.within(first('.diff-files-holder .discussion-reply-holder')) do - click_button('Reply...') + click_button('Reply') fill_in('note_note', with: text) diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index 9556142ecb8..d305359e022 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -186,7 +186,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do it 'adds as discussion' do should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false) expect(page).to have_css('.notes_holder .note.note-discussion', count: 1) - expect(page).to have_button('Reply...') + expect(page).to have_button('Reply') end end end diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index cd06886169d..d546c602d96 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -146,7 +146,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do it 'allows user to comment' do page.within '.diff-content' do - click_button 'Reply...' + click_button 'Reply' find(".js-unresolve-checkbox").set false find('.js-note-text').set 'testing' @@ -176,7 +176,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do it 'allows user to comment & unresolve thread' do page.within '.diff-content' do - click_button 'Reply...' + click_button 'Reply' find('.js-note-text').set 'testing' @@ -205,7 +205,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do it 'allows user to comment & resolve thread' do page.within '.diff-content' do - click_button 'Reply...' + click_button 'Reply' find('.js-note-text').set 'testing' @@ -438,7 +438,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do it 'allows user to comment & resolve thread' do page.within '.diff-content' do - click_button 'Reply...' + click_button 'Reply' find('.js-note-text').set 'testing' @@ -457,7 +457,7 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do page.within '.diff-content' do click_button 'Resolve thread' - click_button 'Reply...' + click_button 'Reply' find('.js-note-text').set 'testing' diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index d15d5b3bc73..8d492708f2c 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -37,7 +37,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do end it 'does not render avatars after commenting on discussion tab' do - click_button 'Reply...' + click_button 'Reply' page.within('.js-discussion-note-form') do find('.note-textarea').native.send_keys('Test comment') @@ -132,7 +132,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do end it 'adds avatar when commenting' do - click_button 'Reply...' + click_button 'Reply' page.within '.js-discussion-note-form' do find('.js-note-text').native.send_keys('Test') @@ -151,7 +151,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do it 'adds multiple comments' do 3.times do - click_button 'Reply...' + click_button 'Reply' page.within '.js-discussion-note-form' do find('.js-note-text').native.send_keys('Test') diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb index 289c861739f..86e4b58b347 100644 --- a/spec/features/merge_request/user_sees_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_spec.rb @@ -60,7 +60,7 @@ RSpec.describe 'Merge request > User sees threads', :js do it 'can be replied to' do within(".discussion[data-discussion-id='#{discussion_id}']") do - click_button 'Reply...' + click_button 'Reply' fill_in 'note[note]', with: 'Test!' click_button 'Comment' diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb index 20c45a1d652..44da911441a 100644 --- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb +++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User sees notes from forked project', :js do expect(page).to have_content('A commit comment') page.within('.discussion-notes') do - find('.btn-text-field').click + find('.js-vue-discussion-reply').click scroll_to(page.find('#note_note', visible: false)) find('#note_note').send_keys('A reply comment') find('.js-comment-button').click diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 979bbd57aa3..d69c3f2652c 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -6,43 +6,64 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date include Select2Helper include ActiveSupport::Testing::TimeHelpers - let(:maintainer) { create(:user) } - let(:project) { create(:project) } - let!(:new_member) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:project) { create(:project) } + let(:new_member) { create(:user) } before do + travel_to Time.now.utc.beginning_of_day + project.add_maintainer(maintainer) sign_in(maintainer) end it 'expiration date is displayed in the members list' do - travel_to Time.zone.parse('2016-08-06 08:00') do - date = 4.days.from_now - visit project_project_members_path(project) + visit project_project_members_path(project) - page.within '.invite-users-form' do - select2(new_member.id, from: '#user_ids', multiple: true) - fill_in 'expires_at', with: date.to_s(:medium) + "\n" - click_on 'Invite' - end + page.within '.invite-users-form' do + select2(new_member.id, from: '#user_ids', multiple: true) - page.within "#project_member_#{new_member.project_members.first.id}" do - expect(page).to have_content('Expires in 4 days') - end + fill_in 'expires_at', with: 3.days.from_now.to_date + find_field('expires_at').native.send_keys :enter + + click_on 'Invite' + end + + page.within "#project_member_#{project_member_id}" do + expect(page).to have_content('Expires in 3 days') end end - it 'change expiration date' do - travel_to Time.zone.parse('2016-08-06 08:00') do - date = 3.days.from_now - project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_s(:medium)) - visit project_project_members_path(project) + it 'changes expiration date' do + project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_date) + visit project_project_members_path(project) - page.within "#project_member_#{new_member.project_members.first.id}" do - find('.js-access-expiration-date').set date.to_s(:medium) + "\n" - wait_for_requests - expect(page).to have_content('Expires in 3 days') - end + page.within "#project_member_#{project_member_id}" do + fill_in 'Expiration date', with: 3.days.from_now.to_date + find_field('Expiration date').native.send_keys :enter + + wait_for_requests + + expect(page).to have_content('Expires in 3 days') end end + + it 'clears expiration date' do + project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date) + visit project_project_members_path(project) + + page.within "#project_member_#{project_member_id}" do + expect(page).to have_content('Expires in 3 days') + + find('.js-clear-input').click + + wait_for_requests + + expect(page).not_to have_content('Expires in') + end + end + + def project_member_id + project.members.find_by(user_id: new_member).id + end end diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js index 1086985eec0..625024ee61f 100644 --- a/spec/frontend/commit/commit_pipeline_status_component_spec.js +++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js @@ -142,7 +142,7 @@ describe('Commit pipeline status component', () => { }); it('renders CI icon', () => { - expect(findCiIcon().attributes('data-original-title')).toEqual('Pipeline: pending'); + expect(findCiIcon().attributes('title')).toEqual('Pipeline: pending'); expect(findCiIcon().props('status')).toEqual(mockCiStatus); }); }); @@ -161,7 +161,7 @@ describe('Commit pipeline status component', () => { }); it('renders not found CI icon', () => { - expect(findCiIcon().attributes('data-original-title')).toEqual('Pipeline: not found'); + expect(findCiIcon().attributes('title')).toEqual('Pipeline: not found'); expect(findCiIcon().props('status')).toEqual({ text: 'not found', icon: 'status_notfound', diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js index 14c710dd7ba..f55cb851dde 100644 --- a/spec/frontend/environments/folder/environments_folder_view_spec.js +++ b/spec/frontend/environments/folder/environments_folder_view_spec.js @@ -1,10 +1,10 @@ +import { GlPagination } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { removeBreakLine, removeWhitespace } from 'helpers/text_helper'; -import { GlPagination } from '@gitlab/ui'; -import axios from '~/lib/utils/axios_utils'; -import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import EnvironmentTable from '~/environments/components/environments_table.vue'; +import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; +import axios from '~/lib/utils/axios_utils'; import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { @@ -89,9 +89,9 @@ describe('Environments Folder View', () => { }); it('should render parent folder name', () => { - expect(removeBreakLine(removeWhitespace(wrapper.find('.js-folder-name').text()))).toContain( - 'Environments / review', - ); + expect( + removeBreakLine(removeWhitespace(wrapper.find('[data-testid="folder-name"]').text())), + ).toContain('Environments / review'); }); describe('pagination', () => { diff --git a/spec/frontend/groups/members/index_spec.js b/spec/frontend/groups/members/index_spec.js index 95a111ef5da..bf4288f7eea 100644 --- a/spec/frontend/groups/members/index_spec.js +++ b/spec/frontend/groups/members/index_spec.js @@ -17,6 +17,7 @@ describe('initGroupMembersApp', () => { el = document.createElement('div'); el.setAttribute('data-members', membersJsonString); el.setAttribute('data-group-id', '234'); + el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id'); window.gon = { current_user_id: 123 }; @@ -69,4 +70,10 @@ describe('initGroupMembersApp', () => { expect(vm.$store.state.tableFields).toEqual(['account']); }); + + it('sets `memberPath` in Vuex store', () => { + setup(); + + expect(vm.$store.state.memberPath).toBe('/groups/foo-bar/-/group_members/:id'); + }); }); diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js index 7b83f0aefca..1a88e80344e 100644 --- a/spec/frontend/helpers/startup_css_helper_spec.js +++ b/spec/frontend/helpers/startup_css_helper_spec.js @@ -1,4 +1,4 @@ -import { waitForCSSLoaded } from '../../../app/assets/javascripts/helpers/startup_css_helper'; +import { waitForCSSLoaded } from '~/helpers/startup_css_helper'; describe('waitForCSSLoaded', () => { let mockedCallback; diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js index b7b7ec08867..c9c33cf3af1 100644 --- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js +++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; const buttonText = 'Test Button Text'; @@ -6,7 +7,7 @@ const buttonText = 'Test Button Text'; describe('ReplyPlaceholder', () => { let wrapper; - const findButton = () => wrapper.find({ ref: 'button' }); + const findButton = () => wrapper.find(GlButton); beforeEach(() => { wrapper = shallowMount(ReplyPlaceholder, { @@ -20,8 +21,8 @@ describe('ReplyPlaceholder', () => { wrapper.destroy(); }); - it('emits onClick event on button click', () => { - findButton().trigger('click'); + it('should emit a onClick event on button click', () => { + findButton().vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.emitted()).toEqual({ diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap index e742a6b9eaf..dd6dc51dfa1 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap @@ -9,6 +9,7 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = ` candelete="true" data-qa-selector="file_name_field" id="blob_local_7_file_path" + showdelete="true" value="foo/bar/test.md" /> diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js index 8b2051008d7..055168a1711 100644 --- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js @@ -19,17 +19,12 @@ const TEST_BLOBS_UNLOADED = TEST_BLOBS.map(blob => ({ ...blob, content: '', isLo describe('snippets/components/snippet_blob_actions_edit', () => { let wrapper; - const createComponent = (props = {}, snippetMultipleFiles = true) => { + const createComponent = (props = {}) => { wrapper = shallowMount(SnippetBlobActionsEdit, { propsData: { initBlobs: TEST_BLOBS, ...props, }, - provide: { - glFeatures: { - snippetMultipleFiles, - }, - }, }); }; @@ -69,28 +64,24 @@ describe('snippets/components/snippet_blob_actions_edit', () => { wrapper = null; }); - describe.each` - featureFlag | label | showDelete | showAdd - ${true} | ${'Files'} | ${true} | ${true} - ${false} | ${'File'} | ${false} | ${false} - `('with feature flag = $featureFlag', ({ featureFlag, label, showDelete, showAdd }) => { + describe('multi-file snippets rendering', () => { beforeEach(() => { - createComponent({}, featureFlag); + createComponent(); }); it('renders label', () => { - expect(findLabel().text()).toBe(label); + expect(findLabel().text()).toBe('Files'); }); - it(`renders delete button (show=${showDelete})`, () => { + it(`renders delete button (show=true)`, () => { expect(findFirstBlobEdit().props()).toMatchObject({ - showDelete, + showDelete: true, canDelete: true, }); }); - it(`renders add button (show=${showAdd})`, () => { - expect(findAddButton().exists()).toBe(showAdd); + it(`renders add button (show=true)`, () => { + expect(findAddButton().exists()).toBe(true); }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js index fc4da46d722..fe9e038f622 100644 --- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -156,7 +156,7 @@ describe('Snippet Blob Edit component', () => { }); it('shows blob header', () => { - const { canDelete = true, showDelete = false } = props; + const { canDelete = true, showDelete = true } = props; expect(findHeader().props()).toMatchObject({ canDelete, diff --git a/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js new file mode 100644 index 00000000000..7c56ef57e47 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/action_buttons/access_request_action_buttons_spec.js @@ -0,0 +1,82 @@ +import { shallowMount } from '@vue/test-utils'; +import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue'; +import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue'; +import { accessRequest as member } from '../mock_data'; + +describe('AccessRequestActionButtons', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(AccessRequestActionButtons, { + propsData: { + member, + isCurrentUser: true, + ...propsData, + }, + }); + }; + + const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when user has `canRemove` permissions', () => { + beforeEach(() => { + createComponent({ + permissions: { + canRemove: true, + }, + }); + }); + + it('renders remove member button', () => { + expect(findRemoveMemberButton().exists()).toBe(true); + }); + + it('sets props correctly', () => { + expect(findRemoveMemberButton().props()).toMatchObject({ + memberId: member.id, + title: 'Deny access', + isAccessRequest: true, + icon: 'close', + }); + }); + + describe('when member is the current user', () => { + it('sets `message` prop correctly', () => { + expect(findRemoveMemberButton().props('message')).toBe( + `Are you sure you want to withdraw your access request for "${member.source.name}"`, + ); + }); + }); + + describe('when member is not the current user', () => { + it('sets `message` prop correctly', () => { + createComponent({ + isCurrentUser: false, + permissions: { + canRemove: true, + }, + }); + + expect(findRemoveMemberButton().props('message')).toBe( + `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.name}"`, + ); + }); + }); + }); + + describe('when user does not have `canRemove` permissions', () => { + it('does not render remove member button', () => { + createComponent({ + permissions: { + canRemove: false, + }, + }); + + expect(findRemoveMemberButton().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js new file mode 100644 index 00000000000..98e04c6babd --- /dev/null +++ b/spec/frontend/vue_shared/components/members/action_buttons/invite_action_buttons_spec.js @@ -0,0 +1,59 @@ +import { shallowMount } from '@vue/test-utils'; +import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue'; +import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue'; +import { invite as member } from '../mock_data'; + +describe('InviteActionButtons', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(InviteActionButtons, { + propsData: { + member, + ...propsData, + }, + }); + }; + + const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when user has `canRemove` permissions', () => { + beforeEach(() => { + createComponent({ + permissions: { + canRemove: true, + }, + }); + }); + + it('renders remove member button', () => { + expect(findRemoveMemberButton().exists()).toBe(true); + }); + + it('sets props correctly', () => { + expect(findRemoveMemberButton().props()).toEqual({ + memberId: member.id, + message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.name}"`, + title: 'Revoke invite', + isAccessRequest: false, + icon: 'remove', + }); + }); + }); + + describe('when user does not have `canRemove` permissions', () => { + it('does not render remove member button', () => { + createComponent({ + permissions: { + canRemove: false, + }, + }); + + expect(findRemoveMemberButton().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js new file mode 100644 index 00000000000..7aa30494234 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/action_buttons/remove_member_button_spec.js @@ -0,0 +1,66 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('RemoveMemberButton', () => { + let wrapper; + + const createStore = (state = {}) => { + return new Vuex.Store({ + state: { + memberPath: '/groups/foo-bar/-/group_members/:id', + ...state, + }, + }); + }; + + const createComponent = (propsData = {}, state) => { + wrapper = shallowMount(RemoveMemberButton, { + localVue, + store: createStore(state), + propsData: { + memberId: 1, + message: 'Are you sure you want to remove John Smith?', + title: 'Remove member', + isAccessRequest: true, + ...propsData, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('sets attributes on button', () => { + createComponent(); + + expect(wrapper.attributes()).toMatchObject({ + 'data-member-path': '/groups/foo-bar/-/group_members/1', + 'data-message': 'Are you sure you want to remove John Smith?', + 'data-is-access-request': 'true', + 'aria-label': 'Remove member', + title: 'Remove member', + icon: 'remove', + }); + }); + + it('displays `title` prop as a tooltip', () => { + createComponent(); + + expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined(); + }); + + it('has CSS class used by `remove_member_modal.vue`', () => { + createComponent(); + + expect(wrapper.classes()).toContain('js-remove-member-button'); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js new file mode 100644 index 00000000000..dc0a8536943 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/action_buttons/user_action_buttons_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue'; +import RemoveMemberButton from '~/vue_shared/components/members/action_buttons/remove_member_button.vue'; +import { member, orphanedMember } from '../mock_data'; + +describe('UserActionButtons', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(UserActionButtons, { + propsData: { + member, + isCurrentUser: false, + ...propsData, + }, + }); + }; + + const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when user has `canRemove` permissions', () => { + beforeEach(() => { + createComponent({ + permissions: { + canRemove: true, + }, + }); + }); + + it('renders remove member button', () => { + expect(findRemoveMemberButton().exists()).toBe(true); + }); + + it('sets props correctly', () => { + expect(findRemoveMemberButton().props()).toEqual({ + memberId: member.id, + message: `Are you sure you want to remove ${member.user.name} from "${member.source.name}"`, + title: 'Remove member', + isAccessRequest: false, + icon: 'remove', + }); + }); + + describe('when member is orphaned', () => { + it('sets `message` prop correctly', () => { + createComponent({ + member: orphanedMember, + permissions: { + canRemove: true, + }, + }); + + expect(findRemoveMemberButton().props('message')).toBe( + `Are you sure you want to remove this orphaned member from "${orphanedMember.source.name}"`, + ); + }); + }); + }); + + describe('when user does not have `canRemove` permissions', () => { + it('does not render remove member button', () => { + createComponent({ + permissions: { + canRemove: false, + }, + }); + + expect(findRemoveMemberButton().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js b/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js new file mode 100644 index 00000000000..e55d9b6be2a --- /dev/null +++ b/spec/frontend/vue_shared/components/members/table/member_action_buttons_spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import { MEMBER_TYPES } from '~/vue_shared/components/members/constants'; +import { member as memberMock, group, invite, accessRequest } from '../mock_data'; +import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue'; +import UserActionButtons from '~/vue_shared/components/members/action_buttons/user_action_buttons.vue'; +import GroupActionButtons from '~/vue_shared/components/members/action_buttons/group_action_buttons.vue'; +import InviteActionButtons from '~/vue_shared/components/members/action_buttons/invite_action_buttons.vue'; +import AccessRequestActionButtons from '~/vue_shared/components/members/action_buttons/access_request_action_buttons.vue'; + +describe('MemberActionButtons', () => { + let wrapper; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(MemberActionButtons, { + propsData: { + isCurrentUser: false, + permissions: { + canRemove: true, + }, + ...propsData, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + test.each` + memberType | member | expectedComponent | expectedComponentName + ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionButtons} | ${'UserActionButtons'} + ${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'} + ${MEMBER_TYPES.invite} | ${invite} | ${InviteActionButtons} | ${'InviteActionButtons'} + ${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${AccessRequestActionButtons} | ${'AccessRequestActionButtons'} + `( + 'renders $expectedComponentName when `memberType` is $memberType', + ({ memberType, member, expectedComponent }) => { + createComponent({ memberType, member }); + + expect(wrapper.find(expectedComponent).exists()).toBe(true); + }, + ); +}); diff --git a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js index 960d9bc164c..e9b5223df73 100644 --- a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js +++ b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js @@ -19,6 +19,10 @@ describe('MemberList', () => { type: Boolean, required: true, }, + permissions: { + type: Object, + required: true, + }, }, render(createElement) { return createElement('div', this.memberType); @@ -52,6 +56,7 @@ describe('MemberList', () => { :member-type="props.memberType" :is-direct-member="props.isDirectMember" :is-current-user="props.isCurrentUser" + :permissions="props.permissions" /> `, }, @@ -60,6 +65,24 @@ describe('MemberList', () => { const findWrappedComponent = () => wrapper.find(WrappedComponent); + const createComponentWithDirectMember = (member = {}) => { + createComponent({ + member: { + ...memberMock, + source: { + ...memberMock.source, + id: 1, + }, + ...member, + }, + }); + }; + const createComponentWithInheritedMember = (member = {}) => { + createComponent({ + member: { ...memberMock, ...member }, + }); + }; + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -82,23 +105,13 @@ describe('MemberList', () => { describe('isDirectMember', () => { it('returns `true` when member source has same ID as `sourceId`', () => { - createComponent({ - member: { - ...memberMock, - source: { - ...memberMock.source, - id: 1, - }, - }, - }); + createComponentWithDirectMember(); expect(findWrappedComponent().props('isDirectMember')).toBe(true); }); it('returns `false` when member is inherited', () => { - createComponent({ - member: memberMock, - }); + createComponentWithInheritedMember(); expect(findWrappedComponent().props('isDirectMember')).toBe(false); }); @@ -127,4 +140,34 @@ describe('MemberList', () => { expect(findWrappedComponent().props('isCurrentUser')).toBe(false); }); }); + + describe('permissions', () => { + describe('canRemove', () => { + describe('for a direct member', () => { + it('returns `true` when `canRemove` is `true`', () => { + createComponentWithDirectMember({ + canRemove: true, + }); + + expect(findWrappedComponent().props('permissions').canRemove).toBe(true); + }); + + it('returns `false` when `canRemove` is `false`', () => { + createComponentWithDirectMember({ + canRemove: false, + }); + + expect(findWrappedComponent().props('permissions').canRemove).toBe(false); + }); + }); + + describe('for an inherited member', () => { + it('returns `false`', () => { + createComponentWithInheritedMember(); + + expect(findWrappedComponent().props('permissions').canRemove).toBe(false); + }); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js index 4979a7096ac..567b0b18c6f 100644 --- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js +++ b/spec/frontend/vue_shared/components/members/table/members_table_spec.js @@ -9,6 +9,7 @@ import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vu import MemberSource from '~/vue_shared/components/members/table/member_source.vue'; import ExpiresAt from '~/vue_shared/components/members/table/expires_at.vue'; import CreatedAt from '~/vue_shared/components/members/table/created_at.vue'; +import MemberActionButtons from '~/vue_shared/components/members/table/member_action_buttons.vue'; import * as initUserPopovers from '~/user_popovers'; import { member as memberMock, invite, accessRequest } from '../mock_data'; @@ -32,7 +33,13 @@ describe('MemberList', () => { wrapper = mount(MembersTable, { localVue, store: createStore(state), - stubs: ['member-avatar', 'member-source', 'expires-at', 'created-at'], + stubs: [ + 'member-avatar', + 'member-source', + 'expires-at', + 'created-at', + 'member-action-buttons', + ], }); }; @@ -77,12 +84,18 @@ describe('MemberList', () => { }); it('renders "Actions" field for screen readers', () => { - createComponent({ tableFields: ['actions'] }); + createComponent({ members: [memberMock], tableFields: ['actions'] }); const actionField = getByTestId('col-actions'); expect(actionField.exists()).toBe(true); expect(actionField.classes('gl-sr-only')).toBe(true); + expect( + wrapper + .find(`[data-label="Actions"][role="cell"]`) + .find(MemberActionButtons) + .exists(), + ).toBe(true); }); }); diff --git a/spec/lib/api/entities/snippet_spec.rb b/spec/lib/api/entities/snippet_spec.rb index 068851f7f6c..090f09c9b61 100644 --- a/spec/lib/api/entities/snippet_spec.rb +++ b/spec/lib/api/entities/snippet_spec.rb @@ -21,16 +21,6 @@ RSpec.describe ::API::Entities::Snippet do it { expect(subject[:visibility]).to eq snippet.visibility } it { expect(subject).to include(:author) } - context 'with snippet_multiple_files feature disabled' do - before do - stub_feature_flags(snippet_multiple_files: false) - end - - it 'does not return files' do - expect(subject).not_to include(:files) - end - end - describe 'file_name' do it 'returns attribute from repository' do expect(subject[:file_name]).to eq snippet.blobs.first.path @@ -77,14 +67,6 @@ RSpec.describe ::API::Entities::Snippet do let(:blob) { snippet.blobs.first } let(:ref) { blob.repository.root_ref } - context 'when repository does not exist' do - it 'does not include the files attribute' do - allow(snippet).to receive(:repository_exists?).and_return(false) - - expect(subject).not_to include(:files) - end - end - shared_examples 'snippet files' do let(:file) { subject[:files].first } @@ -99,6 +81,14 @@ RSpec.describe ::API::Entities::Snippet do it 'has the raw url' do expect(file[:raw_url]).to match(raw_url) end + + context 'when repository does not exist' do + it 'returns empty array' do + allow(snippet.repository).to receive(:empty?).and_return(true) + + expect(subject[:files]).to be_empty + end + end end context 'with PersonalSnippet' do diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb index 362ea3c006e..8c481cdee08 100644 --- a/spec/lib/gitlab/git_access_snippet_spec.rb +++ b/spec/lib/gitlab/git_access_snippet_spec.rb @@ -260,7 +260,7 @@ RSpec.describe Gitlab::GitAccessSnippet do service = double expect(service).to receive(:validate!).and_return(nil) - expect(Snippet).to receive(:max_file_limit).with(user).and_return(5) + expect(Snippet).to receive(:max_file_limit).and_return(5) expect(Gitlab::Checks::PushFileCountCheck).to receive(:new).with(anything, hash_including(limit: 5)).and_return(service) push_access_check diff --git a/spec/lib/gitlab/gitpod_spec.rb b/spec/lib/gitlab/gitpod_spec.rb index f4dda42aeb4..717e396f942 100644 --- a/spec/lib/gitlab/gitpod_spec.rb +++ b/spec/lib/gitlab/gitpod_spec.rb @@ -4,30 +4,29 @@ require 'spec_helper' RSpec.describe Gitlab::Gitpod do let_it_be(:user) { create(:user) } - let(:feature_scope) { true } before do stub_feature_flags(gitpod: feature_scope) end - describe '.feature_conditional?' do - subject { described_class.feature_conditional? } - - context 'when feature is enabled globally' do - it { is_expected.to be_falsey } - end - - context 'when feature is enabled only to a resource' do - let(:feature_scope) { user } - - it { is_expected.to be_truthy } - end - end - describe '.feature_available?' do subject { described_class.feature_available? } + context 'when feature has not been set' do + let(:feature_scope) { nil } + + it { is_expected.to be_truthy } + end + + context 'when feature is disabled' do + let(:feature_scope) { false } + + it { is_expected.to be_falsey } + end + context 'when feature is enabled globally' do + let(:feature_scope) { true } + it { is_expected.to be_truthy } end @@ -43,7 +42,15 @@ RSpec.describe Gitlab::Gitpod do subject { described_class.feature_enabled?(current_user) } + context 'when feature has not been set' do + let(:feature_scope) { nil } + + it { is_expected.to be_truthy } + end + context 'when feature is enabled globally' do + let(:feature_scope) { true } + it { is_expected.to be_truthy } end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index ab614a6d45c..579a17b5a0b 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -717,19 +717,11 @@ RSpec.describe Snippet do end describe '.max_file_limit' do - subject { described_class.max_file_limit(nil) } + subject { described_class.max_file_limit } it "returns #{Snippet::MAX_FILE_COUNT}" do expect(subject).to eq Snippet::MAX_FILE_COUNT end - - context 'when feature flag :snippet_multiple_files is disabled' do - it "returns #{described_class::MAX_SINGLE_FILE_COUNT}" do - stub_feature_flags(snippet_multiple_files: false) - - expect(subject).to eq described_class::MAX_SINGLE_FILE_COUNT - end - end end describe '#list_files' do diff --git a/spec/presenters/snippet_blob_presenter_spec.rb b/spec/presenters/snippet_blob_presenter_spec.rb index d7268c79a2c..ec77f8cdb4a 100644 --- a/spec/presenters/snippet_blob_presenter_spec.rb +++ b/spec/presenters/snippet_blob_presenter_spec.rb @@ -133,28 +133,6 @@ RSpec.describe SnippetBlobPresenter do subject { described_class.new(snippet.blobs.first, current_user: user).raw_path } it_behaves_like 'snippet blob raw path' - - context 'with snippet_multiple_files feature disabled' do - before do - stub_feature_flags(snippet_multiple_files: false) - end - - context 'with ProjectSnippet' do - let(:snippet) { project_snippet } - - it 'returns the raw path' do - expect(subject).to eq "/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw" - end - end - - context 'with PersonalSnippet' do - let(:snippet) { personal_snippet } - - it 'returns the raw path' do - expect(subject).to eq "/-/snippets/#{snippet.id}/raw" - end - end - end end describe '#raw_url' do @@ -165,28 +143,6 @@ RSpec.describe SnippetBlobPresenter do end it_behaves_like 'snippet blob raw url' - - context 'with snippet_multiple_files feature disabled' do - before do - stub_feature_flags(snippet_multiple_files: false) - end - - context 'with ProjectSnippet' do - let(:snippet) { project_snippet } - - it 'returns the raw project snippet url' do - expect(subject).to eq("http://test.host/#{project_snippet.project.full_path}/-/snippets/#{project_snippet.id}/raw") - end - end - - context 'with PersonalSnippet' do - let(:snippet) { personal_snippet } - - it 'returns the raw personal snippet url' do - expect(subject).to eq("http://test.host/-/snippets/#{personal_snippet.id}/raw") - end - end - end end end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index c285f18f8b3..dc063f14bf5 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -116,10 +116,6 @@ RSpec.describe API::ProjectSnippets do let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/123", user) } end end - - it_behaves_like 'snippet_multiple_files feature disabled' do - subject { get api("/projects/#{project.id}/snippets/#{snippet.id}", user) } - end end describe 'POST /projects/:project_id/snippets/' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 9c4902774f5..ea39a3ccd70 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -167,8 +167,6 @@ RSpec.describe API::Snippets do it_behaves_like 'snippet access with different users' do let(:path) { "/snippets/#{snippet.id}" } end - - it_behaves_like 'snippet_multiple_files feature disabled' end describe 'POST /snippets/' do @@ -251,10 +249,6 @@ RSpec.describe API::Snippets do it_behaves_like 'snippet creation' - it_behaves_like 'snippet_multiple_files feature disabled' do - let(:snippet) { Snippet.find(json_response["id"]) } - end - context 'with an external user' do let(:user) { create(:user, :external) } diff --git a/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb index 7a283a974d2..03c402fb066 100644 --- a/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -75,11 +75,9 @@ RSpec.describe Clusters::Kubernetes::FetchKubernetesTokenService do before do stub_kubeclient_get_secret_missing_token_then_with_token( api_url, - { - metadata_name: service_account_token_name, - namespace: namespace, - token: token - } + metadata_name: service_account_token_name, + namespace: namespace, + token: token ) end diff --git a/spec/services/snippets/repository_validation_service_spec.rb b/spec/services/snippets/repository_validation_service_spec.rb index e2a0d0faa18..48aeafa2f4b 100644 --- a/spec/services/snippets/repository_validation_service_spec.rb +++ b/spec/services/snippets/repository_validation_service_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Snippets::RepositoryValidationService do end it 'returns error when the repository has more file than the limit' do - limit = Snippet.max_file_limit(user) + 1 + limit = Snippet.max_file_limit + 1 files = Array.new(limit) { FFaker::Filesystem.file_name } allow(repository).to receive(:ls_files).and_return(files) diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index f4343b8b783..6d3ac699a7c 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -126,17 +126,15 @@ module CycleAnalyticsHelpers end def mock_gitaly_multi_action_dates(repository, commit_time) - allow(repository.raw).to receive(:multi_action).and_wrap_original do |m, *args| + allow(repository.raw).to receive(:multi_action).and_wrap_original do |m, user, kargs| new_date = commit_time || Time.now - branch_update = m.call(*args) + branch_update = m.call(user, **kargs) if branch_update.newrev - _, opts = args - commit = rugged_repo(repository).rev_parse(branch_update.newrev) branch_update.newrev = commit.amend( - update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}", + update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{kargs[:branch_name]}", author: commit.author.merge(time: new_date), committer: commit.committer.merge(time: new_date) ) diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 5f9b6119635..35eba81cfd6 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -23,7 +23,7 @@ module GraphqlHelpers return early_return unless ready - resolver.resolve(args) + resolver.resolve(**args) end # Eagerly run a loader's named resolver diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 16d2e84cc0f..90ddab89943 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -167,7 +167,7 @@ module KubernetesHelpers options[:namespace] ||= "default" WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{options[:namespace]}/secrets/#{options[:metadata_name]}") - .to_return(kube_response(kube_v1_secret_body(**options))) + .to_return(kube_response(kube_v1_secret_body(options))) end def stub_kubeclient_get_secret_error(api_url, name, namespace: 'default', status: 404) @@ -265,7 +265,7 @@ module KubernetesHelpers .to_return(kube_response({})) end - def kube_v1_secret_body(**options) + def kube_v1_secret_body(options) { "kind" => "SecretList", "apiVersion": "v1", diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index 84ef7723b9b..dae3a3e74be 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -99,18 +99,6 @@ RSpec.shared_examples 'snippet blob content' do end end -RSpec.shared_examples 'snippet_multiple_files feature disabled' do - before do - stub_feature_flags(snippet_multiple_files: false) - - subject - end - - it 'does not return files attributes' do - expect(json_response).not_to have_key('files') - end -end - RSpec.shared_examples 'snippet creation with files parameter' do using RSpec::Parameterized::TableSyntax