diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index d34687cfdad..fab33dd3c07 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -174,7 +174,7 @@ review-performance: - .default-retry - .review:rules:review-performance image: - name: sitespeedio/sitespeed.io:6.3.1 + name: sitespeedio/sitespeed.io entrypoint: [""] stage: qa # This is needed so that manual jobs with needs don't block the pipeline. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index c970633b250..4f2b3edfe93 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -b66774f8cfbdde61d45589d7ac4cc030a086cfc6 +8d14377ab5b3914033f85ec3572e0ba65749a6e0 diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue new file mode 100644 index 00000000000..eb0b67a1629 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue @@ -0,0 +1,14 @@ + + + diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue new file mode 100644 index 00000000000..1147ce9af73 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue @@ -0,0 +1,64 @@ + + + diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql new file mode 100644 index 00000000000..fd8282683d9 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/instance_statistics_count.query.graphql @@ -0,0 +1,32 @@ +query getInstanceCounts { + projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: 1) { + nodes { + count + } + } + groups: instanceStatisticsMeasurements(identifier: GROUPS, first: 1) { + nodes { + count + } + } + users: instanceStatisticsMeasurements(identifier: USERS, first: 1) { + nodes { + count + } + } + issues: instanceStatisticsMeasurements(identifier: ISSUES, first: 1) { + nodes { + count + } + } + mergeRequests: instanceStatisticsMeasurements(identifier: MERGE_REQUESTS, first: 1) { + nodes { + count + } + } + pipelines: instanceStatisticsMeasurements(identifier: PIPELINES, first: 1) { + nodes { + count + } + } +} diff --git a/app/assets/javascripts/analytics/instance_statistics/index.js b/app/assets/javascripts/analytics/instance_statistics/index.js new file mode 100644 index 00000000000..0d7dcf6ace8 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import InstanceStatisticsApp from './components/app.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default () => { + const el = document.getElementById('js-instance-statistics-app'); + + if (!el) return false; + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(InstanceStatisticsApp); + }, + }); +}; diff --git a/app/assets/javascripts/analytics/shared/components/metric_card.vue b/app/assets/javascripts/analytics/shared/components/metric_card.vue new file mode 100644 index 00000000000..cee186c057c --- /dev/null +++ b/app/assets/javascripts/analytics/shared/components/metric_card.vue @@ -0,0 +1,80 @@ + + diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue index 5c7e84bd87c..b1a2b2a72ea 100644 --- a/app/assets/javascripts/diffs/components/commit_widget.vue +++ b/app/assets/javascripts/diffs/components/commit_widget.vue @@ -1,19 +1,6 @@ diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 20d9fb82554..52e9b67c77d 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -7,11 +7,6 @@ import { __ } from './locale'; export default class Milestone { constructor() { this.bindTabsSwitching(); - - // Load merge request tab if it is active - // merge request tab is active based on different conditions in the backend - this.loadTab($('.js-milestone-tabs .active a')); - this.loadInitialTab(); } @@ -23,12 +18,14 @@ export default class Milestone { this.loadTab($target); }); } - // eslint-disable-next-line class-methods-use-this + loadInitialTab() { - const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`); + const $target = $(`.js-milestone-tabs a:not(.active)[href="${window.location.hash}"]`); if ($target.length) { $target.tab('show'); + } else { + this.loadTab($('.js-milestone-tabs a.active')); } } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/pages/admin/instance_statistics/index.js b/app/assets/javascripts/pages/admin/instance_statistics/index.js new file mode 100644 index 00000000000..d6b0a834ce3 --- /dev/null +++ b/app/assets/javascripts/pages/admin/instance_statistics/index.js @@ -0,0 +1,3 @@ +import initInstanceStatisticsApp from '~/analytics/instance_statistics'; + +document.addEventListener('DOMContentLoaded', () => initInstanceStatisticsApp()); diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue index 7be68e77def..228a660c997 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue @@ -1,5 +1,4 @@ diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue index cc33b8f85cd..197671b47d6 100644 --- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue +++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue @@ -1,10 +1,12 @@ diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js index a740a3fa6b9..cdbde55901d 100644 --- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js @@ -10,6 +10,10 @@ import { validateParams } from '~/pipelines/utils'; export default { methods: { onChangeTab(scope) { + if (this.scope === scope) { + return; + } + let params = { scope, page: '1', diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index 30d56d99e1c..4cab61a8e39 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -88,11 +88,6 @@ content: '\f078'; } -.fa-remove::before, -.fa-times::before { - content: '\f00d'; -} - .fa-caret-down::before { content: '\f0d7'; } diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index 29138e7b014..6470c75dfbd 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -3,13 +3,25 @@ module MilestoneActions extend ActiveSupport::Concern + def issues + respond_to do |format| + format.html { redirect_to milestone_redirect_path } + format.json do + render json: tabs_json("shared/milestones/_issues_tab", { + issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables + show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name]) + }) + end + end + end + def merge_requests respond_to do |format| format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { merge_requests: @milestone.sorted_merge_requests(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables - show_project_name: true + show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name]) }) end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index df3fb6b67c2..3f2894d378b 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -3,7 +3,7 @@ class Groups::MilestonesController < Groups::ApplicationController include MilestoneActions - before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy] + before_action :milestone, only: [:edit, :show, :update, :issues, :merge_requests, :participants, :labels, :destroy] before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy] before_action do push_frontend_feature_flag(:burnup_charts, @group) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 16d63cc184f..8049a17068b 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -5,7 +5,7 @@ class Projects::MilestonesController < Projects::ApplicationController include MilestoneActions before_action :check_issuables_available! - before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote] + before_action :milestone, only: [:edit, :update, :destroy, :show, :issues, :merge_requests, :participants, :labels, :promote] before_action do push_frontend_feature_flag(:burnup_charts, @project) end @@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController before_action :authorize_read_milestone! # Allow admin milestone - before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels] + before_action :authorize_admin_milestone!, except: [:index, :show, :issues, :merge_requests, :participants, :labels] # Allow to promote milestone before_action :authorize_promote_milestone!, only: :promote diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb index 34919f994ee..bbf8cf7dac3 100644 --- a/app/helpers/timeboxes_helper.rb +++ b/app/helpers/timeboxes_helper.rb @@ -228,8 +228,8 @@ module TimeboxesHelper end alias_method :milestone_date_range, :timebox_date_range - def milestone_tab_path(milestone, tab) - url_for(action: tab, format: :json) + def milestone_tab_path(milestone, tab, params = {}) + url_for(params.merge(action: tab, format: :json)) end def update_milestone_path(milestone, params = {}) diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml index a8ef19dcf46..ca2737ca56f 100644 --- a/app/views/admin/hook_logs/show.html.haml +++ b/app/views/admin/hook_logs/show.html.haml @@ -4,6 +4,6 @@ %hr -= link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn btn-default float-right gl-ml-3" += link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml deleted file mode 100644 index c022d2c70d8..00000000000 --- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml +++ /dev/null @@ -1,11 +0,0 @@ --#----------------------------------------------------------------- - WARNING: Please keep changes up-to-date with the following files: - - `assets/javascripts/diffs/components/commit_widget.vue` --#----------------------------------------------------------------- -- collapsible = local_assigns.fetch(:collapsible, true) - -- if @commit - .info-well.mw-100.mx-0 - .well-segment - %ul.blob-commit-info - = render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true, collapsible: collapsible diff --git a/app/views/projects/merge_requests/diffs/_different_base.html.haml b/app/views/projects/merge_requests/diffs/_different_base.html.haml deleted file mode 100644 index 06a15b96653..00000000000 --- a/app/views/projects/merge_requests/diffs/_different_base.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- if @merge_request_diff && different_base?(@start_version, @merge_request_diff) - .mr-version-controls - .content-block - = sprite_icon('information-o') - Selected versions have different base commits. - Changes will include - = link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do - new commits - from - = succeed '.' do - %code.ref-name= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml deleted file mode 100644 index 9ebd91dea0b..00000000000 --- a/app/views/projects/merge_requests/diffs/_diffs.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -= render 'projects/merge_requests/diffs/version_controls' -= render 'projects/merge_requests/diffs/different_base' -= render 'projects/merge_requests/diffs/not_all_comments_displayed' -= render 'projects/merge_requests/diffs/commit_widget' - -- if @merge_request_diff&.empty? - .row.empty-state.nothing-here-block - .col-12 - .svg-content= image_tag 'illustrations/merge_request_changes_empty.svg' - .col-12 - .text-content.text-center - %p - No changes between - %span.ref-name= @merge_request.source_branch - and - %span.ref-name= @merge_request.target_branch - .text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-success' -- else - - diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true - - if diff_viewable - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml deleted file mode 100644 index b9dc37c9b54..00000000000 --- a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- if @commit || @start_version || (@merge_request_diff && !@merge_request_diff.latest?) - .mr-version-controls - .content-block.comments-disabled-notif.clearfix - = sprite_icon('information-o') - = succeed '.' do - - if @commit - Only comments from the following commit are shown below - - else - Not all comments are displayed because you're - - if @start_version - comparing two versions of the diff - - else - viewing an old version of the diff - .float-right - = link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do - Show latest version - = "of the diff" if @commit diff --git a/app/views/projects/merge_requests/diffs/_version_controls.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml deleted file mode 100644 index 52bf584d550..00000000000 --- a/app/views/projects/merge_requests/diffs/_version_controls.html.haml +++ /dev/null @@ -1,73 +0,0 @@ -- if @merge_request_diff && @merge_request_diffs.size > 1 - .mr-version-controls - .mr-version-menus-container.content-block - Changes between - %span.dropdown.inline.mr-version-dropdown - %a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } } - %span - - if @merge_request_diff.latest? - latest version - - else - version #{version_index(@merge_request_diff)} - = icon('caret-down') - .dropdown-menu.dropdown-select.dropdown-menu-selectable - .dropdown-title - %span Version: - %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } - = icon('times', class: 'dropdown-menu-close-icon') - .dropdown-content - %ul - - @merge_request_diffs.each do |merge_request_diff| - %li - = link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do - %div - %strong - - if merge_request_diff.latest? - latest version - - else - version #{version_index(merge_request_diff)} - %div - %small.commit-sha= short_sha(merge_request_diff.head_commit_sha) - %div - %small - #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)}, - = time_ago_with_tooltip(merge_request_diff.created_at) - - - if @merge_request_diff.base_commit_sha - and - %span.dropdown.inline.mr-version-compare-dropdown - %a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } } - - if @start_version - version #{version_index(@start_version)} - - else - %span.ref-name= @merge_request.target_branch - = icon('caret-down') - .dropdown-menu.dropdown-select.dropdown-menu-selectable - .dropdown-title - %span Compared with: - %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } } - = icon('times', class: 'dropdown-menu-close-icon') - .dropdown-content - %ul - - @comparable_diffs.each do |merge_request_diff| - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do - %div - %strong - - if merge_request_diff.latest? - latest version - - else - version #{version_index(merge_request_diff)} - %div - %small.commit-sha= short_sha(merge_request_diff.head_commit_sha) - %div - %small - = time_ago_with_tooltip(merge_request_diff.created_at) - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_version) do - %div - %strong - %span.ref-name= @merge_request.target_branch - (base) - %div - %strong.commit-sha= short_sha(@merge_request_diff.base_commit_sha) diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index f8bf3e7ad6a..a62ed009552 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -10,8 +10,6 @@ %span - if show_project_name %strong #{project.name} · - - elsif show_full_project_name - %strong #{project.full_name} · - if issuable.is_a?(Issue) = confidential_icon(issuable) = link_to issuable.title, issuable_url_args, title: issuable.title diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index ee97f0172da..9147e1c50e3 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -15,4 +15,4 @@ = render partial: 'shared/milestones/issuable', collection: issuables, as: :issuable, - locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } + locals: { show_project_name: show_project_name } diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml index dc54eefbaa9..76ef636ec96 100644 --- a/app/views/shared/milestones/_issues_tab.html.haml +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -1,5 +1,4 @@ -- args = { show_project_name: local_assigns.fetch(:show_project_name, false), - show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } +- args = { show_project_name: local_assigns.fetch(:show_project_name, false) } - if display_issues_count_warning?(@milestone) .flash-container diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml index 0dbf2b27c8d..a78440600ad 100644 --- a/app/views/shared/milestones/_merge_requests_tab.haml +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -1,5 +1,4 @@ -- args = { show_project_name: local_assigns.fetch(:show_project_name, false), - show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } +- args = { show_project_name: local_assigns.fetch(:show_project_name, false) } .row.gl-mt-3 .col-md-3 diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 34f476241c6..33e634c3e7b 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,14 +1,16 @@ +- show_project_name = local_assigns.fetch(:show_project_name, false) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= sprite_icon('chevron-lg-left', size: 12) .fade-right= sprite_icon('chevron-lg-right', size: 12) %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs %li.nav-item - = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do + = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do = _('Issues') %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size - if milestone.merge_requests_enabled? %li.nav-item - = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do + = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests', show_project_name: show_project_name) } do = _('Merge Requests') %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size %li.nav-item @@ -20,20 +22,13 @@ = _('Labels') %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count -- issues = milestone.sorted_issues(current_user) -- show_project_name = local_assigns.fetch(:show_project_name, false) -- show_full_project_name = local_assigns.fetch(:show_full_project_name, false) - .tab-content.milestone-content - .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } } - = render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane.active#tab-issues + = render "shared/milestones/tab_loading" - if milestone.merge_requests_enabled? .tab-pane#tab-merge-requests - -# loaded async = render "shared/milestones/tab_loading" .tab-pane#tab-participants - -# loaded async = render "shared/milestones/tab_loading" .tab-pane#tab-labels - -# loaded async = render "shared/milestones/tab_loading" diff --git a/changelogs/unreleased/197227-load-milestone-issues-tab-async.yml b/changelogs/unreleased/197227-load-milestone-issues-tab-async.yml new file mode 100644 index 00000000000..b3f427ddbdd --- /dev/null +++ b/changelogs/unreleased/197227-load-milestone-issues-tab-async.yml @@ -0,0 +1,5 @@ +--- +title: Load issues tab in the milestone page asynchronously +merge_request: 42473 +author: +type: performance diff --git a/changelogs/unreleased/230719-tabs-vue-migrate-app-assets-javascripts-environments-components-en.yml b/changelogs/unreleased/230719-tabs-vue-migrate-app-assets-javascripts-environments-components-en.yml new file mode 100644 index 00000000000..8cdd77718f8 --- /dev/null +++ b/changelogs/unreleased/230719-tabs-vue-migrate-app-assets-javascripts-environments-components-en.yml @@ -0,0 +1,5 @@ +--- +title: Migrate custom Tabs to GlTabs +merge_request: 42236 +author: +type: changed diff --git a/config/routes/group.rb b/config/routes/group.rb index e5bbfdf7548..bf14afff318 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -61,6 +61,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do resources :milestones, constraints: { id: %r{[^/]+} } do member do + get :issues get :merge_requests get :participants get :labels diff --git a/config/routes/project.rb b/config/routes/project.rb index 24b44646d95..e0208e36a34 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -161,8 +161,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :milestones, constraints: { id: /\d+/ } do member do post :promote - put :sort_issues - put :sort_merge_requests + get :issues get :merge_requests get :participants get :labels diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index bf816afdfab..eec1d03d1a5 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -298,6 +298,7 @@ OmniAuth onboarding OpenID OpenShift +Opsgenie Packagist parallelization parallelizations @@ -382,6 +383,7 @@ reusability reverified reverifies reverify +RHEL rollout rollouts rsync diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 0fcd95c41ed..8468d696838 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -547,6 +547,7 @@ runtime. at least version **1.8** if you want to use private registries. - Available for [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html) in GitLab Runner 13.1 and later. +- [Credentials Store](#using-credentials-store) and [Credential Helpers](#using-credential-helpers) require binaries to be added to the GitLab Runner's `$PATH`, and require access to do so. Therefore, these features are not available on shared runners or any other runner where the user does not have access to the environment where the runner is installed. ### Using statically-defined credentials diff --git a/doc/development/README.md b/doc/development/README.md index abdd5c662f3..ee4643ed5a5 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -43,6 +43,7 @@ For information on how to install, configure, update, and upgrade your own GitLa **Must-reads:** +- [Guide on adapting existing and introducing new components](architecture.md#adapting-existing-and-introducing-new-components) - [Code review guidelines](code_review.md) for reviewing code and having code reviewed - [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed - [Secure coding guidelines](secure_coding_guidelines.md) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index f6b1c8cd914..b94ee045a60 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -1,32 +1,91 @@ -# GitLab Architecture Overview +# GitLab architecture overview ## Software delivery -There are two software distributions of GitLab: the open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/) (CE), and the open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/) (EE). GitLab is available under [different subscriptions](https://about.gitlab.com/pricing/). +There are two software distributions of GitLab: -New versions of GitLab are released in stable branches and the master branch is for bleeding edge development. +- The open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/) (CE). +- The open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/) (EE). -For information, see the [GitLab Release Process](https://gitlab.com/gitlab-org/release/docs/-/tree/master#gitlab-release-process). +GitLab is available under [different subscriptions](https://about.gitlab.com/pricing/). -Both EE and CE require some add-on components called GitLab Shell and Gitaly. These components are available from the [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell/-/tree/master) and [Gitaly](https://gitlab.com/gitlab-org/gitaly/-/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with the exception of informal security updates deemed critical. +New versions of GitLab are released from stable branches, and the `master` branch is used for +bleeding-edge development. + +For more information, visit the [GitLab Release Process](https://about.gitlab.com/handbook/engineering/releases/). + +Both distributions require additional components. These components are described in the +[Component details](#components) section, and all have their own repositories. +New versions of each dependent component are usually tags, but staying on the `master` branch of the +GitLab codebase gives you the latest stable version of those components. New versions are +generally released around the same time as GitLab releases, with the exception of informal security +updates deemed critical. ## Components -A typical install of GitLab will be on GNU/Linux. It uses NGINX or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and pre-compiled assets. GitLab serves web pages and the [GitLab API](../api/README.md) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses Redis as a non-persistent database backend for job information, meta data, and incoming jobs. +A typical install of GitLab is on GNU/Linux, but growing number of deployments also use the +Kubernetes platform. The largest known GitLab instance is on GitLab.com, which is deployed using our +[official GitLab Helm chart](https://docs.gitlab.com/charts/) and the [official Linux package](https://about.gitlab.com/install/). -We also support deploying GitLab on Kubernetes using our [GitLab Helm chart](https://docs.gitlab.com/charts/). +A typical installation uses NGINX or Apache as a web server to proxy through +[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) and into the [Puma](https://puma.io) +application server. GitLab serves web pages and the [GitLab API](../api/README.md) using the Puma +application server. It uses Sidekiq as a job queue which, in turn, uses Redis as a non-persistent +database backend for job information, metadata, and incoming jobs. -The GitLab web app uses PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare Git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. +By default, communication between Puma and Workhorse is via a Unix domain socket, but forwarding +requests via TCP is also supported. Workhorse accesses the `gitlab/public` directory, bypassing the +Puma application server to serve static pages, uploads (for example, avatar images or attachments), +and pre-compiled assets. -When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving Git objects. +The GitLab application uses PostgreSQL for persistent database information (for example, users, +permissions, issues, or other metadata). GitLab stores the bare Git repositories in the location +defined in [the configuration file, `repositories:` section](https://gitlab.com/gitlab-org/gitlab/blob/master/config/gitlab.yml.example). +It also keeps default branch and hook information with the bare repository. -The add-on component GitLab Shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. GitLab Shell accesses the bare repositories through Gitaly to serve Git objects and communicates with Redis to submit jobs to Sidekiq for GitLab to process. GitLab Shell queries the GitLab API to determine authorization and access. +When serving repositories over HTTP/HTTPS GitLab uses the GitLab API to resolve authorization and +access and to serve Git objects. -Gitaly executes Git operations from GitLab Shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from Git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files). +The add-on component GitLab Shell serves repositories over SSH. It manages the SSH keys within the +location defined in [the configuration file, `GitLab Shell` section](https://gitlab.com/gitlab-org/gitlab/blob/master/config/gitlab.yml.example). +The file in that location should never be manually edited. GitLab Shell accesses the bare +repositories through Gitaly to serve Git objects, and communicates with Redis to submit jobs to +Sidekiq for GitLab to process. GitLab Shell queries the GitLab API to determine authorization and access. + +Gitaly executes Git operations from GitLab Shell and the GitLab web app, and provides an API to the +GitLab web app to get attributes from Git (for example, title, branches, tags, or other metadata), +and to get blobs (for example, diffs, commits, or files). You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/). -### Simplified Component Overview +## Adapting existing and introducing new components + +There are fundamental differences in how the application behaves when it is installed on a +traditional Linux machine compared to a containerized platform, such as Kubernetes. + +Compared to [our official installation methods](https://about.gitlab.com/install/), some of the +notable differences are: + +- Official Linux packages can access files on the same file system with different services. + [Shared files](shared_files.md) are not an option for the application running on the Kubernetes + platform. +- Official Linux packages by default have services that have access to the shared configuration and + network. This is not the case for services running in Kubernetes, where services might be running + in complete isolation, or only accessible through specific ports. + +In other words, the shared state between services needs to be carefully considered when +architecting new features and adding new components. Services that need to have access to the same +files, need to be able to exchange information through the appropriate APIs. Whenever possible, +this should not be done with files. + +Since components written with the API-first philosophy in mind are compatible with both methods, all +new features and services must be written to consider Kubernetes compatibility **first**. + +The simplest way to ensure this, is to add support for your feature or service to +[the official GitLab Helm chart](https://docs.gitlab.com/charts/) or reach out to +[the Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/#how-to-work-with-distribution). + +### Simplified component overview This is a simplified architecture diagram that can be used to understand GitLab's architecture. @@ -411,7 +470,8 @@ For monitoring deployed apps, see [Jaeger tracing documentation](../operations/t - Layer: Core Service - Process: `logrotate` -GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering. +GitLab is comprised of a large number of services that all log. We started bundling our own Logrotate +as of GitLab 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering. #### Mattermost @@ -669,7 +729,7 @@ You can install them after you create a cluster. This includes: - [JupyterHub](https://jupyter.org) - [Knative](https://cloud.google.com/knative/) -## GitLab by Request Type +## GitLab by request type GitLab provides two "interfaces" for end users to access the service: @@ -678,7 +738,7 @@ GitLab provides two "interfaces" for end users to access the service: It's important to understand the distinction as some processes are used in both and others are exclusive to a specific request type. -### GitLab Web HTTP Request Cycle +### GitLab Web HTTP request cycle When making a request to an HTTP Endpoint (think `/users/sign_in`) the request will take the following path through the GitLab Service: @@ -687,11 +747,11 @@ When making a request to an HTTP Endpoint (think `/users/sign_in`) the request w - Unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn. - PostgreSQL/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retrieve data. -### GitLab Git Request Cycle +### GitLab Git request cycle Below we describe the different paths that HTTP vs. SSH Git requests will take. There is some overlap with the Web Request Cycle but also some differences. -### Web Request (80/443) +### Web request (80/443) Git operations over HTTP use the stateless "smart" protocol described in the [Git documentation](https://git-scm.com/docs/http-protocol), but responsibility @@ -736,7 +796,7 @@ sequenceDiagram The sequence is similar for `git push`, except `git-receive-pack` is used instead of `git-upload-pack`. -### SSH Request (22) +### SSH request (22) Git operations over SSH can use the stateful protocol described in the [Git documentation](https://git-scm.com/docs/pack-protocol#_ssh_transport), but @@ -801,7 +861,7 @@ except there is no round-trip into Gitaly - Rails performs the action as part of the [internal API](internal_api.md) call, and GitLab Shell streams the response back to the user directly. -## System Layout +## System layout When referring to `~git` in the pictures it means the home directory of the Git user which is typically `/home/git`. @@ -811,7 +871,7 @@ The bare repositories are located in `/home/git/repositories`. GitLab is a Ruby To serve repositories over SSH there's an add-on application called GitLab Shell which is installed in `/home/git/gitlab-shell`. -### Installation Folder Summary +### Installation folder summary To summarize here's the [directory structure of the `git` user home directory](../install/structure.md). @@ -824,7 +884,7 @@ ps aux | grep '^git' GitLab has several components to operate. It requires a persistent database (PostgreSQL) and Redis database, and uses Apache `httpd` or NGINX to proxypass Unicorn. All these components should run as different system users to GitLab -(e.g., `postgres`, `redis` and `www-data`, instead of `git`). +(for example, `postgres`, `redis`, and `www-data`, instead of `git`). As the `git` user it starts Sidekiq and Unicorn (a simple Ruby HTTP server running on port `8080` by default). Under the GitLab user there are normally 4 @@ -914,15 +974,16 @@ PostgreSQL: ### GitLab specific configuration files -GitLab has configuration files located in `/home/git/gitlab/config/*`. Commonly referenced config files include: +GitLab has configuration files located in `/home/git/gitlab/config/*`. Commonly referenced +configuration files include: -- `gitlab.yml` - GitLab configuration. -- `unicorn.rb` - Unicorn web server settings. -- `database.yml` - Database connection settings. +- `gitlab.yml` - GitLab configuration +- `unicorn.rb` - Unicorn web server settings +- `database.yml` - Database connection settings GitLab Shell has a configuration file at `/home/git/gitlab-shell/config.yml`. -### Maintenance Tasks +### Maintenance tasks [GitLab](https://gitlab.com/gitlab-org/gitlab/tree/master) provides Rake tasks with which you see version information and run a quick check on your configuration to ensure it is configured properly within the application. See [maintenance Rake tasks](../raketasks/maintenance.md). In a nutshell, do the following: @@ -934,7 +995,8 @@ bundle exec rake gitlab:env:info RAILS_ENV=production bundle exec rake gitlab:check RAILS_ENV=production ``` -Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While the sudo commands provided by GitLab work in Ubuntu they do not always work in RHEL. +Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While +the `sudo` commands provided by GitLab work in Ubuntu they do not always work in RHEL. ## GitLab.com diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 51d9b4f45cd..8c461e27e70 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -86,8 +86,9 @@ You can also dismiss vulnerabilities in the table: The group Security Dashboard gives an overview of the vulnerabilities in the default branches of the projects in a group and its subgroups. Access it by navigating to **Security > Security Dashboard** -for your group. By default, the Security Dashboard displays all detected and confirmed -vulnerabilities. +after selecting your group. By default, the Security Dashboard displays all detected and confirmed +vulnerabilities. If you don't see the vulnerabilities over time graph, the likely cause is that you +have not selected a group. NOTE: **Note:** The Security Dashboard only shows projects with [security reports](#supported-reports) enabled in a @@ -96,20 +97,20 @@ group. ![Dashboard with action buttons and metrics](img/group_security_dashboard_v13_3.png) There is a timeline chart that shows how many open -vulnerabilities your projects had at various points in time. You can filter among 30, 60, and -90 days, with the default being 90. Hover over the chart to get more details about -the open vulnerabilities at a specific time. +vulnerabilities your projects had at various points in time. You can display the vulnerability +trends over a 30, 60, or 90-day time frame (the default is 90 days). Hover over the chart to get +more details about the open vulnerabilities at a specific time. Next to the timeline chart is a list of projects, grouped and sorted by the severity of the vulnerability found: -- F: 1 or more "critical" -- D: 1 or more "high" or "unknown" -- C: 1 or more "medium" -- B: 1 or more "low" -- A: 0 vulnerabilities +- F: One or more "critical" +- D: One or more "high" or "unknown" +- C: One or more "medium" +- B: One or more "low" +- A: Zero vulnerabilities Projects with no vulnerability tests configured will not appear in the list. Additionally, dismissed -vulnerabilities are not included either. +vulnerabilities are excluded. Navigate to the group's [Vulnerability Report](#vulnerability-list) to view the vulnerabilities found. diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 077666bc036..c67f2ab5738 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -512,6 +512,11 @@ Cleanup policies can be run on all projects, with these exceptions: for all projects (even those created before 12.8) in [GitLab application settings](../../../api/settings.md#change-application-settings) by setting `container_expiration_policies_enable_historic_entries` to true. + Alternatively, you can execute the following command in the [Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session): + + ```ruby + ApplicationSetting.last.update(container_expiration_policies_enable_historic_entries: true) + ``` There are performance risks with enabling it for all projects, especially if you are using an [external registry](./index.md#use-with-external-container-registries). diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 821b42af049..2c4c5d59ae0 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -32,7 +32,7 @@ file path fragments to start seeing results. ## Syntax highlighting As expected from an IDE, syntax highlighting for many languages within -the Web IDE will make your direct editing even easier. +the Web IDE makes your direct editing even easier. The Web IDE currently provides: @@ -143,7 +143,7 @@ The Web IDE supports configuration of certain editor settings by using [`.editorconfig` files](https://editorconfig.org/). When opening a file, the Web IDE looks for a file named `.editorconfig` in the current directory and all parent directories. If a configuration file is found and has settings -that match the file's path, these settings will be enforced on the opened file. +that match the file's path, these settings are enforced on the opened file. The Web IDE currently supports the following `.editorconfig` settings: @@ -166,7 +166,7 @@ review the list of changed files. Once you have finalized your changes, you can add a commit message, commit the changes and directly create a merge request. In case you don't have write -access to the selected branch, you will see a warning, but still be able to create +access to the selected branch, you see a warning, but can still create a new branch and start a merge request. To discard a change in a particular file, click the **Discard changes** button on that @@ -201,8 +201,7 @@ left. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19318) in [GitLab Core](https://about.gitlab.com/pricing/) 11.0. To switch between your authored and assigned merge requests, click the -dropdown in the top of the sidebar to open a list of merge requests. You will -need to commit or discard all your changes before switching to a different merge +dropdown in the top of the sidebar to open a list of merge requests. You need to commit or discard all your changes before switching to a different merge request. ## Switching branches @@ -211,7 +210,7 @@ request. To switch between branches of the current project repository, click the dropdown in the top of the sidebar to open a list of branches. -You will need to commit or discard all your changes before switching to a +You need to commit or discard all your changes before switching to a different branch. ## Markdown editing @@ -226,7 +225,7 @@ supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-g You can also upload any local images by pasting them directly in the Markdown file. The image is uploaded to the same directory and is named `image.png` by default. If another file already exists with the same name, a numeric suffix is automatically -added to the file name. +added to the filename. ## Live Preview @@ -249,7 +248,7 @@ The Live Preview feature needs to be enabled in the GitLab instances admin settings. Live Preview is enabled for all projects on GitLab.com -![Admin Live Preview setting](img/admin_live_preview_v13_0.png) +![Administrator Live Preview setting](img/admin_live_preview_v13_0.png) Once you have done that, you can preview projects with a `package.json` file and a `main` entry point inside the Web IDE. An example `package.json` is shown @@ -292,7 +291,7 @@ to work: [enabled](../../../administration/integration/terminal.md#enabling-and-disabling-terminal-support). **(ULTIMATE ONLY)** If you have the terminal open and the job has finished with its tasks, the -terminal will block the job from finishing for the duration configured in +terminal blocks the job from finishing for the duration configured in [`[session_server].session_timeout`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) until you close the terminal window. @@ -308,15 +307,15 @@ In order to enable the Web IDE terminals you need to create the file file is fairly similar to the [CI configuration file](../../../ci/yaml/README.md) syntax but with some restrictions: -- No global blocks can be defined (ie: `before_script` or `after_script`) +- No global blocks can be defined (i.e., `before_script` or `after_script`) - Only one job named `terminal` can be added to this file. - Only the keywords `image`, `services`, `tags`, `before_script`, `script`, and `variables` are allowed to be used to configure the job. - To connect to the interactive terminal, the `terminal` job must be still alive - and running, otherwise the terminal won't be able to connect to the job's session. + and running, otherwise the terminal cannot connect to the job's session. By default the `script` keyword has the value `sleep 60` to prevent the job from ending and giving the Web IDE enough time to connect. This means - that, if you override the default `script` value, you'll have to add a command + that, if you override the default `script` value, you have to add a command which would keep the job running, like `sleep`. In the code below there is an example of this configuration file: @@ -333,40 +332,39 @@ terminal: NODE_ENV: "test" ``` -Once the terminal has started, the console will be displayed and we could access +Once the terminal has started, the console is displayed and we could access the project repository files. **Important**. The terminal job is branch dependent. This means that the -configuration file used to trigger and configure the terminal will be the one in +configuration file used to trigger and configure the terminal is the one in the selected branch of the Web IDE. -If there is no configuration file in a branch, an error message will be shown. +If there is no configuration file in a branch, an error message is shown. ### Running interactive terminals in the Web IDE -If Interactive Terminals are available for the current user, the **Terminal** button -will be visible in the right sidebar of the Web IDE. Click this button to open +If Interactive Terminals are available for the current user, the **Terminal** button is visible in the right sidebar of the Web IDE. Click this button to open or close the terminal tab. -Once open, the tab will show the **Start Web Terminal** button. This button may +Once open, the tab shows the **Start Web Terminal** button. This button may be disabled if the environment is not configured correctly. If so, a status -message will describe the issue. Here are some reasons why **Start Web Terminal** +message describes the issue. Here are some reasons why **Start Web Terminal** may be disabled: - `.gitlab/.gitlab-webide.yml` does not exist or is set up incorrectly. - No active private runners are available for the project. -If active, clicking the **Start Web Terminal** button will load the terminal view +If active, clicking the **Start Web Terminal** button loads the terminal view and start connecting to the runner's terminal. At any time, the **Terminal** tab -can be closed and reopened and the state of the terminal will not be affected. +can be closed and reopened and the state of the terminal is not affected. When the terminal is started and is successfully connected to the runner, then the -runner's shell prompt will appear in the terminal. From here, you can enter -commands that will be executed within the runner's environment. This is similar +runner's shell prompt appears in the terminal. From here, you can enter +commands executed within the runner's environment. This is similar to running commands in a local terminal or through SSH. While the terminal is running, it can be stopped by clicking **Stop Terminal**. -This will disconnect the terminal and stop the runner's terminal job. From here, +This disconnects the terminal and stops the runner's terminal job. From here, click **Restart Terminal** to start a new terminal session. ### File syncing to web terminal @@ -408,14 +406,14 @@ terminal: more information. - `$CI_PROJECT_DIR` is a [predefined environment variable](../../../ci/variables/predefined_variables.md) - for GitLab Runner. This is where your project's repository will be. + for GitLab Runners. This is where your project's repository resides. Once you have configured the web terminal for file syncing, then when the web -terminal is started, a **Terminal** status will be visible in the status bar. +terminal is started, a **Terminal** status is visible in the status bar. ![Web IDE Client Side Evaluation](img/terminal_status.png) -Changes made to your files via the Web IDE will sync to the running terminal +Changes made to your files via the Web IDE sync to the running terminal when: - Ctrl + S (or Cmd + S on Mac) @@ -425,7 +423,7 @@ when: ### Limitations -Interactive Terminals is in a beta phase and will continue to be improved upon in upcoming +Interactive Terminals is in a beta phase and continues to be improved in upcoming releases. In the meantime, please note that the user is limited to having only one active terminal at a time. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 409f045cd82..d0d0606a34e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,6 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-09-17 11:26-0400\n" -"PO-Revision-Date: 2020-09-17 11:26-0400\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -7184,6 +7182,9 @@ msgstr "" msgid "Could not find iteration" msgstr "" +msgid "Could not load instance counts. Please refresh the page to try again." +msgstr "" + msgid "Could not remove the trigger." msgstr "" @@ -13586,6 +13587,24 @@ msgstr "" msgid "Instance administrators group already exists" msgstr "" +msgid "InstanceStatistics|Groups" +msgstr "" + +msgid "InstanceStatistics|Issues" +msgstr "" + +msgid "InstanceStatistics|Merge Requests" +msgstr "" + +msgid "InstanceStatistics|Pipelines" +msgstr "" + +msgid "InstanceStatistics|Projects" +msgstr "" + +msgid "InstanceStatistics|Users" +msgstr "" + msgid "Integration" msgstr "" @@ -26166,6 +26185,9 @@ msgstr "" msgid "Threat Monitoring" msgstr "" +msgid "ThreatMonitoring|All Environments" +msgstr "" + msgid "ThreatMonitoring|Anomalous Requests" msgstr "" diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 5c7b88a218a..2c85fe482e2 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -9,7 +9,6 @@ RSpec.describe Groups::MilestonesController do let(:user) { create(:user) } let(:title) { '肯定不是中文的问题' } let(:milestone) { create(:milestone, project: project) } - let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) } let(:milestone_params) do { @@ -25,6 +24,12 @@ RSpec.describe Groups::MilestonesController do project.add_maintainer(user) end + it_behaves_like 'milestone tabs' do + let(:milestone) { create(:milestone, group: group) } + let(:milestone_path) { group_milestone_path(group, milestone.iid) } + let(:request_params) { { group_id: group, id: milestone.iid } } + end + describe '#index' do describe 'as HTML' do render_views diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index fa32d32f552..9e5d41b1075 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -17,7 +17,9 @@ RSpec.describe Projects::MilestonesController do controller.instance_variable_set(:@project, project) end - it_behaves_like 'milestone tabs' + it_behaves_like 'milestone tabs' do + let(:request_params) { { namespace_id: project.namespace, project_id: project, id: milestone.iid } } + end describe "#show" do render_views diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb index 420f8d49483..11c6fa521d5 100644 --- a/spec/features/milestones/user_views_milestone_spec.rb +++ b/spec/features/milestones/user_views_milestone_spec.rb @@ -4,12 +4,16 @@ require 'spec_helper' RSpec.describe "User views milestone" do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:labels) { create_list(:label, 2, project: project) } - before do + before_all do project.add_developer(user) + end + + before do sign_in(user) end @@ -25,7 +29,7 @@ RSpec.describe "User views milestone" do expect { visit_milestone }.not_to exceed_query_limit(control) end - context 'limiting milestone issues' do + context 'issues list', :js do before_all do 2.times do create(:issue, milestone: milestone, project: project) @@ -34,6 +38,28 @@ RSpec.describe "User views milestone" do end end + context 'for a project milestone' do + it 'does not show the project name' do + visit(project_milestone_path(project, milestone)) + + wait_for_requests + + expect(page.find('#tab-issues')).not_to have_text(project.name) + end + end + + context 'for a group milestone' do + let(:group_milestone) { create(:milestone, group: group) } + + it 'shows the project name' do + create(:issue, project: project, milestone: group_milestone) + + visit(group_milestone_path(group, group_milestone)) + + expect(page.find('#tab-issues')).to have_text(project.name) + end + end + context 'when issues on milestone are over DISPLAY_ISSUES_LIMIT' do it "limits issues to display and shows warning" do stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 3) @@ -56,6 +82,40 @@ RSpec.describe "User views milestone" do end end + context 'merge requests list', :js do + context 'for a project milestone' do + it 'does not show the project name' do + create(:merge_request, source_project: project, milestone: milestone) + + visit(project_milestone_path(project, milestone)) + + within('.js-milestone-tabs') do + click_link('Merge Requests') + end + + wait_for_requests + + expect(page.find('#tab-merge-requests')).not_to have_text(project.name) + end + end + + context 'for a group milestone' do + let(:group_milestone) { create(:milestone, group: group) } + + it 'shows the project name' do + create(:merge_request, source_project: project, milestone: group_milestone) + + visit(group_milestone_path(group, group_milestone)) + + within('.js-milestone-tabs') do + click_link('Merge Requests') + end + + expect(page.find('#tab-merge-requests')).to have_text(project.name) + end + end + end + private def visit_milestone diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb index 3f606577121..f8b4b802a60 100644 --- a/spec/features/milestones/user_views_milestones_spec.rb +++ b/spec/features/milestones/user_views_milestones_spec.rb @@ -21,7 +21,7 @@ RSpec.describe "User views milestones" do .and have_content("Merge Requests") end - context "with issues" do + context "with issues", :js do let_it_be(:issue) { create(:issue, project: project, milestone: milestone) } let_it_be(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) } @@ -33,7 +33,6 @@ RSpec.describe "User views milestones" do .and have_selector("#tab-issues li.issuable-row", count: 2) .and have_content(issue.title) .and have_content(closed_issue.title) - .and have_selector("#tab-merge-requests") end end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 7f2ef61bcbe..8c032660726 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -372,7 +372,7 @@ RSpec.describe 'Environments page', :js do let(:role) { :developer } it 'developer creates a new environment with a valid name' do - within(".top-area") { click_link 'New environment' } + within(".environments-section") { click_link 'New environment' } fill_in('Name', with: 'production') click_on 'Save' @@ -380,7 +380,7 @@ RSpec.describe 'Environments page', :js do end it 'developer creates a new environmetn with invalid name' do - within(".top-area") { click_link 'New environment' } + within(".environments-section") { click_link 'New environment' } fill_in('Name', with: 'name,with,commas') click_on 'Save' diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index cfda25b9ab4..5cbfacf4e48 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -30,6 +30,8 @@ RSpec.describe 'User uses header search field', :js do before do find('#search') find('body').native.send_keys('s') + + wait_for_all_requests end it 'shows the category search dropdown' do @@ -89,9 +91,7 @@ RSpec.describe 'User uses header search field', :js do context 'when entering text into the search field' do it 'does not display the category search dropdown' do - page.within('.search-input-wrap') do - fill_in('search', with: scope_name.first(4)) - end + fill_in_search(scope_name.first(4)) expect(page).not_to have_selector('.dropdown-header', text: /#{scope_name}/i) end @@ -105,9 +105,7 @@ RSpec.describe 'User uses header search field', :js do end it 'displays search options' do - page.within('.search-input-wrap') do - fill_in('search', with: 'test') - end + fill_in_search('test') expect(page).to have_selector(scoped_search_link('test')) end @@ -140,9 +138,7 @@ RSpec.describe 'User uses header search field', :js do end it 'displays search options' do - page.within('.search-input-wrap') do - fill_in('search', with: 'test') - end + fill_in_search('test') expect(page).to have_selector(scoped_search_link('test')) expect(page).to have_selector(scoped_search_link('test', group_id: group.id)) @@ -157,9 +153,7 @@ RSpec.describe 'User uses header search field', :js do end it 'displays search options' do - page.within('.search-input-wrap') do - fill_in('search', with: 'test') - end + fill_in_search('test') expect(page).to have_selector(scoped_search_link('test')) expect(page).not_to have_selector(scoped_search_link('test', group_id: project.namespace_id)) @@ -182,9 +176,7 @@ RSpec.describe 'User uses header search field', :js do end it 'displays search options' do - page.within('.search-input-wrap') do - fill_in('search', with: 'test') - end + fill_in_search('test') expect(page).to have_selector(scoped_search_link('test')) expect(page).to have_selector(scoped_search_link('test', group_id: group.id)) @@ -208,9 +200,7 @@ RSpec.describe 'User uses header search field', :js do end it 'displays search options' do - page.within('.search-input-wrap') do - fill_in('search', with: 'test') - end + fill_in_search('test') expect(page).to have_selector(scoped_search_link('test')) expect(page).to have_selector(scoped_search_link('test', group_id: subgroup.id)) diff --git a/spec/frontend/analytics/instance_statistics/components/app_spec.js b/spec/frontend/analytics/instance_statistics/components/app_spec.js new file mode 100644 index 00000000000..242621dc40c --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/app_spec.js @@ -0,0 +1,24 @@ +import { shallowMount } from '@vue/test-utils'; +import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue'; +import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue'; + +describe('InstanceStatisticsApp', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(InstanceStatisticsApp); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('displays the instance counts component', () => { + expect(wrapper.find(InstanceCounts).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js b/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js new file mode 100644 index 00000000000..2274f4c3fde --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; +import InstanceCounts from '~/analytics/instance_statistics/components/instance_counts.vue'; +import MetricCard from '~/analytics/shared/components/metric_card.vue'; +import countsMockData from '../mock_data'; + +describe('InstanceCounts', () => { + let wrapper; + + const createComponent = ({ loading = false, data = {} } = {}) => { + const $apollo = { + queries: { + counts: { + loading, + }, + }, + }; + + wrapper = shallowMount(InstanceCounts, { + mocks: { $apollo }, + data() { + return { + ...data, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findMetricCard = () => wrapper.find(MetricCard); + + describe('while loading', () => { + beforeEach(() => { + createComponent({ loading: true }); + }); + + it('displays the metric card with isLoading=true', () => { + expect(findMetricCard().props('isLoading')).toBe(true); + }); + }); + + describe('with data', () => { + beforeEach(() => { + createComponent({ data: { counts: countsMockData } }); + }); + + it('passes the counts data to the metric card', () => { + expect(findMetricCard().props('metrics')).toEqual(countsMockData); + }); + }); +}); diff --git a/spec/frontend/analytics/instance_statistics/mock_data.js b/spec/frontend/analytics/instance_statistics/mock_data.js new file mode 100644 index 00000000000..9fabf3a4c65 --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/mock_data.js @@ -0,0 +1,4 @@ +export default [ + { key: 'projects', value: 10, label: 'Projects' }, + { key: 'groups', value: 20, label: 'Group' }, +]; diff --git a/spec/frontend/analytics/shared/components/metric_card_spec.js b/spec/frontend/analytics/shared/components/metric_card_spec.js new file mode 100644 index 00000000000..e89d499ed9b --- /dev/null +++ b/spec/frontend/analytics/shared/components/metric_card_spec.js @@ -0,0 +1,129 @@ +import { mount } from '@vue/test-utils'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import MetricCard from '~/analytics/shared/components/metric_card.vue'; + +const metrics = [ + { key: 'first_metric', value: 10, label: 'First metric', unit: 'days', link: 'some_link' }, + { key: 'second_metric', value: 20, label: 'Yet another metric' }, + { key: 'third_metric', value: null, label: 'Null metric without value', unit: 'parsecs' }, + { key: 'fourth_metric', value: '-', label: 'Metric without value', unit: 'parsecs' }, +]; + +const defaultProps = { + title: 'My fancy title', + isLoading: false, + metrics, +}; + +describe('MetricCard', () => { + let wrapper; + + const factory = (props = defaultProps) => { + wrapper = mount(MetricCard, { + propsData: { + ...defaultProps, + ...props, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findTitle = () => wrapper.find({ ref: 'title' }); + const findLoadingIndicator = () => wrapper.find(GlSkeletonLoading); + const findMetricsWrapper = () => wrapper.find({ ref: 'metricsWrapper' }); + const findMetricItem = () => wrapper.findAll({ ref: 'metricItem' }); + const findTooltip = () => wrapper.find('[data-testid="tooltip"]'); + + describe('template', () => { + it('renders the title', () => { + factory(); + + expect(findTitle().text()).toContain('My fancy title'); + }); + + describe('when isLoading is true', () => { + beforeEach(() => { + factory({ isLoading: true }); + }); + + it('displays a loading indicator', () => { + expect(findLoadingIndicator().exists()).toBe(true); + }); + + it('does not display the metrics container', () => { + expect(findMetricsWrapper().exists()).toBe(false); + }); + }); + + describe('when isLoading is false', () => { + beforeEach(() => { + factory({ isLoading: false }); + }); + + it('does not display a loading indicator', () => { + expect(findLoadingIndicator().exists()).toBe(false); + }); + + it('displays the metrics container', () => { + expect(findMetricsWrapper().exists()).toBe(true); + }); + + it('renders two metrics', () => { + expect(findMetricItem()).toHaveLength(metrics.length); + }); + + describe('with tooltip text', () => { + const tooltipText = 'This is a tooltip'; + const tooltipMetric = { + key: 'fifth_metric', + value: '-', + label: 'Metric with tooltip', + unit: 'parsecs', + tooltipText, + }; + + beforeEach(() => { + factory({ + isLoading: false, + metrics: [tooltipMetric], + }); + }); + + it('will render a tooltip', () => { + const tt = getBinding(findTooltip().element, 'gl-tooltip'); + expect(tt.value.title).toEqual(tooltipText); + }); + }); + + describe.each` + columnIndex | label | value | unit | link + ${0} | ${'First metric'} | ${10} | ${' days'} | ${'some_link'} + ${1} | ${'Yet another metric'} | ${20} | ${''} | ${null} + ${2} | ${'Null metric without value'} | ${'-'} | ${''} | ${null} + ${3} | ${'Metric without value'} | ${'-'} | ${''} | ${null} + `('metric columns', ({ columnIndex, label, value, unit, link }) => { + it(`renders ${value}${unit} ${label} with URL ${link}`, () => { + const allMetricItems = findMetricItem(); + const metricItem = allMetricItems.at(columnIndex); + const text = metricItem.text(); + + expect(text).toContain(`${value}${unit}`); + expect(text).toContain(label); + + if (link) { + expect(metricItem.find('a').attributes('href')).toBe(link); + } else { + expect(metricItem.find('a').exists()).toBe(false); + } + }); + }); + }); + }); +}); diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js index fe32bf918dd..22b066fae41 100644 --- a/spec/frontend/environments/environments_app_spec.js +++ b/spec/frontend/environments/environments_app_spec.js @@ -40,6 +40,9 @@ describe('Environment', () => { return axios.waitForAll(); }; + const findEnvironmentsTabAvailable = () => wrapper.find('.js-environments-tab-available > a'); + const findEnvironmentsTabStopped = () => wrapper.find('.js-environments-tab-stopped > a'); + beforeEach(() => { mock = new MockAdapter(axios); }); @@ -108,9 +111,16 @@ describe('Environment', () => { it('should make an API request when using tabs', () => { jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {}); - wrapper.find('.js-environments-tab-stopped').trigger('click'); + findEnvironmentsTabStopped().trigger('click'); expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' }); }); + + it('should not make the same API request when clicking on the current scope tab', () => { + // component starts at available + jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {}); + findEnvironmentsTabAvailable().trigger('click'); + expect(wrapper.vm.updateContent).toHaveBeenCalledTimes(0); + }); }); }); }); diff --git a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap index 4d9e0af1545..d317264bdae 100644 --- a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap +++ b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap @@ -2,151 +2,163 @@ exports[`PackageTitle renders with tags 1`] = `
- -
-

