diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 15:09:35 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 15:09:35 +0000 |
commit | 9c6578ed4e0bc92cd838ef96d978df54403e9609 (patch) | |
tree | 5dff7ad20ae6402e4b7a5a44fe4e81ef04855cdf /app | |
parent | 2af44d609eb8a1579169f9a350bc531d1081d77f (diff) | |
download | gitlab-ce-9c6578ed4e0bc92cd838ef96d978df54403e9609.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
22 files changed, 379 insertions, 195 deletions
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js index f955a41e18a..c719601ee0b 100644 --- a/app/assets/javascripts/pages/projects/snippets/show/index.js +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -1 +1,9 @@ import '~/snippet/snippet_show'; + +const awardEmojiEl = document.getElementById('js-vue-awards-block'); + +if (awardEmojiEl) { + import('~/emoji/awards_app') + .then((m) => m.default(awardEmojiEl)) + .catch(() => {}); +} diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 63048777724..47505093140 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -25,6 +25,10 @@ export default { type: Object, required: true, }, + showLinks: { + type: Boolean, + required: true, + }, viewType: { type: String, required: true, @@ -91,8 +95,8 @@ export default { collectMetrics: true, }; }, - shouldHideLinks() { - return this.isStageView; + showJobLinks() { + return !this.isStageView && this.showLinks; }, shouldShowStageName() { return !this.isStageView; @@ -188,6 +192,7 @@ export default { :config-paths="configPaths" :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" + :show-links="showJobLinks" :type="$options.pipelineTypeConstants.UPSTREAM" :view-type="viewType" @error="onError" @@ -202,9 +207,8 @@ export default { :container-measurements="measurements" :highlighted-job="hoveredJobName" :metrics-config="metricsConfig" - :never-show-links="shouldHideLinks" + :show-links="showJobLinks" :view-type="viewType" - default-link-color="gl-stroke-transparent" @error="onError" @highlightedJobsChange="updateHighlightedJobs" > @@ -234,6 +238,7 @@ export default { :config-paths="configPaths" :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" + :show-links="showJobLinks" :type="$options.pipelineTypeConstants.DOWNSTREAM" :view-type="viewType" @downstreamHovered="setSourceJob" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 0bc6d883245..bff5d3ccdab 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -48,6 +48,7 @@ export default { pipeline: null, pipelineLayers: null, showAlert: false, + showLinks: false, }; }, errorTexts: { @@ -182,6 +183,9 @@ export default { } }, /* eslint-enable @gitlab/require-i18n-strings */ + updateShowLinksState(val) { + this.showLinks = val; + }, updateViewType(type) { this.currentViewType = type; }, @@ -202,7 +206,9 @@ export default { <graph-view-selector v-if="showGraphViewSelector" :type="currentViewType" + :show-links="showLinks" @updateViewType="updateViewType" + @updateShowLinksState="updateShowLinksState" /> </local-storage-sync> <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" /> @@ -211,6 +217,7 @@ export default { :config-paths="configPaths" :pipeline="pipeline" :pipeline-layers="getPipelineLayers()" + :show-links="showLinks" :view-type="currentViewType" @error="reportFailure" @refreshPipelineGraph="refreshPipelineGraph" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue index f33e6290e37..bc038dde21c 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue @@ -1,17 +1,20 @@ <script> -import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui'; +import { GlLoadingIcon, GlSegmentedControl, GlToggle } from '@gitlab/ui'; import { __ } from '~/locale'; import { STAGE_VIEW, LAYER_VIEW } from './constants'; export default { name: 'GraphViewSelector', components: { - GlDropdown, - GlDropdownItem, - GlIcon, - GlSprintf, + GlLoadingIcon, + GlSegmentedControl, + GlToggle, }, props: { + showLinks: { + type: Boolean, + required: true, + }, type: { type: String, required: true, @@ -19,67 +22,119 @@ export default { }, data() { return { - currentViewType: STAGE_VIEW, + currentViewType: this.type, + showLinksActive: false, + isToggleLoading: false, + isSwitcherLoading: false, }; }, i18n: { - labelText: __('Order jobs by'), + viewLabelText: __('Group jobs by'), + linksLabelText: __('Show dependencies'), }, views: { [STAGE_VIEW]: { type: STAGE_VIEW, text: { primary: __('Stage'), - secondary: __('View the jobs grouped into stages'), }, }, [LAYER_VIEW]: { type: LAYER_VIEW, text: { - primary: __('%{codeStart}needs:%{codeEnd} relationships'), - secondary: __('View what jobs are needed for a job to run'), + primary: __('Job dependencies'), }, }, }, computed: { - currentDropdownText() { - return this.$options.views[this.type].text.primary; + showLinksToggle() { + return this.currentViewType === LAYER_VIEW; + }, + viewTypesList() { + return Object.keys(this.$options.views).map((key) => { + return { + value: key, + text: this.$options.views[key].text.primary, + }; + }); + }, + }, + watch: { + /* + How does this reset the loading? As we note in the methods comment below, + the loader is set to on before the update work is undertaken (in the parent). + Once the work is complete, one of these values will change, since that's the + point of the work. When that happens, the related value will update and we are done. + + The bonus for this approach is that it works the same whichever "direction" + the work goes in. + */ + showLinks() { + this.isToggleLoading = false; + }, + type() { + this.isSwitcherLoading = false; }, }, methods: { - itemClick(type) { - this.$emit('updateViewType', type); + /* + In both toggle methods, we use setTimeout so that the loading indicator displays, + then the work is done to update the DOM. The process is: + → user clicks + → call stack: set loading to true + → render: the loading icon appears on the screen + → callback queue: now do the work to calculate the new view / links + (note: this work is done in the parent after the event is emitted) + + setTimeout is how we move the work to the callback queue. + We can't use nextTick because that is called before the render loop. + + See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details. + */ + toggleView(type) { + this.isSwitcherLoading = true; + setTimeout(() => { + this.$emit('updateViewType', type); + }); + }, + toggleShowLinksActive(val) { + this.isToggleLoading = true; + setTimeout(() => { + this.$emit('updateShowLinksState', val); + }); }, }, }; </script> <template> - <div class="gl-display-flex gl-align-items-center gl-my-4"> - <span>{{ $options.i18n.labelText }}</span> - <gl-dropdown data-testid="pipeline-view-selector" class="gl-ml-4"> - <template #button-content> - <gl-sprintf :message="currentDropdownText"> - <template #code="{ content }"> - <code> {{ content }} </code> - </template> - </gl-sprintf> - <gl-icon class="gl-px-2" name="angle-down" :size="16" /> - </template> - <gl-dropdown-item - v-for="view in $options.views" - :key="view.type" - :secondary-text="view.text.secondary" - @click="itemClick(view.type)" - > - <b> - <gl-sprintf :message="view.text.primary"> - <template #code="{ content }"> - <code> {{ content }} </code> - </template> - </gl-sprintf> - </b> - </gl-dropdown-item> - </gl-dropdown> + <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4"> + <gl-loading-icon + v-if="isSwitcherLoading" + data-testid="switcher-loading-state" + class="gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2" + size="lg" + /> + <span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span> + <gl-segmented-control + v-model="currentViewType" + :options="viewTypesList" + :disabled="isSwitcherLoading" + data-testid="pipeline-view-selector" + class="gl-mx-4" + @input="toggleView" + /> + + <div v-if="showLinksToggle"> + <gl-toggle + v-model="showLinksActive" + data-testid="show-links-toggle" + class="gl-mx-4" + :label="$options.i18n.linksLabelText" + :is-loading="isToggleLoading" + label-position="left" + @change="toggleShowLinksActive" + /> + </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 7f772e35e55..89ca6f43abc 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -32,6 +32,10 @@ export default { type: Array, required: true, }, + showLinks: { + type: Boolean, + required: true, + }, type: { type: String, required: true, @@ -217,6 +221,7 @@ export default { :config-paths="configPaths" :pipeline="currentPipeline" :pipeline-layers="getPipelineLayers(pipeline.id)" + :show-links="showLinks" :is-linked-pipeline="true" :view-type="viewType" /> diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue index 8dbab245f44..83843de8085 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue @@ -1,5 +1,4 @@ <script> -import { GlAlert } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { __ } from '~/locale'; import { @@ -19,10 +18,8 @@ import LinksInner from './links_inner.vue'; export default { name: 'LinksLayer', components: { - GlAlert, LinksInner, }, - MAX_GROUPS: 200, props: { containerMeasurements: { type: Object, @@ -37,10 +34,10 @@ export default { required: false, default: () => ({}), }, - neverShowLinks: { + showLinks: { type: Boolean, required: false, - default: false, + default: true, }, }, data() { @@ -67,29 +64,8 @@ export default { shouldCollectMetrics() { return this.metricsConfig.collectMetrics && this.metricsConfig.path; }, - showAlert() { - /* - This is a hard override that allows us to turn off the links without - needing to remove the component entirely for iteration or based on graph type. - */ - if (this.neverShowLinks) { - return false; - } - - return !this.containerZero && !this.showLinkedLayers && !this.alertDismissed; - }, showLinkedLayers() { - /* - This is a hard override that allows us to turn off the links without - needing to remove the component entirely for iteration or based on graph type. - */ - if (this.neverShowLinks) { - return false; - } - - return ( - !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS) - ); + return this.showLinks && !this.containerZero; }, }, errorCaptured(err, _vm, info) { @@ -103,7 +79,7 @@ export default { is closed and functionality is enabled by default. */ - if (this.neverShowLinks && !isEmpty(this.pipelineData)) { + if (!this.showLinks && !isEmpty(this.pipelineData)) { window.requestAnimationFrame(() => { this.prepareLinkData(); }); @@ -151,13 +127,6 @@ export default { reportPerformance(this.metricsConfig.path, data); }); }, - dismissAlert() { - this.alertDismissed = true; - }, - overrideShowLinks() { - this.dismissAlert(); - this.showLinksOverride = true; - }, prepareLinkData() { this.beginPerfMeasure(); let numLinks; @@ -185,15 +154,6 @@ export default { <slot></slot> </links-inner> <div v-else> - <gl-alert - v-if="showAlert" - class="gl-ml-4 gl-mb-4" - :primary-button-text="$options.i18n.showLinksAnyways" - @primaryAction="overrideShowLinks" - @dismiss="dismissAlert" - > - {{ $options.i18n.tooManyJobs }} - </gl-alert> <div class="gl-display-flex gl-relative"> <slot></slot> </div> diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index ff28c3be298..de2ab16b5b1 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -13,6 +13,10 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController before_action :authorize_read_snippet!, except: [:new, :index] before_action :authorize_update_snippet!, only: :edit + before_action only: [:show] do + push_frontend_feature_flag(:improved_emoji_picker, @project, default_enabled: :yaml) + end + def index @snippet_counts = ::Snippets::CountService .new(current_user, project: @project) diff --git a/app/graphql/mutations/boards/lists/base_update.rb b/app/graphql/mutations/boards/lists/base_update.rb new file mode 100644 index 00000000000..c0aa361936f --- /dev/null +++ b/app/graphql/mutations/boards/lists/base_update.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class BaseUpdate < BaseMutation + argument :position, GraphQL::INT_TYPE, + required: false, + description: 'Position of list within the board.' + + argument :collapsed, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Indicates if the list is collapsed for this user.' + + def resolve(list: nil, **args) + if list.nil? || !can_read_list?(list) + raise_resource_not_available_error! + end + + update_result = update_list(list, args) + + { + list: update_result[:list], + errors: list.errors.full_messages + } + end + + private + + def update_list(list, args) + raise NotImplementedError + end + + def can_read_list?(list) + raise NotImplementedError + end + end + end + end +end diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb index 504082ec22c..d17dd5162a0 100644 --- a/app/graphql/mutations/boards/lists/update.rb +++ b/app/graphql/mutations/boards/lists/update.rb @@ -3,7 +3,7 @@ module Mutations module Boards module Lists - class Update < BaseMutation + class Update < BaseUpdate graphql_name 'UpdateBoardList' argument :list_id, Types::GlobalIDType[List], @@ -11,29 +11,11 @@ module Mutations loads: Types::BoardListType, description: 'Global ID of the list.' - argument :position, GraphQL::INT_TYPE, - required: false, - description: 'Position of list within the board.' - - argument :collapsed, GraphQL::BOOLEAN_TYPE, - required: false, - description: 'Indicates if the list is collapsed for this user.' - field :list, Types::BoardListType, null: true, description: 'Mutated list.' - def resolve(list: nil, **args) - raise_resource_not_available_error! unless can_read_list?(list) - update_result = update_list(list, args) - - { - list: update_result[:list], - errors: list.errors.full_messages - } - end - private def update_list(list, args) @@ -42,8 +24,6 @@ module Mutations end def can_read_list?(list) - return false unless list.present? - Ability.allowed?(current_user, :read_issue_board_list, list.board) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 504ebb5606e..cda87cbd212 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -310,9 +310,15 @@ module ApplicationSettingsHelper :throttle_authenticated_web_enabled, :throttle_authenticated_web_period_in_seconds, :throttle_authenticated_web_requests_per_period, + :throttle_authenticated_packages_api_enabled, + :throttle_authenticated_packages_api_period_in_seconds, + :throttle_authenticated_packages_api_requests_per_period, :throttle_unauthenticated_enabled, :throttle_unauthenticated_period_in_seconds, :throttle_unauthenticated_requests_per_period, + :throttle_unauthenticated_packages_api_enabled, + :throttle_unauthenticated_packages_api_period_in_seconds, + :throttle_unauthenticated_packages_api_requests_per_period, :throttle_protected_paths_enabled, :throttle_protected_paths_period_in_seconds, :throttle_protected_paths_requests_per_period, diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index f4af7a5a350..84eb0405c01 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -72,4 +72,10 @@ module SnippetsHelper concat(file_count) end end + + def project_snippets_award_api_path(snippet) + if Feature.enabled?(:improved_emoji_picker, snippet.project, default_enabled: :yaml) + api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id) + end + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f405f5ca5d3..caed36c9fe3 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -434,6 +434,14 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :throttle_unauthenticated_packages_api_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_unauthenticated_packages_api_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + validates :throttle_authenticated_api_requests_per_period, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -450,6 +458,14 @@ class ApplicationSetting < ApplicationRecord presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :throttle_authenticated_packages_api_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_packages_api_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + validates :throttle_protected_paths_requests_per_period, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 66a8d1f8105..88fd17133f2 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -156,6 +156,9 @@ module ApplicationSettingImplementation throttle_authenticated_web_enabled: false, throttle_authenticated_web_period_in_seconds: 3600, throttle_authenticated_web_requests_per_period: 7200, + throttle_authenticated_packages_api_enabled: false, + throttle_authenticated_packages_api_period_in_seconds: 15, + throttle_authenticated_packages_api_requests_per_period: 1000, throttle_incident_management_notification_enabled: false, throttle_incident_management_notification_per_period: 3600, throttle_incident_management_notification_period_in_seconds: 3600, @@ -165,6 +168,9 @@ module ApplicationSettingImplementation throttle_unauthenticated_enabled: false, throttle_unauthenticated_period_in_seconds: 3600, throttle_unauthenticated_requests_per_period: 3600, + throttle_unauthenticated_packages_api_enabled: false, + throttle_unauthenticated_packages_api_period_in_seconds: 15, + throttle_unauthenticated_packages_api_requests_per_period: 800, time_tracking_limit_to_hours: false, two_factor_grace_period: 48, unique_ips_limit_enabled: false, diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb index 8a6f4de540c..028add66a14 100644 --- a/app/models/project_services/confluence_service.rb +++ b/app/models/project_services/confluence_service.rb @@ -27,7 +27,7 @@ class ConfluenceService < Service end def description - s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab') + s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.') end def help @@ -37,11 +37,11 @@ class ConfluenceService < Service wiki_url = project.wiki.web_url s_( - 'ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration' % + 'ConfluenceService|Your GitLab wiki is still available at %{wiki_link}. To re-enable the link to the GitLab wiki, disable this integration.' % { wiki_link: link_to(wiki_url, wiki_url) } ).html_safe else - s_('ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration').html_safe + s_('ConfluenceService|Link to a Confluence Workspace from the sidebar. Enabling this integration replaces the "Wiki" sidebar link with a link to the Confluence Workspace. The GitLab wiki is still available at the original URL.').html_safe end end @@ -50,8 +50,8 @@ class ConfluenceService < Service { type: 'text', name: 'confluence_url', - title: 'Confluence Cloud Workspace URL', - placeholder: s_('ConfluenceService|The URL of the Confluence Workspace'), + title: s_('Confluence Cloud Workspace URL'), + placeholder: 'https://example.atlassian.net/wiki', required: true } ] diff --git a/app/models/wiki.rb b/app/models/wiki.rb index 47fe40b0e57..089fc887d97 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -192,16 +192,9 @@ class Wiki def delete_page(page, message = nil) return unless page - if Feature.enabled?(:gitaly_replace_wiki_delete_page, user, default_enabled: :yaml) - capture_git_error(:deleted) do - repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title)) + capture_git_error(:deleted) do + repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title)) - after_wiki_activity - - true - end - else - wiki.delete_page(page.path, commit_details(:deleted, message, page.title)) after_wiki_activity true diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml index f050c0816b1..fab3ce584f0 100644 --- a/app/views/admin/application_settings/_abuse.html.haml +++ b/app/views/admin/application_settings/_abuse.html.haml @@ -3,9 +3,9 @@ %fieldset .form-group - = f.label :abuse_notification_email, 'Abuse reports notification email', class: 'label-bold' + = f.label :abuse_notification_email, _('Abuse reports notification email'), class: 'label-bold' = f.text_field :abuse_notification_email, class: 'form-control gl-form-input' .form-text.text-muted - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + = _('Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.') - = f.submit 'Save changes', class: "gl-button btn btn-confirm" + = f.submit _('Save changes'), class: "gl-button btn btn-confirm" diff --git a/app/views/admin/application_settings/_package_registry_limits.html.haml b/app/views/admin/application_settings/_package_registry_limits.html.haml new file mode 100644 index 00000000000..b1dfd04c55e --- /dev/null +++ b/app/views/admin/application_settings/_package_registry_limits.html.haml @@ -0,0 +1,37 @@ += form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-packages-limits-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + %h5 + = _('Unauthenticated API request rate limit') + .form-group + .form-check + = f.check_box :throttle_unauthenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_packages_api_checkbox' } + = f.label :throttle_unauthenticated_packages_api_enabled, class: 'form-check-label label-bold' do + = _('Enable unauthenticated API request rate limit') + %span.form-text.text-muted + = _('Helps reduce request volume (e.g. from crawlers or abusive bots)') + .form-group + = f.label :throttle_unauthenticated_packages_api_requests_per_period, 'Max unauthenticated API requests per period per IP', class: 'label-bold' + = f.number_field :throttle_unauthenticated_packages_api_requests_per_period, class: 'form-control gl-form-input' + .form-group + = f.label :throttle_unauthenticated_packages_api_period_in_seconds, 'Unauthenticated API rate limit period in seconds', class: 'label-bold' + = f.number_field :throttle_unauthenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input' + %hr + %h5 + = _('Authenticated API request rate limit') + .form-group + .form-check + = f.check_box :throttle_authenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_packages_api_checkbox' } + = f.label :throttle_authenticated_packages_api_enabled, class: 'form-check-label label-bold' do + = _('Enable authenticated API request rate limit') + %span.form-text.text-muted + = _('Helps reduce request volume (e.g. from crawlers or abusive bots)') + .form-group + = f.label :throttle_authenticated_packages_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold' + = f.number_field :throttle_authenticated_packages_api_requests_per_period, class: 'form-control gl-form-input' + .form-group + = f.label :throttle_authenticated_packages_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold' + = f.number_field :throttle_authenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input' + + = f.submit 'Save changes', class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml index 72716e76013..72a27e4523f 100644 --- a/app/views/admin/application_settings/network.html.haml +++ b/app/views/admin/application_settings/network.html.haml @@ -24,6 +24,17 @@ .settings-content = render 'ip_limits' +%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'packages_limits_content' } } + .settings-header + %h4 + = _('Package Registry Rate Limits') + %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Configure specific limits for Packages API requests that supersede the general user and IP rate limits.') + .settings-content + = render 'package_registry_limits' + %section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_content' } } .settings-header %h4 diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml index 6c75dfe9733..9d42a2bfa93 100644 --- a/app/views/admin/requests_profiles/index.html.haml +++ b/app/views/admin/requests_profiles/index.html.haml @@ -4,9 +4,7 @@ = page_title .bs-callout.clearfix - Pass the header - %code X-Profile-Token: #{@profile_token} - to profile the request + = html_escape(_('Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request')) % { profile_token: @profile_token, codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe } - if @profiles.present? .gl-mt-3 @@ -21,4 +19,4 @@ admin_requests_profile_path(profile) - else %p - No profiles found + = _('No profiles found') diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 726ab7d2372..a296394a2e0 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -6,6 +6,6 @@ #js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} } .row-content-block.top-block.content-component-block - = render 'award_emoji/awards_block', awardable: @snippet, inline: true + = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet) #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 0e54f1a7672..0088cd35781 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -79,7 +79,7 @@ %span= milestone.issues_visible_to_user(current_user).count .title.hide-collapsed = s_('MilestoneSidebar|Issues') - %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).count + %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.issues_visible_to_user(current_user).count - if show_new_issue_link?(project) = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: s_('MilestoneSidebar|New Issue') do = s_('MilestoneSidebar|New issue') @@ -110,7 +110,7 @@ %span= milestone.merge_requests.count .title.hide-collapsed = s_('MilestoneSidebar|Merge requests') - %span.badge.badge-pill= milestone.merge_requests.count + %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.merge_requests.count .value.hide-collapsed.bold - if !project || can?(current_user, :read_merge_request, project) %span.milestone-stat diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb index 53220a7afed..1868fe607c4 100644 --- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb +++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb @@ -21,82 +21,34 @@ module ContainerExpirationPolicies cleanup_tags_service_deleted_size ].freeze - def perform_work - return unless throttling_enabled? - return unless container_repository - - log_extra_metadata_on_done(:container_repository_id, container_repository.id) - log_extra_metadata_on_done(:project_id, project.id) - - unless allowed_to_run?(container_repository) - container_repository.cleanup_unscheduled! - log_extra_metadata_on_done(:cleanup_status, :skipped) - return + delegate :perform_work, :remaining_work_count, to: :inner_instance + + def inner_instance + strong_memoize(:inner_instance) do + if loopless_enabled? + Loopless.new(self) + else + Looping.new(self) + end end - - result = ContainerExpirationPolicies::CleanupService.new(container_repository) - .execute - log_on_done(result) - end - - def remaining_work_count - cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count - cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count - total_count = cleanup_scheduled_count + cleanup_unfinished_count - - log_info( - cleanup_scheduled_count: cleanup_scheduled_count, - cleanup_unfinished_count: cleanup_unfinished_count, - cleanup_total_count: total_count - ) - - total_count end def max_running_jobs return 0 unless throttling_enabled? - ::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_worker_capacity - end - - private - - def allowed_to_run?(container_repository) - return false unless policy&.enabled && policy&.next_run_at - - Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at + ::Gitlab::CurrentSettings.container_registry_expiration_policies_worker_capacity end def throttling_enabled? Feature.enabled?(:container_registry_expiration_policies_throttling) end - def max_cleanup_execution_time - ::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout - end - - def policy - project.container_expiration_policy + def loopless_enabled? + Feature.enabled?(:container_registry_expiration_policies_loopless) end - def project - container_repository.project - end - - def container_repository - strong_memoize(:container_repository) do - ContainerRepository.transaction do - # rubocop: disable CodeReuse/ActiveRecord - # We need a lock to prevent two workers from picking up the same row - container_repository = ContainerRepository.waiting_for_cleanup - .order(:expiration_policy_cleanup_status, :expiration_policy_started_at) - .limit(1) - .lock('FOR UPDATE SKIP LOCKED') - .first - # rubocop: enable CodeReuse/ActiveRecord - container_repository&.tap(&:cleanup_ongoing!) - end - end + def max_cleanup_execution_time + ::Gitlab::CurrentSettings.container_registry_delete_tags_service_timeout end def log_info(extra_structure) @@ -120,5 +72,100 @@ module ContainerExpirationPolicies log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated) log_extra_metadata_on_done(:running_jobs_count, running_jobs_count) end + + # rubocop: disable Scalability/IdempotentWorker + # TODO: move the logic from this class to the parent one when container_registry_expiration_policies_loopless is removed + # Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/325273 + class Loopless + # TODO fill the logic here with the approach documented in + # https://gitlab.com/gitlab-org/gitlab/-/issues/267546#limited-worker + def initialize(parent) + @parent = parent + end + end + # rubocop: enable Scalability/IdempotentWorker + + # rubocop: disable Scalability/IdempotentWorker + # TODO remove this class when `container_registry_expiration_policies_loopless` is removed + # Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/325273 + class Looping + include Gitlab::Utils::StrongMemoize + + delegate :throttling_enabled?, + :log_extra_metadata_on_done, + :log_info, + :log_on_done, + :max_cleanup_execution_time, + to: :@parent + + def initialize(parent) + @parent = parent + end + + def perform_work + return unless throttling_enabled? + return unless container_repository + + log_extra_metadata_on_done(:container_repository_id, container_repository.id) + log_extra_metadata_on_done(:project_id, project.id) + + unless allowed_to_run?(container_repository) + container_repository.cleanup_unscheduled! + log_extra_metadata_on_done(:cleanup_status, :skipped) + return + end + + result = ContainerExpirationPolicies::CleanupService.new(container_repository) + .execute + log_on_done(result) + end + + def remaining_work_count + cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count + cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count + total_count = cleanup_scheduled_count + cleanup_unfinished_count + + log_info( + cleanup_scheduled_count: cleanup_scheduled_count, + cleanup_unfinished_count: cleanup_unfinished_count, + cleanup_total_count: total_count + ) + + total_count + end + + private + + def allowed_to_run?(container_repository) + return false unless policy&.enabled && policy&.next_run_at + + Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at + end + + def policy + project.container_expiration_policy + end + + def project + container_repository.project + end + + def container_repository + strong_memoize(:container_repository) do + ContainerRepository.transaction do + # rubocop: disable CodeReuse/ActiveRecord + # We need a lock to prevent two workers from picking up the same row + container_repository = ContainerRepository.waiting_for_cleanup + .order(:expiration_policy_cleanup_status, :expiration_policy_started_at) + .limit(1) + .lock('FOR UPDATE SKIP LOCKED') + .first + # rubocop: enable CodeReuse/ActiveRecord + container_repository&.tap(&:cleanup_ongoing!) + end + end + end + end + # rubocop: enable Scalability/IdempotentWorker end end |