diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml index efdae0715aa..162dd7b3163 100644 --- a/.gitlab/ci/memory.gitlab-ci.yml +++ b/.gitlab/ci/memory.gitlab-ci.yml @@ -26,6 +26,7 @@ memory-on-boot: NODE_ENV: "production" RAILS_ENV: "production" SETUP_DB: "true" + GITLAB_ALLOW_SEPARATE_CI_DATABASE: "true" MEMORY_ON_BOOT_FILE_PREFIX: "tmp/memory_on_boot_" TEST_COUNT: 5 script: diff --git a/app/assets/javascripts/ci_secure_files/components/metadata/button.vue b/app/assets/javascripts/ci_secure_files/components/metadata/button.vue new file mode 100644 index 00000000000..799c6ec79d4 --- /dev/null +++ b/app/assets/javascripts/ci_secure_files/components/metadata/button.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue b/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue new file mode 100644 index 00000000000..a459b721394 --- /dev/null +++ b/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue @@ -0,0 +1,129 @@ + +`` + + diff --git a/app/assets/javascripts/ci_secure_files/components/metadata/table.vue b/app/assets/javascripts/ci_secure_files/components/metadata/table.vue new file mode 100644 index 00000000000..92043ff0a31 --- /dev/null +++ b/app/assets/javascripts/ci_secure_files/components/metadata/table.vue @@ -0,0 +1,36 @@ + + + diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue index 661389f4059..dd80698ec1a 100644 --- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue +++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue @@ -17,6 +17,8 @@ import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status'; import { __, s__, sprintf } from '~/locale'; import Tracking from '~/tracking'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import MetadataButton from './metadata/button.vue'; +import MetadataModal from './metadata/modal.vue'; export default { components: { @@ -29,6 +31,8 @@ export default { GlSprintf, GlTable, TimeagoTooltip, + MetadataButton, + MetadataModal, }, directives: { GlTooltip: GlTooltipDirective, @@ -57,6 +61,7 @@ export default { deleteModalButton: s__('SecureFiles|Delete secure file'), }, deleteModalId: 'deleteModalId', + metadataModalId: 'metadataModalId', data() { return { page: 1, @@ -68,6 +73,7 @@ export default { projectSecureFiles: [], deleteModalFileId: null, deleteModalFileName: null, + metadataSecureFile: {}, }; }, fields: [ @@ -162,6 +168,9 @@ export default { this.deleteModalFileId = secureFile.id; this.deleteModalFileName = secureFile.name; }, + updateMetadataSecureFile(secureFile) { + this.metadataSecureFile = secureFile; + }, uploadFormData(file) { const formData = new FormData(); formData.append('name', file.name); @@ -208,6 +217,12 @@ export default { + + diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue new file mode 100644 index 00000000000..4973f8596b0 --- /dev/null +++ b/app/assets/javascripts/merge_requests/components/compare_app.vue @@ -0,0 +1,119 @@ + + + diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue new file mode 100644 index 00000000000..e005044b4e5 --- /dev/null +++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue @@ -0,0 +1,139 @@ + + + diff --git a/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue b/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue deleted file mode 100644 index cd2e25793f4..00000000000 --- a/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index b3868653d6a..d2ac23c48e2 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,37 +1,84 @@ -import $ from 'jquery'; import Vue from 'vue'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; import MergeRequest from '~/merge_request'; -import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue'; +import CompareApp from '~/merge_requests/components/compare_app.vue'; +import { __ } from '~/locale'; import initCompare from './compare'; const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); if (mrNewCompareNode) { - initCompare(mrNewCompareNode); + const targetCompareEl = document.getElementById('js-target-project-dropdown'); + const sourceCompareEl = document.getElementById('js-source-project-dropdown'); + const compareEl = document.querySelector('.js-merge-request-new-compare'); - const el = document.getElementById('js-target-project-dropdown'); - const { targetProjectsPath, currentProject } = el.dataset; - - // eslint-disable-next-line no-new - new Vue({ - el, - name: 'TargetProjectDropdown', - provide: { - targetProjectsPath, - currentProject: JSON.parse(currentProject), - }, - render(h) { - return h(TargetProjectDropdown, { - on: { - 'project-selected': function projectSelectedFunction(refsUrl) { - const $targetBranchDropdown = $('.js-target-branch'); - $targetBranchDropdown.data('refsUrl', refsUrl); - $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu(); + if (window.gon?.features?.mrCompareDropdowns) { + // eslint-disable-next-line no-new + new Vue({ + el: sourceCompareEl, + name: 'SourceCompareApp', + provide: { + currentProject: JSON.parse(sourceCompareEl.dataset.currentProject), + currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch), + branchCommitPath: compareEl.dataset.sourceBranchUrl, + inputs: { + project: { + id: 'merge_request_source_project_id', + name: 'merge_request[source_project_id]', + }, + branch: { + id: 'merge_request_source_branch', + name: 'merge_request[source_branch]', }, }, - }); - }, - }); + i18n: { + projectHeaderText: __('Select source project'), + branchHeaderText: __('Select source branch'), + }, + toggleClass: { + project: 'js-source-project', + branch: 'js-source-branch', + }, + }, + render(h) { + return h(CompareApp); + }, + }); + + // eslint-disable-next-line no-new + new Vue({ + el: targetCompareEl, + name: 'TargetCompareApp', + provide: { + currentProject: JSON.parse(targetCompareEl.dataset.currentProject), + currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch), + projectsPath: targetCompareEl.dataset.targetProjectsPath, + branchCommitPath: compareEl.dataset.targetBranchUrl, + inputs: { + project: { + id: 'merge_request_target_project_id', + name: 'merge_request[target_project_id]', + }, + branch: { + id: 'merge_request_target_branch', + name: 'merge_request[target_branch]', + }, + }, + i18n: { + projectHeaderText: __('Select target project'), + branchHeaderText: __('Select target branch'), + }, + toggleClass: { + project: 'js-target-project', + branch: 'js-target-branch', + }, + }, + render(h) { + return h(CompareApp); + }, + }); + } else { + initCompare(mrNewCompareNode); + } } else { const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue new file mode 100644 index 00000000000..355f17e970b --- /dev/null +++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue @@ -0,0 +1,80 @@ + + + diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index b078711ec5d..62680195984 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -1,13 +1,5 @@ diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index 81e2bb76900..bf48bcde7e5 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -1,7 +1,5 @@ diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6b662359a67..c0cb831149b 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -384,7 +384,15 @@ $comparison-empty-state-height: 62px; } .mr-compare-dropdown { + @include gl-w-full; + .gl-button-text { @include gl-w-full; } } + +.merge-request-branch-select { + .gl-button-text { + @include gl-font-monospace; + } +} diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb index 1e6c48bbef5..5e273e0fd4b 100644 --- a/app/models/ci/secure_file.rb +++ b/app/models/ci/secure_file.rb @@ -35,7 +35,7 @@ module Ci end def file_extension - File.extname(name).delete_prefix('.') + File.extname(name).delete_prefix('.').presence end def metadata_parsable? diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 48334023cf0..51238ab9bce 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -9,41 +9,44 @@ .card-new-merge-request %h2.gl-font-size-h2 = _('Source branch') - .clearfix - .merge-request-select.dropdown - = f.hidden_field :source_project_id - = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" } - .dropdown-menu.dropdown-menu-selectable.dropdown-source-project - = dropdown_title(_("Select source project")) - = dropdown_filter(_("Search projects")) - = dropdown_content do - = render 'projects/merge_requests/dropdowns/project', - projects: [@merge_request.source_project], - selected: f.object.source_project_id - .merge-request-select.dropdown - = f.hidden_field :source_branch - = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" } - .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown - = dropdown_title(_("Select source branch")) - = dropdown_filter(_("Search branches")) - = dropdown_content - = dropdown_loading - .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4 - .compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' } - = sprite_icon('branch', size: 16, css_class: 'gl-mr-3') - = _('Select a branch to compare') - = gl_loading_icon(css_class: 'js-source-loading gl-py-3') - %ul.list-unstyled.mr_source_commit + - if Feature.enabled?(:mr_compare_dropdowns, @project) + #js-source-project-dropdown{ data: { current_project: { value: f.object.source_project_id.to_s, text: f.object.source_project.full_path, refsUrl: refs_project_path(f.object.source_project) }.to_json, current_branch: { value: f.object.source_branch.presence, text: f.object.source_branch.presence }.to_json } } + - else + .clearfix + .merge-request-select.dropdown + = f.hidden_field :source_project_id + = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" } + .dropdown-menu.dropdown-menu-selectable.dropdown-source-project + = dropdown_title(_("Select source project")) + = dropdown_filter(_("Search projects")) + = dropdown_content do + = render 'projects/merge_requests/dropdowns/project', + projects: [@merge_request.source_project], + selected: f.object.source_project_id + .merge-request-select.dropdown + = f.hidden_field :source_branch + = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" } + .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown + = dropdown_title(_("Select source branch")) + = dropdown_filter(_("Search branches")) + = dropdown_content + = dropdown_loading + .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4 + .compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' } + = sprite_icon('branch', size: 16, css_class: 'gl-mr-3') + = _('Select a branch to compare') + = gl_loading_icon(css_class: 'js-source-loading gl-py-3') + %ul.list-unstyled.mr_source_commit .col-lg-6 .card-new-merge-request %h2.gl-font-size-h2 = _('Target branch') - .clearfix - .merge-request-select.dropdown - - if Feature.enabled?(:mr_compare_dropdowns, @project) - #js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path }.to_json } } - - else + - if Feature.enabled?(:mr_compare_dropdowns, @project) + #js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path, refsUrl: refs_project_path(f.object.target_project) }.to_json, current_branch: { value: f.object.target_branch.presence, text: f.object.target_branch.presence }.to_json } } + - else + .clearfix + .merge-request-select.dropdown - projects = target_projects(@project) = f.hidden_field :target_project_id = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" } @@ -54,21 +57,23 @@ = render 'projects/merge_requests/dropdowns/project', projects: projects, selected: f.object.target_project_id - .merge-request-select.dropdown - = f.hidden_field :target_branch - = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" } - .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown - = dropdown_title(_("Select target branch")) - = dropdown_filter(_("Search branches")) - = dropdown_content - = dropdown_loading - .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4 - .compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' } - = sprite_icon('branch', size: 16, css_class: 'gl-mr-3') - = _('Select a branch to compare') - = gl_loading_icon(css_class: 'js-target-loading gl-py-3') - %ul.list-unstyled.mr_target_commit + .merge-request-select.dropdown + = f.hidden_field :target_branch + = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" } + .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown + = dropdown_title(_("Select target branch")) + = dropdown_filter(_("Search branches")) + = dropdown_content + = dropdown_loading + .gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4 + .compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' } + = sprite_icon('branch', size: 16, css_class: 'gl-mr-3') + = _('Select a branch to compare') + = gl_loading_icon(css_class: 'js-target-loading gl-py-3') + %ul.list-unstyled.mr_target_commit - if @merge_request.errors.any? = form_errors(@merge_request) - = f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true + .row + .col-12 + = f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true diff --git a/config/initializers/check_forced_decomposition.rb b/config/initializers/check_forced_decomposition.rb new file mode 100644 index 00000000000..7da2f047ca1 --- /dev/null +++ b/config/initializers/check_forced_decomposition.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +return if Gitlab::Utils.to_boolean(ENV.fetch('GITLAB_ALLOW_SEPARATE_CI_DATABASE', false)) + +# GitLab.com is already decomposed +return if Gitlab.com? + +# It is relatively safe for development, and GDK defaults to decomposed already +return if Gitlab.dev_or_test_env? + +ci_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'ci') +main_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'main') + +return unless ci_config + +# If the ci `database` is the same as main `database`, it is likely the same +return if ci_config.database == main_config.database && + ci_config.host == main_config.host + +raise "Separate CI database is not ready for production use!\n\n" \ + "Did you mean to use `database: #{main_config.database}` for the `ci:` database connection?\n" \ + "Or, use `export GITLAB_ALLOW_SEPARATE_CI_DATABASE=1` to ignore this check." diff --git a/db/post_migrate/20230126133531_remove_not_null_constraints_for_tables_with_schema_differences_v3.rb b/db/post_migrate/20230126133531_remove_not_null_constraints_for_tables_with_schema_differences_v3.rb new file mode 100644 index 00000000000..04ed7a7ee12 --- /dev/null +++ b/db/post_migrate/20230126133531_remove_not_null_constraints_for_tables_with_schema_differences_v3.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemoveNotNullConstraintsForTablesWithSchemaDifferencesV3 < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + return unless Gitlab.com? + + change_column_null :integrations, :updated_at, true + change_column_null :integrations, :created_at, true + + change_column_null :project_settings, :show_default_award_emojis, true + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20230126133531 b/db/schema_migrations/20230126133531 new file mode 100644 index 00000000000..21a2394d94e --- /dev/null +++ b/db/schema_migrations/20230126133531 @@ -0,0 +1 @@ +259fa00544b9f63d512738dcbd4fb1ffcdbbfae58e15f7fbeb4fe34e5e7fe1f3 \ No newline at end of file diff --git a/doc/administration/postgresql/multiple_databases.md b/doc/administration/postgresql/multiple_databases.md index 25f148dbe84..836736fb73f 100644 --- a/doc/administration/postgresql/multiple_databases.md +++ b/doc/administration/postgresql/multiple_databases.md @@ -67,6 +67,9 @@ the other way around. 1. Save the `config/database.yml` file. +1. Update the service files to set the `GITLAB_ALLOW_SEPARATE_CI_DATABASE` + environment variable to `true`. + 1. Create the `gitlabhq_production_ci` database: ```shell @@ -100,6 +103,7 @@ the other way around. 1. Edit `/etc/gitlab/gitlab.rb` and add the following lines: ```ruby + gitlab_rails['env'] = { 'GITLAB_ALLOW_SEPARATE_CI_DATABASE' => 'true' } gitlab_rails['databases']['ci']['enable'] = true gitlab_rails['databases']['ci']['db_database'] = 'gitlabhq_production_ci' ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 41e4ffcf2c5..356492fb3ab 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1229,8 +1229,8 @@ curl --request POST --header "PRIVATE-TOKEN: " \ | Attribute | Type | Required | Description | |-------------------------------------------------------------|---------|------------------------|-------------| -| `name` | string | **{check-circle}** Yes (if path isn't provided) | The name of the new project. Equals path if not provided. | -| `path` | string | **{check-circle}** Yes (if name isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). Starting with GitLab 14.9, path must not start or end with a special character and must not contain consecutive special characters. | +| `name` | string | **{check-circle}** Yes (if `path` isn't provided) | The name of the new project. Equals path if not provided. | +| `path` | string | **{check-circle}** Yes (if `name` isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). Starting with GitLab 14.9, path must not start or end with a special character and must not contain consecutive special characters. | | `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. | | `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. | | `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` | diff --git a/doc/development/value_stream_analytics/value_stream_analytics_aggregated_backend.md b/doc/development/value_stream_analytics/value_stream_analytics_aggregated_backend.md index 3d1286a5809..4d2a751e6c6 100644 --- a/doc/development/value_stream_analytics/value_stream_analytics_aggregated_backend.md +++ b/doc/development/value_stream_analytics/value_stream_analytics_aggregated_backend.md @@ -31,7 +31,7 @@ for long-term growth. Our main database is not prepared for analytical workloads. Executing long-running queries can affect the reliability of the application. For large groups, the current implementation (old backend) is slow and, in some cases, doesn't even load due to the configured -statement timeout (15s). +statement timeout (15 s). The database queries in the old backend use the core domain models directly through `IssuableFinders` classes: ([MergeRequestsFinder](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/finders/merge_requests_finder.rb) and [IssuesFinder](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/finders/issues_finder.rb)). @@ -62,7 +62,7 @@ different development workflows within the `Test Group` (top-level namespace). The first value stream uses standard timestamp-based events for defining the stages. The second value stream uses label events. -Each value stream and stage item from the example will be persisted in the database. Notice that +Each value stream and stage item from the example is persisted in the database. Notice that the `Deployment` stage is identical for both value streams; that means that the underlying `stage_event_hash_id` is the same for both stages. The `stage_event_hash_id` reduces the amount of data the backend collects and plays a vital role in database partitioning. @@ -111,8 +111,8 @@ the service performs operations in batches and enforces strict application limit later. - Continue processing data from a specific point. -As of GitLab 14.7, the data loading is done manually. Once the feature is ready, the service will -be invoked periodically by the system via a cron job (this part is not implemented yet). +As of GitLab 14.7, the data loading is done manually. Once the feature is ready, the service is +invoked periodically by the system via a cron job (this part is not implemented yet). #### Record iteration @@ -193,7 +193,7 @@ aggregated data separated, we use two additional database tables: - `analytics_cycle_analytics_merge_request_stage_events` Both tables are hash partitioned by the `stage_event_hash_id`. Each table uses 32 partitions. It's -an arbitrary number and it could be changed. Important is to keep the partitions under 100GB in +an arbitrary number and it could be changed. Important is to keep the partitions under 100 GB in size (which gives the feature a lot of headroom). |Column|Description| diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 70a9358ff7f..8c6f469aca2 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -54,12 +54,12 @@ Memory requirements are dependent on the number of users and expected workload. The following is the recommended minimum Memory hardware guidance for a handful of example GitLab user base sizes. -- **4GB RAM** is the **required** minimum memory size and supports up to 500 users +- **4 GB RAM** is the **required** minimum memory size and supports up to 500 users - Our [Memory Team](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/application_performance/) is working to reduce the memory requirement. -- 8GB RAM supports up to 1000 users +- 8 GB RAM supports up to 1000 users - More users? Consult the [reference architectures page](../administration/reference_architectures/index.md) -In addition to the above, we generally recommend having at least 2GB of swap on your server, +In addition to the above, we generally recommend having at least 2 GB of swap on your server, even if you currently have enough available RAM. Having swap helps to reduce the chance of errors occurring if your available memory changes. We also recommend configuring the kernel's swappiness setting to a low value like `10` to make the most of your RAM while still having the swap @@ -154,7 +154,7 @@ of GitLab Support or other GitLab engineers. - GitLab may create new schemas as part of Rails database migrations. This happens when performing a GitLab upgrade. The GitLab database account requires access to do this. -- GitLab creates and modifies tables during the upgrade process, and also as part of normal +- GitLab creates and modifies tables during the upgrade process, and also as part of standard operations to manage partitioned tables. - You should not modify the GitLab schema (for example, adding triggers or modifying tables). @@ -254,10 +254,10 @@ if you must increase the number of Puma workers. ## Redis and Sidekiq Redis stores all user sessions and the background task queue. -The storage requirements for Redis are minimal, about 25kB per user. +The storage requirements for Redis are minimal, about 25 kB per user. Sidekiq processes the background jobs with a multi-threaded process. -This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks. -On a very active server (10,000 billable users) the Sidekiq process can use 1GB+ of memory. +This process starts with the entire Rails stack (200 MB+) but it can grow over time due to memory leaks. +On a very active server (10,000 billable users) the Sidekiq process can use 1 GB+ of memory. ## Prometheus and its exporters diff --git a/doc/integration/advanced_search/elasticsearch.md b/doc/integration/advanced_search/elasticsearch.md index 69d3a125ae6..14d62b01991 100644 --- a/doc/integration/advanced_search/elasticsearch.md +++ b/doc/integration/advanced_search/elasticsearch.md @@ -142,7 +142,7 @@ cd $indexer_path && sudo make install The `gitlab-elasticsearch-indexer` is installed to `/usr/local/bin`. You can change the installation path with the `PREFIX` environment variable. -Please remember to pass the `-E` flag to `sudo` if you do so. +Remember to pass the `-E` flag to `sudo` if you do so. Example: @@ -165,7 +165,7 @@ These errors may occur when indexing Git repository data. ## Enable Advanced Search -For GitLab instances with more than 50GB repository data you can follow the instructions for [how to index large instances efficiently](#how-to-index-large-instances-efficiently) below. +For GitLab instances with more than 50 GB repository data you can follow the instructions for [how to index large instances efficiently](#how-to-index-large-instances-efficiently) below. To enable Advanced Search, you must have administrator access to GitLab: @@ -217,7 +217,7 @@ The following Elasticsearch settings are available: | `Number of Elasticsearch shards` | Elasticsearch indices are split into multiple shards for performance reasons. In general, you should use at least 5 shards, and indices with tens of millions of documents need to have more shards ([see below](#guidance-on-choosing-optimal-cluster-configuration)). Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html). | | `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value increases total disk space required by the index. | | `Limit the number of namespaces and projects that can be indexed` | Enabling this allows you to select namespaces and projects to index. All other namespaces and projects use database search instead. If you enable this option but do not select any namespaces or projects, none are indexed. [Read more below](#limit-the-number-of-namespaces-and-projects-that-can-be-indexed).| -| `Using AWS OpenSearch Service with IAM credentials` | Sign your OpenSearch requests using [AWS IAM authorization](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html), [AWS EC2 Instance Profile Credentials](https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli), or [AWS ECS Tasks Credentials](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html). Please refer to [Identity and Access Management in Amazon OpenSearch Service](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html) for details of AWS hosted OpenSearch domain access policy configuration. | +| `Using AWS OpenSearch Service with IAM credentials` | Sign your OpenSearch requests using [AWS IAM authorization](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html), [AWS EC2 Instance Profile Credentials](https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli), or [AWS ECS Tasks Credentials](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html). Refer to [Identity and Access Management in Amazon OpenSearch Service](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html) for details of AWS hosted OpenSearch domain access policy configuration. | | `AWS Region` | The AWS region in which your OpenSearch Service is located. | | `AWS Access Key` | The AWS access key. | | `AWS Secret Access Key` | The AWS secret access key. | @@ -319,7 +319,7 @@ under **Elasticsearch indexing restrictions** more options become available. ![limit namespaces and projects options](img/limit_namespaces_projects_options.png) -You can select namespaces and projects to index exclusively. Note that if the namespace is a group, it includes +You can select namespaces and projects to index exclusively. If the namespace is a group, it includes any subgroups and projects belonging to those subgroups to be indexed as well. Advanced Search only provides cross-group code/commit search (global) if all name-spaces are indexed. In this particular scenario where only a subset of namespaces are indexed, a global search does not provide a code or commit scope. This is possible only in the scope of an indexed namespace. There is no way to code/commit search in multiple indexed namespaces (when only a subset of namespaces has been indexed). For example if two groups are indexed, there is no way to run a single code search on both. You can only run a code search on the first group and then on the second. @@ -343,7 +343,7 @@ You can improve the language support for Chinese and Japanese languages by utili To enable languages support: -1. Install the desired plugins, please refer to [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/plugins/7.9/installation.html) for plugins installation instructions. The plugins must be installed on every node in the cluster, and each node must be restarted after installation. For a list of plugins, see the table later in this section. +1. Install the desired plugins, refer to [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/plugins/7.9/installation.html) for plugins installation instructions. The plugins must be installed on every node in the cluster, and each node must be restarted after installation. For a list of plugins, see the table later in this section. 1. On the top bar, select **Main menu > Admin**. 1. On the left sidebar, select **Settings > Advanced Search**. 1. Locate **Custom analyzers: language support**. @@ -357,9 +357,9 @@ For guidance on what to install, see the following Elasticsearch language plugin | Parameter | Description | |-------------------------------------------------------|-------------| | `Enable Chinese (smartcn) custom analyzer: Indexing` | Enables or disables Chinese language support using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) custom analyzer for newly created indices.| -| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.| +| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.| | `Enable Japanese (kuromoji) custom analyzer: Indexing` | Enables or disables Japanese language support using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) custom analyzer for newly created indices.| -| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.| +| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.| ## Disable Advanced Search @@ -393,7 +393,7 @@ and Elasticsearch index alias feature to perform the operation. We set up an ind `primary` index which is used by GitLab for reads/writes. When reindexing process starts, we temporarily pause the writes to the `primary` index. Then, we create another index and invoke the Reindex API which migrates the index data onto the new index. After the reindexing job is complete, we switch to the new index by connecting the -index alias to it which becomes the new `primary` index. At the end, we resume the writes and normal operation resumes. +index alias to it which becomes the new `primary` index. At the end, we resume the writes and typical operation resumes. ### Trigger the reindex via the Advanced Search administration @@ -521,7 +521,7 @@ This should return something similar to: } ``` -In order to debug issues with the migrations you can check the [`elasticsearch.log` file](../../administration/logs/index.md#elasticsearchlog). +To debug issues with the migrations you can check the [`elasticsearch.log` file](../../administration/logs/index.md#elasticsearchlog). ### Retry a halted migration @@ -565,7 +565,7 @@ The following are some available Rake tasks: | [`sudo gitlab-rake gitlab:elastic:resume_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Resumes Elasticsearch indexing. | | [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects, and queues Sidekiq jobs to index them in the background. It can only be used after the index is created. | | [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. | -| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. Note that this command results in a complete wipe of the index, and it should be used with caution. | +| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. This command results in a complete wipe of the index, and it should be used with caution. | | [`sudo gitlab-rake gitlab:elastic:create_empty_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Generates empty indices (the default index and a separate issues index) and assigns an alias for each on the Elasticsearch side only if it doesn't already exist. | | [`sudo gitlab-rake gitlab:elastic:delete_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Removes the GitLab indices and aliases (if they exist) on the Elasticsearch instance. | | [`sudo gitlab-rake gitlab:elastic:recreate_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:delete_index` and `gitlab:elastic:create_empty_index`. | @@ -632,7 +632,7 @@ For basic guidance on choosing a cluster configuration you may refer to [Elastic - You can use the [GitLab Performance Tool](https://gitlab.com/gitlab-org/quality/performance) to benchmark search performance with different search cluster sizes and configurations. - `Heap size` should be set to no more than 50% of your physical RAM. Additionally, it shouldn't be set to more than the threshold for zero-based compressed oops. The exact threshold varies, but 26 GB is safe on most systems, but can also be as large as 30 GB on some systems. See [Heap size settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/important-settings.html#heap-size-settings) and [Setting JVM options](https://www.elastic.co/guide/en/elasticsearch/reference/current/jvm-options.html) for more details. - Number of CPUs (CPU cores) per node usually corresponds to the `Number of Elasticsearch shards` setting described below. -- A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This generally helps the cluster stay in good health. +- A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30 GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This generally helps the cluster stay in good health. - Number of Elasticsearch shards: - Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB. - Another consideration is the number of documents. To determine the number of shards to use, sum the numbers in the **Main menu > Admin > Dashboard > Statistics** pane (the number of documents to be indexed), divide by 5 million, and add 5. For example: @@ -685,7 +685,7 @@ Make sure to prepare for this task by having a - You can temporarily disable [`refresh`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html), the operation responsible for making changes to an index available to search. - - You can set the number of replicas to 0. This setting controls the number of copies each primary shard of an index will have. Thus, having 0 replicas effectively disables the replication of shards across nodes, which should increase the indexing performance. This is an important trade-off in terms of reliability and query performance. It is important to remember to set the replicas to a considered value after the initial indexing is complete. + - You can set the number of replicas to 0. This setting controls the number of copies each primary shard of an index has. Thus, having 0 replicas effectively disables the replication of shards across nodes, which should increase the indexing performance. This is an important trade-off in terms of reliability and query performance. It is important to remember to set the replicas to a considered value after the initial indexing is complete. In our experience, you can expect a 20% decrease in indexing time. After completing indexing in a later step, you can return `refresh` and `number_of_replicas` to their desired settings. @@ -737,17 +737,17 @@ Make sure to prepare for this task by having a ``` Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional. - The above example will index all projects from ID `1001` up to (and including) ID `2000`. + The above example indexes all projects from ID `1001` up to (and including) ID `2000`. NOTE: Sometimes the project indexing jobs queued by `gitlab:elastic:index_projects` can get interrupted. This may happen for many reasons, but it's always safe - to run the indexing task again. It will skip repositories that have + to run the indexing task again. It skips repositories that have already been indexed. As the indexer stores the last commit SHA of every indexed repository in the database, you can run the indexer with the special parameter `UPDATE_INDEX` and - it will check every project repository again to make sure that every commit in + it checks every project repository again to make sure that every commit in a repository is indexed, which can be useful in case if your index is outdated: ```shell @@ -817,11 +817,11 @@ Make sure to prepare for this task by having a Whenever a change or deletion is made to an indexed GitLab object (a merge request description is changed, a file is deleted from the default branch in a repository, a project is deleted, etc), a document in the index is deleted. However, since these are "soft" deletes, the overall number of "deleted documents", and therefore wasted space, increases. Elasticsearch does intelligent merging of segments to remove these deleted documents. However, depending on the amount and type of activity in your GitLab installation, it's possible to see as much as 50% wasted space in the index. -In general, we recommend letting Elasticsearch merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene's defaults as-is and not fret too much about when deletes are reclaimed."_ +In general, we recommend letting Elasticsearch merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene defaults as-is and not fret too much about when deletes are reclaimed."_ However, some larger installations may wish to tune the merge policy settings: -- Consider reducing the `index.merge.policy.max_merged_segment` size from the default 5 GB to maybe 2 GB or 3 GB. Merging only happens when a segment has at least 50% deletions. Smaller segment sizes will allow merging to happen more frequently. +- Consider reducing the `index.merge.policy.max_merged_segment` size from the default 5 GB to maybe 2 GB or 3 GB. Merging only happens when a segment has at least 50% deletions. Smaller segment sizes allows merging to happen more frequently. ```shell curl --request PUT localhost:9200/gitlab-production/_settings ---header 'Content-Type: application/json' \ diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md index ad320632628..632581e66d3 100644 --- a/doc/subscriptions/gitlab_com/index.md +++ b/doc/subscriptions/gitlab_com/index.md @@ -296,7 +296,7 @@ To renew your subscription: 1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and beneath your existing subscription, select **Renew**. The **Renew** button remains disabled (grayed-out) until 15 days before a subscription expires. -You can hover your mouse on the **Renew** button to see the date when it will become active. +You can hover your mouse on the **Renew** button to see the date when it becomes active. 1. Review your renewal details and complete the payment process. 1. Select **Confirm purchase**. @@ -362,7 +362,7 @@ for your personal or group namespace. CI/CD minutes are a **one-time purchase**, ## Add-on subscription for additional Storage and Transfer NOTE: -Free namespaces are subject to a 5GB storage and 10GB transfer [soft limit](https://about.gitlab.com/pricing/). Once all storage is available to view in the usage quota workflow, GitLab will automatically enforce the namespace storage limit and the project limit will be removed. This change will be announced separately. The storage and transfer add-on can be purchased to increase the limits. +Free namespaces are subject to a 5 GB storage and 10 GB transfer [soft limit](https://about.gitlab.com/pricing/). Once all storage is available to view in the usage quota workflow, GitLab will automatically enforce the namespace storage limit and the project limit is removed. This change is announced separately. The storage and transfer add-on can be purchased to increase the limits. Projects have a free storage quota of 10 GB. To exceed this quota you must first [purchase one or more storage subscription units](#purchase-more-storage-and-transfer). Each unit provides 10 GB of additional @@ -449,7 +449,7 @@ and for communicating directly with the relevant GitLab team members. If your credit card is declined when purchasing a GitLab subscription, possible reasons include: -- The credit card details provided are incorrect. The most common cause for this is an incomplete or dummy address. +- The credit card details provided are incorrect. The most common cause for this is an incomplete or fake address. - The credit card account has insufficient funds. - You are using a virtual credit card and it has insufficient funds, or has expired. - The transaction exceeds the credit limit. diff --git a/lib/api/entities/ci/secure_file.rb b/lib/api/entities/ci/secure_file.rb index a234ada6f82..bf8d2776db1 100644 --- a/lib/api/entities/ci/secure_file.rb +++ b/lib/api/entities/ci/secure_file.rb @@ -12,6 +12,7 @@ documentation: { type: 'string', example: '16630b189ab34b2e3504f4758e1054d2e478d expose :created_at, documentation: { type: 'dateTime', example: '2022-02-22T22:22:22.222Z' } expose :expires_at, documentation: { type: 'dateTime', example: '2022-09-21T14:56:00.000Z' } expose :metadata, documentation: { type: 'Hash', example: { "id" => "75949910542696343243264405377658443914" } } + expose :file_extension, documentation: { type: 'string', example: 'jks' } end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 99acbce98f7..2c39f85ed7a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16067,6 +16067,9 @@ msgstr "" msgid "Error fetching burnup chart data" msgstr "" +msgid "Error fetching data. Please try again." +msgstr "" + msgid "Error fetching diverging counts for branches. Please try again." msgstr "" @@ -16085,9 +16088,6 @@ msgstr "" msgid "Error fetching refs" msgstr "" -msgid "Error fetching target projects. Please try again." -msgstr "" - msgid "Error fetching the dependency list. Please check your network connection and try again." msgstr "" @@ -37556,15 +37556,45 @@ msgstr "" msgid "Secure token that identifies an external storage request." msgstr "" +msgid "SecureFiles|%{name} Metadata" +msgstr "" + +msgid "SecureFiles|App" +msgstr "" + +msgid "SecureFiles|Certificates" +msgstr "" + msgid "SecureFiles|Delete %{name}?" msgstr "" msgid "SecureFiles|Delete secure file" msgstr "" +msgid "SecureFiles|Expires at" +msgstr "" + +msgid "SecureFiles|Issuer" +msgstr "" + +msgid "SecureFiles|Name" +msgstr "" + +msgid "SecureFiles|Platforms" +msgstr "" + msgid "SecureFiles|Secure File %{name} will be permanently deleted. Are you sure?" msgstr "" +msgid "SecureFiles|Serial" +msgstr "" + +msgid "SecureFiles|Team" +msgstr "" + +msgid "SecureFiles|UUID" +msgstr "" + msgid "Security" msgstr "" @@ -39720,7 +39750,7 @@ msgstr "" msgid "Slack|%{asterisk}Slash commands%{asterisk}" msgstr "" -msgid "Slack|%{emoji}Connected to GitLab account %{account}" +msgid "Slack|%{emoji}Connected to GitLab account %{account}." msgstr "" msgid "Slack|%{emoji}Welcome to GitLab for Slack!" @@ -39732,10 +39762,10 @@ msgstr "" msgid "Slack|Control GitLab from Slack with %{startMarkup}slash commands%{endMarkup}. For a list of available commands, enter %{command}." msgstr "" -msgid "Slack|GitLab for Slack now supports channel-based notifications.Let your team know when new issues are created or new CI/CD jobs are run.%{startMarkup}Learn more%{endMarkup}." +msgid "Slack|GitLab for Slack now supports channel-based notifications. Let your team know when new issues are created or new CI/CD jobs are run.%{startMarkup}Learn more%{endMarkup}." msgstr "" -msgid "Slack|To start using notifications, %{startMarkup}enable the Slack integration%{endMarkup} in your project settings." +msgid "Slack|To start using notifications, %{startMarkup}enable the GitLab for Slack app integration%{endMarkup} in your project settings." msgstr "" msgid "Slack|To start using slash commands, connect your GitLab account." @@ -46427,6 +46457,9 @@ msgstr "" msgid "VersionCheck|Your GitLab Version" msgstr "" +msgid "View File Metadata" +msgstr "" + msgid "View Stage: %{title}" msgstr "" @@ -47873,9 +47906,6 @@ msgstr "" msgid "WorkItem|Closed" msgstr "" -msgid "WorkItem|Collapse tasks" -msgstr "" - msgid "WorkItem|Create %{workItemType}" msgstr "" @@ -47900,9 +47930,6 @@ msgstr "" msgid "WorkItem|Existing task" msgstr "" -msgid "WorkItem|Expand tasks" -msgstr "" - msgid "WorkItem|Health status" msgstr "" diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index 1717069a259..6be0eee21fc 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -73,19 +73,9 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi expect(page).to have_content('You must select source and target branch') - first('.js-source-project').click - first('.dropdown-source-project a', text: forked_project.full_path) - - first('.js-target-project').click - first('.dropdown-target-project li', text: project.full_path) - - first('.js-source-branch').click - - wait_for_requests - - source_branch = 'fix' - - first('.js-source-branch-dropdown .dropdown-content a', text: source_branch).click + select_project('.js-source-project', forked_project) + select_project('.js-target-project', project) + select_branch('.js-source-branch', 'fix') click_button('Compare branches and continue') @@ -107,7 +97,7 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi click_button('Create merge request') - expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master") + expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:fix into master") end end end @@ -153,12 +143,27 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi private def compare_source_and_target(source_branch, target_branch) - find('.js-source-branch').click - click_link(source_branch) - - find('.js-target-branch').click - click_link(target_branch) + select_branch('.js-source-branch', source_branch) + select_branch('.js-target-branch', target_branch) click_button('Compare branches') end + + def select_project(selector, project) + first(selector).click + + wait_for_requests + + find('.gl-listbox-search-input').set(project.full_path) + first('.gl-dropdown-item', text: project.full_path).click + end + + def select_branch(selector, branch) + first(selector).click + + wait_for_requests + + find('.gl-listbox-search-input').set(branch) + first('.gl-dropdown-item', text: branch).click + end end diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb index 523027582b3..cde1b71a7ee 100644 --- a/spec/features/merge_request/user_creates_mr_spec.rb +++ b/spec/features/merge_request/user_creates_mr_spec.rb @@ -59,9 +59,9 @@ RSpec.describe 'Merge request > User creates MR', feature_category: :code_review it 'filters source project' do find('.js-source-project').click - find('.dropdown-source-project input').set('source') + find('.gl-listbox-search-input').set('source') - expect(find('.dropdown-source-project .dropdown-content')).not_to have_content(source_project.name) + expect(first('.merge-request-select .dropdown-menu')).not_to have_content(source_project.name) end end end diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index b7784de12b9..f79821e9420 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -8,8 +8,8 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ def select_source_branch(branch_name) find('.js-source-branch', match: :first).click - find('.js-source-branch-dropdown .dropdown-input-field').native.send_keys branch_name - find('.js-source-branch-dropdown .dropdown-content a', text: branch_name, match: :first).click + find('.gl-listbox-search-input').native.send_keys branch_name + find('.gl-dropdown-item', text: branch_name, match: :first).click end before do @@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ expect(page).to have_content('Target branch') first('.js-source-branch').click - find('.js-source-branch-dropdown .dropdown-content a', match: :first).click + find('.gl-dropdown-item', match: :first).click expect(page).to have_content "b83d6e3" end @@ -43,7 +43,8 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ expect(page).to have_content('Target branch') first('.js-target-branch').click - find('.js-target-branch-dropdown .dropdown-content a', text: 'v1.1.0', match: :first).click + find('.gl-listbox-search-input').native.send_keys 'v1.1.0' + find('.gl-dropdown-item', text: 'v1.1.0', match: :first).click expect(page).to have_content "b83d6e3" end @@ -68,27 +69,6 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\'' end - it 'allows filtering multiple dropdowns' do - visit project_new_merge_request_path(project) - - first('.js-source-branch').click - - page.within '.js-source-branch-dropdown' do - input = find('.dropdown-input-field') - input.click - input.send_keys('orphaned-branch') - - expect(page).to have_css('.dropdown-content li', count: 1) - end - - first('.js-target-branch').click - - find('.js-target-branch-dropdown .dropdown-content li', match: :first) - target_items = all('.js-target-branch-dropdown .dropdown-content li') - - expect(target_items.count).to be > 1 - end - context 'when target project cannot be viewed by the current user' do it 'does not leak the private project name & namespace' do private_project = create(:project, :private, :repository) diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb index 4403ca60d11..f41fb86d13c 100644 --- a/spec/features/work_items/work_item_children_spec.rb +++ b/spec/features/work_items/work_item_children_spec.rb @@ -31,15 +31,15 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do it 'toggles widget body', :aggregate_failures do page.within('[data-testid="work-item-links"]') do - expect(page).to have_selector('[data-testid="links-body"]') + expect(page).to have_selector('[data-testid="widget-body"]') - click_button 'Collapse tasks' + click_button 'Collapse' - expect(page).not_to have_selector('[data-testid="links-body"]') + expect(page).not_to have_selector('[data-testid="widget-body"]') - click_button 'Expand tasks' + click_button 'Expand' - expect(page).to have_selector('[data-testid="links-body"]') + expect(page).to have_selector('[data-testid="widget-body"]') end end diff --git a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap new file mode 100644 index 00000000000..b2084e3a7de --- /dev/null +++ b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap @@ -0,0 +1,386 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the snapshot 1`] = ` +
+
+ myfile.cer Metadata +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + Apple Distribution: Team Name (ABC123XYZ) + +
+ + Serial + + + + 33669367788748363528491290218354043267 + +
+ + Team + + + + Team Name (ABC123XYZ) + +
+ + Issuer + + + + Apple Worldwide Developer Relations Certification Authority - G3 + +
+ + Expires at + + + + April 26, 2022 at 7:20:40 PM GMT + +
+
+
+`; + +exports[`Secure File Metadata Modal when a .mobileprovision file is supplied matches the mobileprovision snapshot 1`] = ` +
+
+ sample.mobileprovision Metadata +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + UUID + + + + 6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf + +
+ + Platforms + + + + iOS + +
+ + Team + + + + Team Name (ABC123XYZ) + +
+ + App + + + + iOS Demo - match Development com.gitlab.ios-demo + +
+ + Certificates + + + + 33669367788748363528491290218354043267 + +
+ + Expires at + + + + August 1, 2023 at 11:15:13 PM GMT + +
+
+
+`; diff --git a/spec/frontend/ci_secure_files/components/metadata/button_spec.js b/spec/frontend/ci_secure_files/components/metadata/button_spec.js new file mode 100644 index 00000000000..4ac5b3325d4 --- /dev/null +++ b/spec/frontend/ci_secure_files/components/metadata/button_spec.js @@ -0,0 +1,49 @@ +import { GlButton } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Button from '~/ci_secure_files/components/metadata/button.vue'; +import { secureFiles } from '../../mock_data'; + +const secureFileWithoutMetadata = secureFiles[0]; +const secureFileWithMetadata = secureFiles[2]; +const modalId = 'metadataModalId'; + +describe('Secure File Metadata Button', () => { + let wrapper; + + const findButton = () => wrapper.findComponent(GlButton); + + afterEach(() => { + wrapper.destroy(); + }); + + const createWrapper = (secureFile = {}, admin = false) => { + wrapper = mount(Button, { + propsData: { + admin, + modalId, + secureFile, + }, + }); + }; + + describe('metadata button visibility', () => { + it.each` + visibility | admin | fileName + ${true} | ${true} | ${secureFileWithMetadata} + ${false} | ${false} | ${secureFileWithMetadata} + ${false} | ${false} | ${secureFileWithoutMetadata} + ${false} | ${false} | ${secureFileWithoutMetadata} + `( + 'button visibility is $visibility when admin equals $admin and $fileName.name is suppled', + ({ visibility, admin, fileName }) => { + createWrapper(fileName, admin); + expect(findButton().exists()).toBe(visibility); + + if (visibility) { + expect(findButton().isVisible()).toBe(true); + expect(findButton().attributes('aria-label')).toBe('View File Metadata'); + } + }, + ); + }); +}); diff --git a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js new file mode 100644 index 00000000000..230507d32d7 --- /dev/null +++ b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js @@ -0,0 +1,78 @@ +import { GlModal } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; + +import Modal from '~/ci_secure_files/components/metadata/modal.vue'; + +import { secureFiles } from '../../mock_data'; + +const cerFile = secureFiles[2]; +const mobileprovisionFile = secureFiles[3]; +const modalId = 'metadataModalId'; + +describe('Secure File Metadata Modal', () => { + let wrapper; + let trackingSpy; + + const createWrapper = (secureFile = {}) => { + wrapper = mount(Modal, { + stubs: { + GlModal: stubComponent(GlModal, { + template: RENDER_ALL_SLOTS_TEMPLATE, + }), + }, + propsData: { + modalId, + name: secureFile.name, + metadata: secureFile.metadata, + fileExtension: secureFile.file_extension, + }, + }); + }; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + wrapper.destroy(); + }); + + describe('when a .cer file is supplied', () => { + it('matches cer the snapshot', () => { + createWrapper(cerFile); + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('when a .mobileprovision file is supplied', () => { + it('matches the mobileprovision snapshot', () => { + createWrapper(mobileprovisionFile); + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('event tracking', () => { + it('sends cer tracking information when the modal is loaded', () => { + createWrapper(cerFile); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {}); + expect(trackingSpy).not.toHaveBeenCalledWith( + undefined, + 'load_secure_file_metadata_mobileprovision', + {}, + ); + }); + + it('sends mobileprovision tracking information when the modal is loaded', () => { + createWrapper(mobileprovisionFile); + expect(trackingSpy).toHaveBeenCalledWith( + undefined, + 'load_secure_file_metadata_mobileprovision', + {}, + ); + expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {}); + }); + }); +}); diff --git a/spec/frontend/ci_secure_files/mock_data.js b/spec/frontend/ci_secure_files/mock_data.js index 5a9e16d1ad6..f532b468fb9 100644 --- a/spec/frontend/ci_secure_files/mock_data.js +++ b/spec/frontend/ci_secure_files/mock_data.js @@ -4,15 +4,72 @@ export const secureFiles = [ name: 'myfile.jks', checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac', checksum_algorithm: 'sha256', - permissions: 'read_only', created_at: '2022-02-22T22:22:22.222Z', + file_extension: 'jks', + metadata: null, }, { id: 2, name: 'myotherfile.jks', checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2', checksum_algorithm: 'sha256', - permissions: 'execute', created_at: '2022-02-22T22:22:22.222Z', + file_extension: 'jks', + metadata: null, + }, + { + id: 3, + name: 'myfile.cer', + checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2', + checksum_algorithm: 'sha256', + created_at: '2022-02-22T22:22:22.222Z', + file_extension: 'cer', + expires_at: '2022-04-26T19:20:40.000Z', + metadata: { + id: '33669367788748363528491290218354043267', + issuer: { + C: 'US', + O: 'Apple Inc.', + CN: 'Apple Worldwide Developer Relations Certification Authority', + OU: 'G3', + }, + subject: { + C: 'US', + O: 'Team Name', + CN: 'Apple Distribution: Team Name (ABC123XYZ)', + OU: 'ABC123XYZ', + UID: 'ABC123XYZ', + }, + expires_at: '2022-04-26T19:20:40.000Z', + }, + }, + { + id: 4, + name: 'sample.mobileprovision', + checksum: '9e194bbde00d57c64b6640ed2c9e166d76b4c79d9dbd49770f95be56678f2a62', + checksum_algorithm: 'sha256', + created_at: '2022-11-15T19:29:57.577Z', + expires_at: '2023-08-01T23:15:13.000Z', + metadata: { + id: '6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf', + app_id: 'match Development com.gitlab.ios-demo', + devices: ['00008101-001454860C10001E'], + team_id: ['ABC123XYZ'], + app_name: 'iOS Demo', + platforms: ['iOS'], + team_name: 'Team Name', + expires_at: '2023-08-01T18:15:13.000-05:00', + entitlements: { + 'get-task-allow': true, + 'application-identifier': 'N7SYAN8PX8.com.gitlab.ios-demo', + 'keychain-access-groups': ['ABC123XYZ.*', 'com.apple.token'], + 'com.apple.developer.game-center': true, + 'com.apple.developer.team-identifier': 'ABC123XYZ', + }, + app_id_prefix: ['ABC123XYZ'], + xcode_managed: false, + certificate_ids: ['33669367788748363528491290218354043267'], + }, + file_extension: 'mobileprovision', }, ]; diff --git a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js b/spec/frontend/merge_requests/components/compare_dropdown_spec.js similarity index 69% rename from spec/frontend/merge_requests/components/target_project_dropdown_spec.js rename to spec/frontend/merge_requests/components/compare_dropdown_spec.js index 3fddbe7ae21..fecb82aaa5e 100644 --- a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js +++ b/spec/frontend/merge_requests/components/compare_dropdown_spec.js @@ -3,23 +3,28 @@ import { GlCollapsibleListbox } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; -import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue'; +import CompareDropdown from '~/merge_requests/components/compare_dropdown.vue'; let wrapper; let mock; -function factory() { - wrapper = mount(TargetProjectDropdown, { - provide: { - targetProjectsPath: '/gitlab-org/gitlab/target_projects', - currentProject: { value: 1, text: 'gitlab-org/gitlab' }, +function factory(propsData = {}) { + wrapper = mount(CompareDropdown, { + propsData: { + endpoint: '/gitlab-org/gitlab/target_projects', + default: { value: 1, text: 'gitlab-org/gitlab' }, + dropdownHeader: 'Select', + inputId: 'input_id', + inputName: 'input_name', + isProject: true, + ...propsData, }, }); } const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); -describe('Merge requests target project dropdown component', () => { +describe('Merge requests compare dropdown component', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/gitlab-org/gitlab/target_projects').reply(200, [ @@ -77,4 +82,22 @@ describe('Merge requests target project dropdown component', () => { expect(mock.history.get[1].params).toEqual({ search: 'test' }); }); + + it('renders static data', async () => { + factory({ + endpoint: undefined, + staticData: [ + { + value: '10', + text: 'GitLab Org', + }, + ], + }); + + wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click'); + + await waitForPromises(); + + expect(wrapper.findAll('li').length).toBe(1); + }); }); diff --git a/spec/frontend/work_items/components/widget_wrapper_spec.js b/spec/frontend/work_items/components/widget_wrapper_spec.js new file mode 100644 index 00000000000..a87233300fc --- /dev/null +++ b/spec/frontend/work_items/components/widget_wrapper_spec.js @@ -0,0 +1,46 @@ +import { nextTick } from 'vue'; +import { GlAlert, GlButton } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; + +describe('WidgetWrapper component', () => { + let wrapper; + + const createComponent = ({ error } = {}) => { + wrapper = shallowMountExtended(WidgetWrapper, { propsData: { error } }); + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findToggleButton = () => wrapper.findComponent(GlButton); + const findWidgetBody = () => wrapper.findByTestId('widget-body'); + + it('is expanded by default', () => { + createComponent(); + + expect(findToggleButton().props('icon')).toBe('chevron-lg-up'); + expect(findWidgetBody().exists()).toBe(true); + }); + + it('collapses on click toggle button', async () => { + createComponent(); + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findToggleButton().props('icon')).toBe('chevron-lg-down'); + expect(findWidgetBody().exists()).toBe(false); + }); + + it('shows alert when list loading fails', () => { + const error = 'Some error'; + createComponent({ error }); + + expect(findAlert().text()).toBe(error); + }); + + it('emits event when dismissing the alert', () => { + createComponent({ error: 'error' }); + findAlert().vm.$emit('dismiss'); + + expect(wrapper.emitted('dismissAlert')).toEqual([[]]); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index a61de78c623..aedf6bf2e2c 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -1,5 +1,4 @@ import Vue, { nextTick } from 'vue'; -import { GlAlert } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -8,6 +7,7 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { stubComponent } from 'helpers/stub_component'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; +import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; @@ -127,12 +127,12 @@ describe('WorkItemLinks', () => { }, }); + wrapper.vm.$refs.wrapper.show = jest.fn(); + await waitForPromises(); }; - const findAlert = () => wrapper.findComponent(GlAlert); - const findToggleButton = () => wrapper.findByTestId('toggle-links'); - const findLinksBody = () => wrapper.findByTestId('links-body'); + const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper); const findEmptyState = () => wrapper.findByTestId('links-empty'); const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); @@ -142,31 +142,14 @@ describe('WorkItemLinks', () => { const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); const findChildrenCount = () => wrapper.findByTestId('children-count'); - beforeEach(async () => { - await createComponent(); - }); - afterEach(() => { - wrapper.destroy(); mockApollo = null; setWindowLocation(''); }); - it('is expanded by default', () => { - expect(findToggleButton().props('icon')).toBe('chevron-lg-up'); - expect(findLinksBody().exists()).toBe(true); - }); - - it('collapses on click toggle button', async () => { - findToggleButton().vm.$emit('click'); - await nextTick(); - - expect(findToggleButton().props('icon')).toBe('chevron-lg-down'); - expect(findLinksBody().exists()).toBe(false); - }); - describe('add link form', () => { it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => { + await createComponent(); findToggleFormDropdown().vm.$emit('click'); findToggleAddFormButton().vm.$emit('click'); await nextTick(); @@ -181,6 +164,7 @@ describe('WorkItemLinks', () => { }); it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => { + await createComponent(); findToggleFormDropdown().vm.$emit('click'); findToggleCreateFormButton().vm.$emit('click'); await nextTick(); @@ -207,8 +191,8 @@ describe('WorkItemLinks', () => { }); }); - it('renders all hierarchy widget children', () => { - expect(findLinksBody().exists()).toBe(true); + it('renders all hierarchy widget children', async () => { + await createComponent(); expect(findWorkItemLinkChildItems()).toHaveLength(4); }); @@ -219,15 +203,13 @@ describe('WorkItemLinks', () => { fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)), }); - await nextTick(); - - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(errorMessage); + expect(findWidgetWrapper().props('error')).toBe(errorMessage); }); - it('displays number if children', () => { - expect(findChildrenCount().exists()).toBe(true); + it('displays number of children', async () => { + await createComponent(); + expect(findChildrenCount().exists()).toBe(true); expect(findChildrenCount().text()).toContain('4'); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 156f06a0d5e..0236fe2e60d 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -23,8 +23,6 @@ describe('WorkItemTree', () => { let getWorkItemQueryHandler; let wrapper; - const findToggleButton = () => wrapper.findByTestId('toggle-tree'); - const findTreeBody = () => wrapper.findByTestId('tree-body'); const findEmptyState = () => wrapper.findByTestId('tree-empty'); const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton); const findForm = () => wrapper.findComponent(WorkItemLinksForm); @@ -64,36 +62,25 @@ describe('WorkItemTree', () => { projectPath: 'test/project', }, }); + + wrapper.vm.$refs.wrapper.show = jest.fn(); }; - beforeEach(() => { + it('displays Add button', () => { createComponent(); - }); - afterEach(() => { - wrapper.destroy(); - }); - - it('is expanded by default and displays Add button', () => { - expect(findToggleButton().props('icon')).toBe('chevron-lg-up'); - expect(findTreeBody().exists()).toBe(true); expect(findToggleFormSplitButton().exists()).toBe(true); }); - it('collapses on click toggle button', async () => { - findToggleButton().vm.$emit('click'); - await nextTick(); - - expect(findToggleButton().props('icon')).toBe('chevron-lg-down'); - expect(findTreeBody().exists()).toBe(false); - }); - it('displays empty state if there are no children', () => { createComponent({ children: [] }); + expect(findEmptyState().exists()).toBe(true); }); it('renders all hierarchy widget children', () => { + createComponent(); + const workItemLinkChildren = findWorkItemLinkChildItems(); expect(workItemLinkChildren).toHaveLength(4); expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe( @@ -102,6 +89,8 @@ describe('WorkItemTree', () => { }); it('does not display form by default', () => { + createComponent(); + expect(findForm().exists()).toBe(false); }); @@ -114,6 +103,8 @@ describe('WorkItemTree', () => { `( 'when selecting $option from split button, renders the form passing $formType and $childType', async ({ event, formType, childType }) => { + createComponent(); + findToggleFormSplitButton().vm.$emit(event); await nextTick(); @@ -128,13 +119,16 @@ describe('WorkItemTree', () => { ); it('remove event on child triggers `removeChild` event', () => { + createComponent(); const firstChild = findWorkItemLinkChildItems().at(0); + firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2'); expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]); }); it('emits `show-modal` on `click` event', () => { + createComponent(); const firstChild = findWorkItemLinkChildItems().at(0); const event = { childItem: 'gid://gitlab/WorkItem/2', diff --git a/spec/initializers/check_forced_decomposition_spec.rb b/spec/initializers/check_forced_decomposition_spec.rb new file mode 100644 index 00000000000..011a093b711 --- /dev/null +++ b/spec/initializers/check_forced_decomposition_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'check_forced_decomposition initializer', feature_category: :pods do + subject(:check_forced_decomposition) do + load Rails.root.join('config/initializers/check_forced_decomposition.rb') + end + + context 'for production env' do + before do + allow(Gitlab).to receive(:dev_or_test_env?).and_return(false) + end + + context 'for single database' do + before do + skip_if_multiple_databases_are_setup + end + + it { expect { check_forced_decomposition }.not_to raise_error } + end + + context 'for multiple database' do + before do + skip_if_multiple_databases_not_setup + end + + let(:main_database_config) do + Rails.application.config.load_database_yaml + .dig('test', 'main') + .slice('adapter', 'encoding', 'database', 'username', 'password', 'host') + .symbolize_keys + end + + let(:additional_database_config) do + # Use built-in postgres database + main_database_config.merge(database: 'postgres') + end + + around do |example| + with_reestablished_active_record_base(reconnect: true) do + with_db_configs(test: test_config) do + example.run + end + end + end + + context 'when ci and main share the same database' do + let(:test_config) do + { + main: main_database_config, + ci: additional_database_config.merge(database: main_database_config[:database]) + } + end + + it { expect { check_forced_decomposition }.not_to raise_error } + + context 'when host is not present' do + let(:test_config) do + { + main: main_database_config.except(:host), + ci: additional_database_config.merge(database: main_database_config[:database]).except(:host) + } + end + + it { expect { check_forced_decomposition }.not_to raise_error } + end + end + + context 'when ci and main share the same database but different host' do + let(:test_config) do + { + main: main_database_config, + ci: additional_database_config.merge( + database: main_database_config[:database], + host: 'otherhost.localhost' + ) + } + end + + it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) } + end + + context 'when ci and main are different databases' do + let(:test_config) do + { + main: main_database_config, + ci: additional_database_config + } + end + + it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) } + + context 'for GitLab.com' do + before do + allow(::Gitlab).to receive(:com?).and_return(true) + end + + it { expect { check_forced_decomposition }.not_to raise_error } + end + + context 'when env var GITLAB_ALLOW_SEPARATE_CI_DATABASE is true' do + before do + stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', 'true') + end + + it { expect { check_forced_decomposition }.not_to raise_error } + end + + context 'when env var GITLAB_ALLOW_SEPARATE_CI_DATABASE is false' do + before do + stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', 'false') + end + + it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) } + end + end + end + end +end diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb index 87077fe2db1..38ae908fb00 100644 --- a/spec/models/ci/secure_file_spec.rb +++ b/spec/models/ci/secure_file_spec.rb @@ -101,6 +101,11 @@ RSpec.describe Ci::SecureFile do file = build(:ci_secure_file, name: 'file1.tar.gz') expect(file.file_extension).to eq('gz') end + + it 'returns nil if there is no file extension' do + file = build(:ci_secure_file, name: 'file1') + expect(file.file_extension).to be nil + end end describe '#metadata_parsable?' do diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb index 700fd97152a..0e9ee4822ef 100644 --- a/spec/requests/api/ci/secure_files_spec.rb +++ b/spec/requests/api/ci/secure_files_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do before do stub_ci_secure_file_object_storage - stub_feature_flags(ci_secure_files: true) stub_feature_flags(ci_secure_files_read_only: false) end @@ -128,6 +127,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do expect(json_response['name']).to eq(secure_file.name) expect(json_response['expires_at']).to be nil expect(json_response['metadata']).to be nil + expect(json_response['file_extension']).to be nil end it 'returns project secure file details with metadata when supported' do @@ -138,6 +138,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do expect(json_response['name']).to eq(secure_file_with_metadata.name) expect(json_response['expires_at']).to eq('2022-04-26T19:20:40.000Z') expect(json_response['metadata'].keys).to match_array(%w[id issuer subject expires_at]) + expect(json_response['file_extension']).to eq('cer') end it 'responds with 404 Not Found if requesting non-existing secure file' do @@ -250,6 +251,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do expect(json_response['name']).to eq('upload-keystore.jks') expect(json_response['checksum']).to eq(secure_file.checksum) expect(json_response['checksum_algorithm']).to eq('sha256') + expect(json_response['file_extension']).to eq('jks') secure_file = Ci::SecureFile.find(json_response['id']) expect(secure_file.checksum).to eq( diff --git a/spec/support/helpers/database/multiple_databases_helpers.rb b/spec/support/helpers/database/multiple_databases_helpers.rb index 16f5168ca29..aede5180c02 100644 --- a/spec/support/helpers/database/multiple_databases_helpers.rb +++ b/spec/support/helpers/database/multiple_databases_helpers.rb @@ -71,6 +71,14 @@ module Database end # rubocop:enable Database/MultipleDatabases + def with_db_configs(test: test_config) + current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases + ActiveRecord::Base.configurations = { test: test_config } + yield + ensure + ActiveRecord::Base.configurations = current_configurations + end + def with_added_ci_connection if Gitlab::Database.has_config?(:ci) # No need to add a ci: connection if we already have one diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 7f31ea8f9be..3941a239c51 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -64,14 +64,16 @@ RSpec.shared_examples 'a creatable merge request' do visit project_new_merge_request_path(source_project) first('.js-target-project').click - find('.dropdown-target-project li', text: target_project.full_path).click + find('.gl-dropdown-item', text: target_project.full_path).click wait_for_requests first('.js-target-branch').click - within('.js-target-branch-dropdown .dropdown-content') do - expect(page).to have_content('a-brand-new-branch-to-test') - end + find('.gl-listbox-search-input').set('a-brand-new-branch-to-test') + + wait_for_requests + + expect(page).to have_selector('.gl-dropdown-item', text: 'a-brand-new-branch-to-test') end end diff --git a/spec/tasks/gitlab/db/validate_config_rake_spec.rb b/spec/tasks/gitlab/db/validate_config_rake_spec.rb index 1d47c94aa77..cc90345c7e0 100644 --- a/spec/tasks/gitlab/db/validate_config_rake_spec.rb +++ b/spec/tasks/gitlab/db/validate_config_rake_spec.rb @@ -2,7 +2,7 @@ require 'rake_helper' -RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_schemas_validate_connection do +RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_schemas_validate_connection, feature_category: :pods do # We don't need to delete this data since it only modifies `ar_internal_metadata` # which would not be cleaned either by `DbCleaner` self.use_transactional_tests = false @@ -235,12 +235,4 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_sc end end end - - def with_db_configs(test: test_config) - current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases - ActiveRecord::Base.configurations = { test: test_config } - yield - ensure - ActiveRecord::Base.configurations = current_configurations - end end