- Test package -

+
- +

+ Test package +

- + + + +
+
+
+ +
+
+ +
+
+ +
+
+
-
-
- -
-
- -
-
- -
-
+
- +

`; exports[`PackageTitle renders without tags 1`] = `

- -
-

- Test package -

+
- +

+ Test package +

- + + + +
+
+
+ +
+
+ +
+
+
-
-
- -
-
- -
-
+
- +

`; diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js index 7a27f8fa431..3c997093d46 100644 --- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlSprintf, GlLink } from '@gitlab/ui'; +import { GlSprintf } from '@gitlab/ui'; import Component from '~/registry/explorer/components/list_page/registry_header.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { @@ -19,12 +19,8 @@ describe('registry_header', () => { const findTitleArea = () => wrapper.find(TitleArea); const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]'); - const findInfoArea = () => wrapper.find('[data-testid="info-area"]'); - const findIntroText = () => wrapper.find('[data-testid="default-intro"]'); const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]'); const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]'); - const findDisabledExpirationPolicyMessage = () => - wrapper.find('[data-testid="expiration-disabled-message"]'); const mountComponent = (propsData, slots) => { wrapper = shallowMount(Component, { @@ -123,44 +119,18 @@ describe('registry_header', () => { }); }); - describe('info area', () => { - it('exists', () => { - mountComponent(); - - expect(findInfoArea().exists()).toBe(true); - }); - + describe('info messages', () => { describe('default message', () => { - beforeEach(() => { - return mountComponent({ helpPagePath: 'bar' }); - }); + it('is correctly bound to title_area props', () => { + mountComponent({ helpPagePath: 'foo' }); - it('exists', () => { - expect(findIntroText().exists()).toBe(true); - }); - - it('has the correct copy', () => { - expect(findIntroText().text()).toMatchInterpolatedText(LIST_INTRO_TEXT); - }); - - it('has the correct link', () => { - expect( - findIntroText() - .find(GlLink) - .attributes('href'), - ).toBe('bar'); + expect(findTitleArea().props('infoMessages')).toEqual([ + { text: LIST_INTRO_TEXT, link: 'foo' }, + ]); }); }); describe('expiration policy info message', () => { - describe('when there are no images', () => { - it('is hidden', () => { - mountComponent(); - - expect(findDisabledExpirationPolicyMessage().exists()).toBe(false); - }); - }); - describe('when there are images', () => { describe('when expiration policy is disabled', () => { beforeEach(() => { @@ -170,43 +140,27 @@ describe('registry_header', () => { imagesCount: 1, }); }); - it('message exist', () => { - expect(findDisabledExpirationPolicyMessage().exists()).toBe(true); - }); - it('has the correct copy', () => { - expect(findDisabledExpirationPolicyMessage().text()).toMatchInterpolatedText( - EXPIRATION_POLICY_DISABLED_MESSAGE, - ); - }); - it('has the correct link', () => { - expect( - findDisabledExpirationPolicyMessage() - .find(GlLink) - .attributes('href'), - ).toBe('foo'); + it('the prop is correctly bound', () => { + expect(findTitleArea().props('infoMessages')).toEqual([ + { text: LIST_INTRO_TEXT, link: '' }, + { text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: 'foo' }, + ]); }); }); - describe('when expiration policy is enabled', () => { + describe.each` + desc | props + ${'when there are no images'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 0 }} + ${'when expiration policy is enabled'} | ${{ expirationPolicy: { enabled: true }, imagesCount: 1 }} + ${'when the expiration policy is completely disabled'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 1, hideExpirationPolicyData: true }} + `('$desc', ({ props }) => { it('message does not exist', () => { - mountComponent({ - expirationPolicy: { enabled: true }, - imagesCount: 1, - }); + mountComponent(props); - expect(findDisabledExpirationPolicyMessage().exists()).toBe(false); - }); - }); - describe('when the expiration policy is completely disabled', () => { - it('message does not exist', () => { - mountComponent({ - expirationPolicy: { enabled: true }, - imagesCount: 1, - hideExpirationPolicyData: true, - }); - - expect(findDisabledExpirationPolicyMessage().exists()).toBe(false); + expect(findTitleArea().props('infoMessages')).toEqual([ + { text: LIST_INTRO_TEXT, link: '' }, + ]); }); }); }); diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js index 6740d6097a4..a513f178f45 100644 --- a/spec/frontend/vue_shared/components/registry/title_area_spec.js +++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js @@ -1,4 +1,4 @@ -import { GlAvatar } from '@gitlab/ui'; +import { GlAvatar, GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import component from '~/vue_shared/components/registry/title_area.vue'; @@ -10,10 +10,12 @@ describe('title area', () => { const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`); const findTitle = () => wrapper.find('[data-testid="title"]'); const findAvatar = () => wrapper.find(GlAvatar); + const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]'); const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => { wrapper = shallowMount(component, { propsData, + stubs: { GlSprintf }, slots: { 'sub-header': '

