From e8a31d8dc2afd673ca50d74d26edab0a0fec83ca Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 7 May 2021 09:10:27 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/board_content_sidebar.vue | 18 +++- app/assets/javascripts/boards/index.js | 2 + .../charts/components/pipeline_charts.vue | 2 +- .../static_site_editor/constants.js | 1 - .../resolvers/submit_content_changes.js | 2 + .../static_site_editor/pages/home.vue | 1 + .../services/generate_branch_name.js | 4 +- .../services/submit_content_changes.js | 26 +++--- app/controllers/application_controller.rb | 1 + app/controllers/concerns/floc_opt_out.rb | 17 ++++ app/helpers/application_settings_helper.rb | 1 + app/models/application_setting.rb | 3 + .../application_setting_implementation.rb | 1 + .../application_settings/_floc.html.haml | 22 +++++ .../application_settings/general.html.haml | 1 + .../32107-delete-package-file-api.yml | 5 ++ .../leipert-floc-opt-out-327904.yml | 5 ++ .../track_epic_boards_activity.yml | 8 ++ ...504135823_add_floc_application_settings.rb | 7 ++ db/schema_migrations/20210504135823 | 1 + db/structure.sql | 1 + doc/api/packages.md | 30 +++++++ doc/api/settings.md | 1 + doc/development/usage_ping/dictionary.md | 72 ++++++++++++++++ .../img/pipelines_duration_chart.png | Bin 10587 -> 22289 bytes doc/user/group/saml_sso/index.md | 10 +-- lib/api/package_files.rb | 23 +++++ lib/api/settings.rb | 1 + .../known_events/epic_board_events.yml | 22 +++++ locale/gitlab.pot | 19 ++-- .../application_controller_spec.rb | 40 +++++++++ spec/features/projects/graph_spec.rb | 2 +- spec/frontend/static_site_editor/mock_data.js | 6 +- .../static_site_editor/pages/home_spec.js | 1 + .../services/generate_branch_name_spec.js | 8 +- .../services/renderers/render_image_spec.js | 10 +-- .../services/submit_content_changes_spec.js | 23 ++--- .../hll_redis_counter_spec.rb | 1 + spec/requests/api/package_files_spec.rb | 81 +++++++++++++++++- 39 files changed, 420 insertions(+), 59 deletions(-) create mode 100644 app/controllers/concerns/floc_opt_out.rb create mode 100644 app/views/admin/application_settings/_floc.html.haml create mode 100644 changelogs/unreleased/32107-delete-package-file-api.yml create mode 100644 changelogs/unreleased/leipert-floc-opt-out-327904.yml create mode 100644 config/feature_flags/development/track_epic_boards_activity.yml create mode 100644 db/migrate/20210504135823_add_floc_application_settings.rb create mode 100644 db/schema_migrations/20210504135823 create mode 100644 lib/gitlab/usage_data_counters/known_events/epic_board_events.yml diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index 5a41a01e167..7ce9492f79b 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -11,7 +11,6 @@ import { contentTop } from '~/lib/utils/common_utils'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { headerHeight: `${contentTop()}px`, @@ -32,7 +31,17 @@ export default { SidebarIterationWidget: () => import('ee_component/sidebar/components/sidebar_iteration_widget.vue'), }, - mixins: [glFeatureFlagsMixin()], + inject: { + epicFeatureAvailable: { + default: false, + }, + iterationFeatureAvailable: { + default: false, + }, + weightFeatureAvailable: { + default: false, + }, + }, computed: { ...mapGetters([ 'isSidebarOpen', @@ -77,10 +86,11 @@ export default { class="assignee" @assignees-updated="setAssignees" /> - +
- + { labelsManagePath: $boardApp.dataset.labelsManagePath, labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath, timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours), + epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable), + iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable), weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable), boardWeight: $boardApp.dataset.boardWeight ? parseInt($boardApp.dataset.boardWeight, 10) diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue index b7b33701ab1..1c4413bef71 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -301,7 +301,7 @@ export default {
- {{ __('Duration for the last 30 commits') }} + {{ __('Pipeline durations for the last 30 commits') }} `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT); -const generateBranchName = (username, targetBranch = DEFAULT_TARGET_BRANCH) => +const generateBranchName = (username, targetBranch) => `${username}-${targetBranch}-patch-${generateBranchSuffix()}`; export default generateBranchName; diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js index 6391cfd6cc2..ecb7f60a421 100644 --- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js +++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js @@ -4,7 +4,6 @@ import generateBranchName from '~/static_site_editor/services/generate_branch_na import Tracking from '~/tracking'; import { - DEFAULT_TARGET_BRANCH, SUBMIT_CHANGES_BRANCH_ERROR, SUBMIT_CHANGES_COMMIT_ERROR, SUBMIT_CHANGES_MERGE_REQUEST_ERROR, @@ -16,9 +15,9 @@ import { DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION, } from '../constants'; -const createBranch = (projectId, branch) => +const createBranch = (projectId, branch, targetBranch) => Api.createBranch(projectId, { - ref: DEFAULT_TARGET_BRANCH, + ref: targetBranch, branch, }).catch(() => { throw new Error(SUBMIT_CHANGES_BRANCH_ERROR); @@ -73,13 +72,7 @@ const commit = (projectId, message, branch, actions) => { }); }; -const createMergeRequest = ( - projectId, - title, - description, - sourceBranch, - targetBranch = DEFAULT_TARGET_BRANCH, -) => { +const createMergeRequest = (projectId, title, description, sourceBranch, targetBranch) => { Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_MERGE_REQUEST); Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST); @@ -100,16 +93,17 @@ const submitContentChanges = ({ username, projectId, sourcePath, + targetBranch, content, images, mergeRequestMeta, formattedMarkdown, }) => { - const branch = generateBranchName(username); + const branch = generateBranchName(username, targetBranch); const { title: mergeRequestTitle, description: mergeRequestDescription } = mergeRequestMeta; const meta = {}; - return createBranch(projectId, branch) + return createBranch(projectId, branch, targetBranch) .then(({ data: { web_url: url } }) => { const message = `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`; @@ -133,7 +127,13 @@ const submitContentChanges = ({ .then(({ data: { short_id: label, web_url: url } }) => { Object.assign(meta, { commit: { label, url } }); - return createMergeRequest(projectId, mergeRequestTitle, mergeRequestDescription, branch); + return createMergeRequest( + projectId, + mergeRequestTitle, + mergeRequestDescription, + branch, + targetBranch, + ); }) .then(({ data: { iid: label, web_url: url } }) => { Object.assign(meta, { mergeRequest: { label: label.toString(), url } }); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8a53a9b9555..56ccb7090f4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base include Gitlab::Logging::CloudflareHelper include Gitlab::Utils::StrongMemoize include ::Gitlab::WithFeatureCategory + include FlocOptOut before_action :authenticate_user!, except: [:route_not_found] before_action :enforce_terms!, if: :should_enforce_terms? diff --git a/app/controllers/concerns/floc_opt_out.rb b/app/controllers/concerns/floc_opt_out.rb new file mode 100644 index 00000000000..3039af02bbb --- /dev/null +++ b/app/controllers/concerns/floc_opt_out.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module FlocOptOut + extend ActiveSupport::Concern + + included do + after_action :set_floc_opt_out_header, unless: :floc_enabled? + end + + def floc_enabled? + Gitlab::CurrentSettings.floc_enabled + end + + def set_floc_opt_out_header + response.headers['Permissions-Policy'] = 'interest-cohort=()' + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 051f7cf5bb8..90f5ab5f647 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -233,6 +233,7 @@ module ApplicationSettingsHelper :external_pipeline_validation_service_token, :external_pipeline_validation_service_url, :first_day_of_week, + :floc_enabled, :force_pages_access_control, :gitaly_timeout_default, :gitaly_timeout_medium, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2d4b2ce135e..429f0e58e45 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -504,6 +504,9 @@ class ApplicationSetting < ApplicationRecord validates :whats_new_variant, inclusion: { in: ApplicationSetting.whats_new_variants.keys } + validates :floc_enabled, + inclusion: { in: [true, false], message: _('must be a boolean value') } + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index a38804c7367..5ff1c653f9e 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -77,6 +77,7 @@ module ApplicationSettingImplementation external_pipeline_validation_service_token: nil, external_pipeline_validation_service_url: nil, first_day_of_week: 0, + floc_enabled: false, gitaly_timeout_default: 55, gitaly_timeout_fast: 10, gitaly_timeout_medium: 30, diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml new file mode 100644 index 00000000000..398064f9730 --- /dev/null +++ b/app/views/admin/application_settings/_floc.html.haml @@ -0,0 +1,22 @@ +- expanded = integration_expanded?('floc_') + +%section.settings.no-animate#js-floc-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = s_('FloC|Federated Learning of Cohorts') + %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = s_('FloC|Configure whether you want to participate in FloC.').html_safe + = link_to sprite_icon('question-o'), 'https://github.com/WICG/floc', target: '_blank', class: 'has-tooltip', title: _('More information') + + .settings-content + = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-floc-settings'), html: { class: 'fieldset-form', id: 'floc-settings' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :floc_enabled, class: 'form-check-input' + = f.label :floc_enabled, s_('FloC|Enable FloC (Federated Learning of Cohorts)'), class: 'form-check-label' + = f.submit s_('Save changes'), class: 'gl-button btn btn-confirm' diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index 86226a9de2f..217225e6186 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -112,3 +112,4 @@ = render 'admin/application_settings/third_party_offers' = render 'admin/application_settings/snowplow' = render 'admin/application_settings/eks' += render 'admin/application_settings/floc' diff --git a/changelogs/unreleased/32107-delete-package-file-api.yml b/changelogs/unreleased/32107-delete-package-file-api.yml new file mode 100644 index 00000000000..0aeffe42b77 --- /dev/null +++ b/changelogs/unreleased/32107-delete-package-file-api.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint for deleting a package file +merge_request: 60970 +author: +type: added diff --git a/changelogs/unreleased/leipert-floc-opt-out-327904.yml b/changelogs/unreleased/leipert-floc-opt-out-327904.yml new file mode 100644 index 00000000000..93bff73e640 --- /dev/null +++ b/changelogs/unreleased/leipert-floc-opt-out-327904.yml @@ -0,0 +1,5 @@ +--- +title: Application setting for FloC participation (disabled by default) +merge_request: 60933 +author: +type: added diff --git a/config/feature_flags/development/track_epic_boards_activity.yml b/config/feature_flags/development/track_epic_boards_activity.yml new file mode 100644 index 00000000000..6461a9e826c --- /dev/null +++ b/config/feature_flags/development/track_epic_boards_activity.yml @@ -0,0 +1,8 @@ +--- +name: track_epic_boards_activity +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 +rollout_issue_url: +milestone: '13.12' +type: development +group: group::product planning +default_enabled: true diff --git a/db/migrate/20210504135823_add_floc_application_settings.rb b/db/migrate/20210504135823_add_floc_application_settings.rb new file mode 100644 index 00000000000..a5e3aad6b8c --- /dev/null +++ b/db/migrate/20210504135823_add_floc_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFlocApplicationSettings < ActiveRecord::Migration[6.0] + def change + add_column :application_settings, :floc_enabled, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20210504135823 b/db/schema_migrations/20210504135823 new file mode 100644 index 00000000000..24c5fd50087 --- /dev/null +++ b/db/schema_migrations/20210504135823 @@ -0,0 +1 @@ +9d1254393da80e0b1e387fba493f83f8775f0340f23c648e638a9983f965f5c9 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ce42f7362ca..976ac394aae 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9513,6 +9513,7 @@ CREATE TABLE application_settings ( whats_new_variant smallint DEFAULT 0, encrypted_spam_check_api_key bytea, encrypted_spam_check_api_key_iv bytea, + floc_enabled boolean DEFAULT false NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), diff --git a/doc/api/packages.md b/doc/api/packages.md index 7ad99475515..c257105f72e 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -352,3 +352,33 @@ Can return the following status codes: - `204 No Content`, if the package was deleted successfully. - `404 Not Found`, if the package was not found. + +## Delete a package file + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32107) in GitLab 13.12. + +WARNING: +Deleting a package file may corrupt your package making it unusable or unpullable from your package +manager client. When deleting a package file, be sure that you understand what you're doing. + +Delete a package file: + +```plaintext +DELETE /projects/:id/packages/:package_id/package_files/:package_file_id +``` + +| Attribute | Type | Required | Description | +| ----------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `package_id` | integer | yes | ID of a package. | +| `package_file_id` | integer | yes | ID of a package file. | + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/:id/packages/:package_id/package_files/:package_file_id" +``` + +Can return the following status codes: + +- `204 No Content`: The package was deleted successfully. +- `403 Forbidden`: The user does not have permission to delete the file. +- `404 Not Found`: The package or package file was not found. diff --git a/doc/api/settings.md b/doc/api/settings.md index 38251550ae4..0f0a36c0b5d 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -88,6 +88,7 @@ Example response: "rate_limiting_response_text": null, "keep_latest_artifact": true, "admin_mode": false, + "floc_enabled": false, "external_pipeline_validation_service_timeout": null, "external_pipeline_validation_service_token": null, "external_pipeline_validation_service_url": null diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 80816bf583a..0ec481511fa 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -10234,6 +10234,78 @@ Status: `data_available` Tiers: `free`, `premium`, `ultimate` +### `redis_hll_counters.epic_boards_usage.g_project_management_users_creating_epic_boards_monthly` + +Count of MAU creating epic boards + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210428072511_g_project_management_users_creating_epic_boards_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epic_boards_usage.g_project_management_users_creating_epic_boards_weekly` + +Count of WAU creating epic boards + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210428072508_g_project_management_users_creating_epic_boards_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epic_boards_usage.g_project_management_users_updating_epic_board_names_monthly` + +Count of MAU updating epic board names + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210428073607_g_project_management_users_updating_epic_board_names_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epic_boards_usage.g_project_management_users_updating_epic_board_names_weekly` + +Count of WAU updating epic board names + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210428073604_g_project_management_users_updating_epic_board_names_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epic_boards_usage.g_project_management_users_viewing_epic_boards_monthly` + +Count of MAU viewing epic boards + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210428073329_g_project_management_users_viewing_epic_boards_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epic_boards_usage.g_project_management_users_viewing_epic_boards_weekly` + +Count of WAU viewing epic boards + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210428073327_g_project_management_users_viewing_epic_boards_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + ### `redis_hll_counters.epics_usage.epics_usage_total_unique_counts_monthly` Total monthly users count for epics_usage diff --git a/doc/user/analytics/img/pipelines_duration_chart.png b/doc/user/analytics/img/pipelines_duration_chart.png index 12ec262dadb3ae78989feb1057d992b0516f30b0..17ab717355779430e77e0ecbd5d85d319dd9f77e 100644 GIT binary patch literal 22289 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 - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5291) in GitLab 11.8. +> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/9255) in GitLab 11.11 with ongoing enforcement in the GitLab UI. +> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/292811) in GitLab 13.8, with an updated timeout experience. +> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/211962) in GitLab 13.8 with allowing group owners to not go through SSO. +> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/9152) in GitLab 13.11 with enforcing open SSO session to use Git if this setting is switched on. With this option enabled, users (except owners) must go through your group's GitLab single sign-on URL if they wish to access group resources through the UI. Users can't be manually added as members. diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb index 4a33f3e8af2..6d0c1f44a36 100644 --- a/lib/api/package_files.rb +++ b/lib/api/package_files.rb @@ -30,6 +30,29 @@ module API present paginate(package.package_files), with: ::API::Entities::PackageFile end + + desc 'Remove a package file' do + detail 'This feature was introduced in GitLab 13.12' + end + params do + requires :package_file_id, type: Integer, desc: 'The ID of a package file' + end + delete ':id/packages/:package_id/package_files/:package_file_id' do + authorize_destroy_package!(user_project) + + # We want to make sure the file belongs to the declared package + # so we look up the package before looking up the file. + package = ::Packages::PackageFinder + .new(user_project, params[:package_id]).execute + + not_found! unless package + + package_file = package.package_files.find_by_id(params[:package_file_id]) + + not_found! unless package_file + + destroy_conditionally!(package_file) + end end end end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f54b92134dc..c32825a556f 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -171,6 +171,7 @@ module API optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes" optional :require_admin_approval_after_user_signup, type: Boolean, desc: 'Require explicit admin approval for new signups' optional :whats_new_variant, type: String, values: ApplicationSetting.whats_new_variants.keys, desc: "What's new variant, possible values: `all_tiers`, `current_tier`, and `disabled`." + optional :floc_enabled, type: Grape::API::Boolean, desc: 'Enable FloC (Federated Learning of Cohorts)' ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/gitlab/usage_data_counters/known_events/epic_board_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_board_events.yml new file mode 100644 index 00000000000..281db441829 --- /dev/null +++ b/lib/gitlab/usage_data_counters/known_events/epic_board_events.yml @@ -0,0 +1,22 @@ +# Epic board events +# +# We are using the same slot of issue events 'project_management' for +# epic events to allow data aggregation. +# More information in: https://gitlab.com/gitlab-org/gitlab/-/issues/322405 +- name: g_project_management_users_creating_epic_boards + category: epic_boards_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epic_boards_activity + +- name: g_project_management_users_viewing_epic_boards + category: epic_boards_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epic_boards_activity + +- name: g_project_management_users_updating_epic_board_names + category: epic_boards_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epic_boards_activity diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 24f71c8c45d..eec9886323d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11738,9 +11738,6 @@ msgstr "" msgid "Duration" msgstr "" -msgid "Duration for the last 30 commits" -msgstr "" - msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below." msgstr "" @@ -14097,6 +14094,15 @@ msgstr "" msgid "Flags" msgstr "" +msgid "FloC|Configure whether you want to participate in FloC." +msgstr "" + +msgid "FloC|Enable FloC (Federated Learning of Cohorts)" +msgstr "" + +msgid "FloC|Federated Learning of Cohorts" +msgstr "" + msgid "FlowdockService|1b609b52537..." msgstr "" @@ -14856,10 +14862,10 @@ msgstr "" msgid "Geo|Verification failed - %{error}" msgstr "" -msgid "Geo|Verification status" +msgid "Geo|Verification information" msgstr "" -msgid "Geo|Verificaton information" +msgid "Geo|Verification status" msgstr "" msgid "Geo|Waiting for scheduler" @@ -23668,6 +23674,9 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" +msgid "Pipeline durations for the last 30 commits" +msgstr "" + msgid "Pipeline minutes quota" msgstr "" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index fec81c6a979..0235d7eb95a 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -1027,4 +1027,44 @@ RSpec.describe ApplicationController do get :index end end + + describe 'setting permissions-policy header' do + controller do + skip_before_action :authenticate_user! + + def index + render html: 'It is a flock of sheep, not a floc of sheep.' + end + end + + before do + routes.draw do + get 'index' => 'anonymous#index' + end + end + + context 'with FloC enabled' do + before do + stub_application_setting floc_enabled: true + end + + it 'does not set the Permissions-Policy header' do + get :index + + expect(response.headers['Permissions-Policy']).to eq(nil) + end + end + + context 'with FloC disabled' do + before do + stub_application_setting floc_enabled: false + end + + it 'sets the Permissions-Policy header' do + get :index + + expect(response.headers['Permissions-Policy']).to eq('interest-cohort=()') + end + end + end end diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb index 72df84bf905..7e039a087c7 100644 --- a/spec/features/projects/graph_spec.rb +++ b/spec/features/projects/graph_spec.rb @@ -75,7 +75,7 @@ RSpec.describe 'Project Graph', :js do expect(page).to have_content 'Last week' expect(page).to have_content 'Last month' expect(page).to have_content 'Last year' - expect(page).to have_content 'Duration for the last 30 commits' + expect(page).to have_content 'Pipeline durations for the last 30 commits' end end end diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js index 8bc65c6ce31..8d64e1799b8 100644 --- a/spec/frontend/static_site_editor/mock_data.js +++ b/spec/frontend/static_site_editor/mock_data.js @@ -55,7 +55,7 @@ export const mergeRequestTemplates = [ export const submitChangesError = 'Could not save changes'; export const commitBranchResponse = { - web_url: '/tree/root-master-patch-88195', + web_url: '/tree/root-main-patch-88195', }; export const commitMultipleResponse = { short_id: 'ed899a2f4b5', @@ -84,8 +84,8 @@ export const mounts = [ }, ]; -export const branch = 'master'; +export const branch = 'main'; -export const baseUrl = '/user1/project1/-/sse/master%2Ftest.md'; +export const baseUrl = '/user1/project1/-/sse/main%2Ftest.md'; export const imageRoot = 'source/images/'; diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js index 0936ba3011c..eb056469603 100644 --- a/spec/frontend/static_site_editor/pages/home_spec.js +++ b/spec/frontend/static_site_editor/pages/home_spec.js @@ -275,6 +275,7 @@ describe('static_site_editor/pages/home', () => { formattedMarkdown, project, sourcePath, + targetBranch: branch, username, images, mergeRequestMeta, diff --git a/spec/frontend/static_site_editor/services/generate_branch_name_spec.js b/spec/frontend/static_site_editor/services/generate_branch_name_spec.js index 0624fc3b7b4..7e437506a16 100644 --- a/spec/frontend/static_site_editor/services/generate_branch_name_spec.js +++ b/spec/frontend/static_site_editor/services/generate_branch_name_spec.js @@ -1,7 +1,7 @@ -import { DEFAULT_TARGET_BRANCH, BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants'; +import { BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants'; import generateBranchName from '~/static_site_editor/services/generate_branch_name'; -import { username } from '../mock_data'; +import { username, branch as targetBranch } from '../mock_data'; describe('generateBranchName', () => { const timestamp = 12345678901234; @@ -11,11 +11,11 @@ describe('generateBranchName', () => { }); it('generates a name that includes the username and target branch', () => { - expect(generateBranchName(username)).toMatch(`${username}-${DEFAULT_TARGET_BRANCH}`); + expect(generateBranchName(username, targetBranch)).toMatch(`${username}-${targetBranch}`); }); it(`adds the first ${BRANCH_SUFFIX_COUNT} numbers of the current timestamp`, () => { - expect(generateBranchName(username)).toMatch( + expect(generateBranchName(username, targetBranch)).toMatch( timestamp.toString().substring(BRANCH_SUFFIX_COUNT), ); }); diff --git a/spec/frontend/static_site_editor/services/renderers/render_image_spec.js b/spec/frontend/static_site_editor/services/renderers/render_image_spec.js index e9e40835982..d3298aa0b26 100644 --- a/spec/frontend/static_site_editor/services/renderers/render_image_spec.js +++ b/spec/frontend/static_site_editor/services/renderers/render_image_spec.js @@ -47,11 +47,11 @@ describe('rich_content_editor/renderers/render_image', () => { it.each` destination | isAbsolute | src ${'http://test.host/absolute/path/to/image.png'} | ${true} | ${'http://test.host/absolute/path/to/image.png'} - ${'/relative/path/to/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/default/source/relative/path/to/image.png'} - ${'/target/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/source/with/target/image.png'} - ${'relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/relative/to/current/image.png'} - ${'./relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/./relative/to/current/image.png'} - ${'../relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/master/../relative/to/current/image.png'} + ${'/relative/path/to/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/default/source/relative/path/to/image.png'} + ${'/target/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/source/with/target/image.png'} + ${'relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/relative/to/current/image.png'} + ${'./relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/./relative/to/current/image.png'} + ${'../relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/../relative/to/current/image.png'} `('returns an image with the correct attributes', ({ destination, isAbsolute, src }) => { node.destination = destination; diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js index d4cbc5d235e..d9bceb76a37 100644 --- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js +++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js @@ -3,7 +3,6 @@ import Api from '~/api'; import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; import { - DEFAULT_TARGET_BRANCH, SUBMIT_CHANGES_BRANCH_ERROR, SUBMIT_CHANGES_COMMIT_ERROR, SUBMIT_CHANGES_MERGE_REQUEST_ERROR, @@ -25,6 +24,7 @@ import { createMergeRequestResponse, mergeRequestMeta, sourcePath, + branch as targetBranch, sourceContentYAML as content, trackingCategory, images, @@ -33,7 +33,7 @@ import { jest.mock('~/static_site_editor/services/generate_branch_name'); describe('submitContentChanges', () => { - const branch = 'branch-name'; + const sourceBranch = 'branch-name'; let trackingSpy; let origPage; @@ -41,6 +41,7 @@ describe('submitContentChanges', () => { username, projectId, sourcePath, + targetBranch, content, images, mergeRequestMeta, @@ -54,7 +55,7 @@ describe('submitContentChanges', () => { .spyOn(Api, 'createProjectMergeRequest') .mockResolvedValue({ data: createMergeRequestResponse }); - generateBranchName.mockReturnValue(branch); + generateBranchName.mockReturnValue(sourceBranch); origPage = document.body.dataset.page; document.body.dataset.page = trackingCategory; @@ -69,8 +70,8 @@ describe('submitContentChanges', () => { it('creates a branch named after the username and target branch', () => { return submitContentChanges(buildPayload()).then(() => { expect(Api.createBranch).toHaveBeenCalledWith(projectId, { - ref: DEFAULT_TARGET_BRANCH, - branch, + ref: targetBranch, + branch: sourceBranch, }); }); }); @@ -86,7 +87,7 @@ describe('submitContentChanges', () => { describe('committing markdown formatting changes', () => { const formattedMarkdown = `formatted ${content}`; const commitPayload = { - branch, + branch: sourceBranch, commit_message: `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`, actions: [ { @@ -116,7 +117,7 @@ describe('submitContentChanges', () => { it('commits the content changes to the branch when creating branch succeeds', () => { return submitContentChanges(buildPayload()).then(() => { expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, { - branch, + branch: sourceBranch, commit_message: mergeRequestMeta.title, actions: [ { @@ -140,7 +141,7 @@ describe('submitContentChanges', () => { const payload = buildPayload({ content: contentWithoutImages }); return submitContentChanges(payload).then(() => { expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, { - branch, + branch: sourceBranch, commit_message: mergeRequestMeta.title, actions: [ { @@ -169,8 +170,8 @@ describe('submitContentChanges', () => { convertObjectPropsToSnakeCase({ title, description, - targetBranch: DEFAULT_TARGET_BRANCH, - sourceBranch: branch, + targetBranch, + sourceBranch, }), ); }); @@ -194,7 +195,7 @@ describe('submitContentChanges', () => { }); it('returns the branch name', () => { - expect(result).toMatchObject({ branch: { label: branch } }); + expect(result).toMatchObject({ branch: { label: sourceBranch } }); }); it('returns commit short id and web url', () => { diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 9fc28f6c4ec..4efacae0a48 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -45,6 +45,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'quickactions', 'pipeline_authoring', 'epics_usage', + 'epic_boards_usage', 'secure' ) end diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb index 11170066d6e..137ded050c5 100644 --- a/spec/requests/api/package_files_spec.rb +++ b/spec/requests/api/package_files_spec.rb @@ -7,13 +7,13 @@ RSpec.describe API::PackageFiles do let(:project) { create(:project, :public) } let(:package) { create(:maven_package, project: project) } - before do - project.add_developer(user) - end - describe 'GET /projects/:id/packages/:package_id/package_files' do let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files" } + before do + project.add_developer(user) + end + context 'without the need for a license' do context 'project is public' do it 'returns 200' do @@ -78,4 +78,77 @@ RSpec.describe API::PackageFiles do end end end + + describe 'DELETE /projects/:id/packages/:package_id/package_files/:package_file_id' do + let(:package_file_id) { package.package_files.first.id } + let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files/#{package_file_id}" } + + subject(:api_request) { delete api(url, user) } + + context 'project is public' do + context 'without user' do + let(:user) { nil } + + it 'returns 403 for non authenticated user', :aggregate_failures do + expect { api_request }.not_to change { package.package_files.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + it 'returns 403 for a user without access to the project', :aggregate_failures do + expect { api_request }.not_to change { package.package_files.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'project is private' do + let_it_be_with_refind(:project) { create(:project, :private) } + + it 'returns 404 for a user without access to the project', :aggregate_failures do + expect { api_request }.not_to change { package.package_files.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 403 for a user without enough permissions', :aggregate_failures do + project.add_developer(user) + + expect { api_request }.not_to change { package.package_files.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 204', :aggregate_failures do + project.add_maintainer(user) + + expect { api_request }.to change { package.package_files.count }.by(-1) + + expect(response).to have_gitlab_http_status(:no_content) + end + + context 'without user' do + let(:user) { nil } + + it 'returns 404 for non authenticated user', :aggregate_failures do + expect { api_request }.not_to change { package.package_files.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'invalid file' do + let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files/999999" } + + it 'returns 404 when the package file does not exist', :aggregate_failures do + project.add_maintainer(user) + + expect { api_request }.not_to change { package.package_files.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end end