', 'right-actions': '
', @@ -95,4 +97,33 @@ describe('title area', () => { }); }); }); + + describe('info-messages', () => { + it('shows a message when the props contains one', () => { + mountComponent({ propsData: { infoMessages: [{ text: 'foo foo bar bar' }] } }); + + const messages = findInfoMessages(); + expect(messages).toHaveLength(1); + expect(messages.at(0).text()).toBe('foo foo bar bar'); + }); + + it('shows a link when the props contains one', () => { + mountComponent({ + propsData: { + infoMessages: [{ text: 'foo %{docLinkStart}link%{docLinkEnd}', link: 'bar' }], + }, + }); + + const message = findInfoMessages().at(0); + + expect(message.find(GlLink).attributes('href')).toBe('bar'); + expect(message.text()).toBe('foo link'); + }); + + it('multiple messages generates multiple spans', () => { + mountComponent({ propsData: { infoMessages: [{ text: 'foo' }, { text: 'bar' }] } }); + + expect(findInfoMessages()).toHaveLength(2); + }); + }); }); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11a83bd9501..6ddced09699 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -124,6 +124,7 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature config.include WaitHelpers, type: :feature + config.include WaitForRequests, type: :feature config.include EmailHelpers, :mailer, type: :mailer config.include Warden::Test::Helpers, type: :request config.include Gitlab::Routing, type: :routing @@ -133,7 +134,6 @@ RSpec.configure do |config| config.include InputHelper, :js config.include SelectionHelper, :js config.include InspectRequests, :js - config.include WaitForRequests, :js config.include LiveDebugger, :js config.include MigrationsHelpers, :migration config.include RedisHelpers diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index db6e47459e9..328f272724a 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true module SearchHelpers + def fill_in_search(text) + page.within('.search-input-wrap') do + fill_in('search', with: text) + end + + wait_for_all_requests + end + def submit_search(query, scope: nil) page.within('.search-form, .search-page-form') do field = find_field('search') @@ -11,6 +19,8 @@ module SearchHelpers else click_button('Search') end + + wait_for_all_requests end end diff --git a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb index 925c45005f0..2d35b1681ea 100644 --- a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb +++ b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb @@ -2,9 +2,28 @@ RSpec.shared_examples 'milestone tabs' do def go(path, extra_params = {}) - params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } + get path, params: request_params.merge(extra_params) + end - get path, params: params.merge(extra_params) + describe '#issues' do + context 'as html' do + before do + go(:issues, format: 'html') + end + + it 'redirects to milestone#show' do + expect(response).to redirect_to(milestone_path) + end + end + + context 'as json' do + it 'renders the issues tab template to a string' do + go(:issues, format: 'json') + + expect(response).to render_template('shared/milestones/_issues_tab') + expect(json_response).to have_key('html') + end + end end describe '#merge_requests' do diff --git a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb deleted file mode 100644 index 7cdc817d784..00000000000 --- a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'projects/merge_requests/diffs/_diffs.html.haml' do - include Devise::Test::ControllerHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: user) } - - before do - allow(view).to receive(:url_for).and_return(controller.request.fullpath) - - assign(:merge_request, merge_request) - assign(:environment, merge_request.environments_for(user).last) - assign(:diffs, merge_request.diffs) - assign(:merge_request_diffs, merge_request.diffs) - assign(:diff_notes_disabled, true) # disable note creation - assign(:use_legacy_diff_notes, false) - assign(:grouped_diff_discussions, {}) - assign(:notes, []) - end - - context 'for a commit' do - let(:commit) { merge_request.commits.last } - - before do - assign(:commit, commit) - end - - it "shows the commit scope" do - render - - expect(rendered).to have_content "Only comments from the following commit are shown below" - end - end -end diff --git a/spec/views/shared/milestones/_issuables.html.haml_spec.rb b/spec/views/shared/milestones/_issuables.html.haml_spec.rb index 70ab6914580..5eed2c96a45 100644 --- a/spec/views/shared/milestones/_issuables.html.haml_spec.rb +++ b/spec/views/shared/milestones/_issuables.html.haml_spec.rb @@ -6,8 +6,7 @@ RSpec.describe 'shared/milestones/_issuables.html.haml' do let(:issuables_size) { 100 } before do - allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil, - show_full_project_name: nil, dom_class: '', + allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil, dom_class: '', issuables: double(length: issuables_size).as_null_object) stub_template 'shared/milestones/_issuable.html.haml' => ''