diff options
33 files changed, 540 insertions, 361 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 33f6e917932..35ed83513ea 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -506,7 +506,6 @@ Layout/ArgumentAlignment: - 'app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb' - 'app/graphql/types/x509_certificate_type.rb' - 'app/graphql/types/x509_issuer_type.rb' - - 'app/models/application_setting.rb' - 'app/models/bulk_imports/configuration.rb' - 'app/models/bulk_imports/entity.rb' - 'app/models/clusters/kubernetes_namespace.rb' @@ -1121,24 +1120,6 @@ Layout/ArgumentAlignment: - 'ee/db/geo/post_migrate/20210217020154_add_unique_index_on_container_repository_registry.rb' - 'ee/db/geo/post_migrate/20210217020156_add_unique_index_on_terraform_state_version_registry.rb' - 'ee/db/seeds/awesome_co/awesome_co.rb' - - 'ee/lib/api/analytics/code_review_analytics.rb' - - 'ee/lib/api/analytics/product_analytics.rb' - - 'ee/lib/api/audit_events.rb' - - 'ee/lib/api/dependencies.rb' - - 'ee/lib/api/epics.rb' - - 'ee/lib/api/group_push_rule.rb' - - 'ee/lib/api/helpers/project_approval_rules_helpers.rb' - - 'ee/lib/api/ldap_group_links.rb' - - 'ee/lib/api/merge_request_approval_settings.rb' - - 'ee/lib/api/merge_trains.rb' - - 'ee/lib/api/project_aliases.rb' - - 'ee/lib/api/project_push_rule.rb' - - 'ee/lib/api/related_epic_links.rb' - - 'ee/lib/api/saml_group_links.rb' - - 'ee/lib/api/status_checks.rb' - - 'ee/lib/api/vulnerability_exports.rb' - - 'ee/lib/api/vulnerability_findings.rb' - - 'ee/lib/api/vulnerability_issue_links.rb' - 'ee/lib/audit/compliance_framework_changes_auditor.rb' - 'ee/lib/audit/external_status_check_changes_auditor.rb' - 'ee/lib/audit/group_changes_auditor.rb' diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_actions.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_actions.vue index f2271f8af24..3fac8fd78a2 100644 --- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_actions.vue +++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_actions.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlDropdown, GlModal } from '@gitlab/ui'; +import { GlDisclosureDropdown, GlModal } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { __, sprintf } from '~/locale'; import { createAlert, VARIANT_SUCCESS } from '~/alert'; @@ -25,12 +25,13 @@ const modalActionButtonAttributes = { }, }, }; +const BLOCK_ACTION = 'block'; +const REMOVE_USER_AND_REPORT_ACTION = 'removeUserAndReport'; export default { name: 'AbuseReportActions', components: { - GlButton, - GlDropdown, + GlDisclosureDropdown, GlModal, }, modalId: 'abuse-report-row-action-confirm-modal', @@ -62,16 +63,43 @@ export default { }, modalData() { return { - block: { + [BLOCK_ACTION]: { action: this.blockUser, confirmText: this.$options.i18n.blockUserConfirm, }, - removeUserAndReport: { + [REMOVE_USER_AND_REPORT_ACTION]: { action: this.removeUserAndReport, confirmText: this.removeUserAndReportConfirmText, }, }; }, + reportActionsDropdownItems() { + return [ + { + text: this.$options.i18n.removeUserAndReport, + action: () => { + this.showConfirmModal(REMOVE_USER_AND_REPORT_ACTION); + }, + extraAttrs: { class: 'gl-text-red-500!' }, + }, + { + text: this.blockUserButtonText, + action: () => { + this.showConfirmModal(BLOCK_ACTION); + }, + extraAttrs: { + disabled: this.userBlocked, + 'data-testid': 'block-user-button', + }, + }, + { + text: this.$options.i18n.removeReport, + action: () => { + this.removeReport(); + }, + }, + ]; + }, }, methods: { showConfirmModal(action) { @@ -123,18 +151,16 @@ export default { </script> <template> - <gl-dropdown text="Actions" text-sr-only icon="ellipsis_v" category="tertiary" no-caret right> - <div class="gl-px-2"> - <gl-button block variant="danger" @click="showConfirmModal('removeUserAndReport')"> - {{ $options.i18n.removeUserAndReport }} - </gl-button> - <gl-button block :disabled="userBlocked" @click="showConfirmModal('block')"> - {{ blockUserButtonText }} - </gl-button> - <gl-button block @click="removeReport"> - {{ $options.i18n.removeReport }} - </gl-button> - </div> + <div> + <gl-disclosure-dropdown + :toggle-text="$options.i18n.actionsToggleText" + text-sr-only + icon="ellipsis_v" + category="tertiary" + no-caret + placement="right" + :items="reportActionsDropdownItems" + /> <gl-modal v-model="confirmModalShown" :modal-id="$options.modalId" @@ -144,5 +170,5 @@ export default { :action-secondary="$options.modalActionButtonAttributes.secondary" @primary="modalData[actionToConfirm].action" /> - </gl-dropdown> + </div> </template> diff --git a/app/assets/javascripts/admin/abuse_reports/constants.js b/app/assets/javascripts/admin/abuse_reports/constants.js index ee002f269ac..7dd60e9da95 100644 --- a/app/assets/javascripts/admin/abuse_reports/constants.js +++ b/app/assets/javascripts/admin/abuse_reports/constants.js @@ -86,4 +86,5 @@ export const ACTIONS_I18N = { removeUserAndReportConfirm: __('USER %{user} WILL BE REMOVED! Are you sure?'), removeUserAndReport: __('Remove user & report'), removeReport: __('Remove report'), + actionsToggleText: __('Actions'), }; diff --git a/app/assets/javascripts/lib/utils/datetime/constants.js b/app/assets/javascripts/lib/utils/datetime/constants.js new file mode 100644 index 00000000000..869ade45ebd --- /dev/null +++ b/app/assets/javascripts/lib/utils/datetime/constants.js @@ -0,0 +1,7 @@ +// Keys for the memoized Intl dateTime formatters +export const DATE_WITH_TIME_FORMAT = 'DATE_WITH_TIME_FORMAT'; +export const DATE_ONLY_FORMAT = 'DATE_ONLY_FORMAT'; + +export const DEFAULT_DATE_TIME_FORMAT = DATE_WITH_TIME_FORMAT; + +export const DATE_TIME_FORMATS = [DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT]; diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index 05f34db662a..a973cd890ba 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -1,6 +1,7 @@ import * as timeago from 'timeago.js'; import { languageCode, s__, createDateTimeFormat } from '~/locale'; import { formatDate } from './date_format_utility'; +import { DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT, DEFAULT_DATE_TIME_FORMAT } from './constants'; /** * Timeago uses underscores instead of dashes to separate language from country code. @@ -106,26 +107,39 @@ timeago.register(timeagoLanguageCode, memoizedLocale()); timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration()); -let memoizedFormatter = null; +const setupAbsoluteFormatters = () => { + const cache = {}; -function setupAbsoluteFormatter() { - if (memoizedFormatter === null) { - const formatter = createDateTimeFormat({ - dateStyle: 'medium', - timeStyle: 'short', - }); + // Intl.DateTimeFormat options (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options) + const formats = { + [DATE_WITH_TIME_FORMAT]: () => ({ dateStyle: 'medium', timeStyle: 'short' }), + [DATE_ONLY_FORMAT]: () => ({ dateStyle: 'medium' }), + }; + + return (formatName = DEFAULT_DATE_TIME_FORMAT) => { + if (cache[formatName]) { + return cache[formatName]; + } + + let format = formats[formatName] && formats[formatName](); + if (!format) { + format = formats[DEFAULT_DATE_TIME_FORMAT](); + } + + const formatter = createDateTimeFormat(format); - memoizedFormatter = { + cache[formatName] = { format(date) { return formatter.format(date instanceof Date ? date : new Date(date)); }, }; - } - return memoizedFormatter; -} + return cache[formatName]; + }; +}; +const memoizedFormatters = setupAbsoluteFormatters(); -export const getTimeago = () => - window.gon?.time_display_relative === false ? setupAbsoluteFormatter() : timeago; +export const getTimeago = (formatName) => + window.gon?.time_display_relative === false ? memoizedFormatters(formatName) : timeago; /** * For the given elements, sets a tooltip with a formatted date. diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index f9a70371680..a6331bc6551 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,3 +1,4 @@ +export * from './datetime/constants'; export * from './datetime/timeago_utility'; export * from './datetime/date_format_utility'; export * from './datetime/date_calculation_utility'; diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue index e8a54b0515e..ce0c08109b7 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue @@ -217,6 +217,8 @@ export default { body-class="gl-p-0!" modal-class="global-search-modal" :centered="false" + @hidden="$emit('hidden')" + @shown="$emit('shown')" > <form role="search" diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index f311c5242f5..e6a6d0f94e3 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -67,6 +67,7 @@ export default { return { mrMenuShown: false, todoCount: this.sidebarData.todos_pending_count, + searchTooltip: this.$options.i18n.searchKbdHelp, }; }, computed: { @@ -84,6 +85,12 @@ export default { updateTodos(e) { this.todoCount = e.detail.count || 0; }, + hideSearchTooltip() { + this.searchTooltip = ''; + }, + showSearchTooltip() { + this.searchTooltip = this.$options.i18n.searchKbdHelp; + }, }, }; </script> @@ -93,6 +100,7 @@ export default { <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2"> <a v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage" + class="tanuki-logo-container" :href="rootPath" :title="$options.i18n.homepage" data-track-action="click_link" @@ -128,14 +136,14 @@ export default { <gl-button id="super-sidebar-search" - v-gl-tooltip.bottom.hover.html="$options.i18n.searchKbdHelp" + v-gl-tooltip.bottom.hover.html="searchTooltip" v-gl-modal="$options.SEARCH_MODAL_ID" data-testid="super-sidebar-search-button" icon="search" :aria-label="$options.i18n.search" category="tertiary" /> - <search-modal /> + <search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" /> <user-menu :data="sidebarData" /> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 0d466df1b7f..7c9a1bcd8cc 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -1,8 +1,8 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; +import { DATE_TIME_FORMATS, DEFAULT_DATE_TIME_FORMAT } from '~/lib/utils/datetime_utility'; import timeagoMixin from '../mixins/timeago'; -import '~/lib/utils/datetime_utility'; /** * Port of ruby helper time_ago_with_tooltip @@ -15,7 +15,7 @@ export default { mixins: [timeagoMixin], props: { time: { - type: [String, Number], + type: [String, Number, Date], required: true, }, tooltipPlacement: { @@ -28,10 +28,16 @@ export default { required: false, default: '', }, + dateTimeFormat: { + type: String, + required: false, + default: DEFAULT_DATE_TIME_FORMAT, + validator: (timeFormat) => DATE_TIME_FORMATS.includes(timeFormat), + }, }, computed: { timeAgo() { - return this.timeFormatted(this.time); + return this.timeFormatted(this.time, this.dateTimeFormat); }, }, }; diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js index 2a0256548a8..61e45fa5195 100644 --- a/app/assets/javascripts/vue_shared/mixins/timeago.js +++ b/app/assets/javascripts/vue_shared/mixins/timeago.js @@ -5,8 +5,8 @@ import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetim */ export default { methods: { - timeFormatted(time) { - const timeago = getTimeago(); + timeFormatted(time, format) { + const timeago = getTimeago(format); return timeago.format(time, timeagoLanguageCode); }, diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 2653e0acb22..592fc886f0a 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -5,10 +5,6 @@ .gl-dark & { mix-blend-mode: screen; } - - .notification { - border-color: $gray-50; - } } @mixin notification-dot($color, $size, $top, $left) { @@ -57,21 +53,39 @@ @include gl-vertical-align-middle; } - .user-bar-item { + .user-bar-item, + .tanuki-logo-container { @include gl-rounded-base; @include gl-p-2; @include gl-bg-transparent; @include gl-border-none; + &:focus, + &:active { + @include gl-focus; + } + } + + .user-bar-item { &:hover, &:focus, &:active { @include active-toggle; } + } + + $light-mode-btn-bg: #e0dfe5; + $dark-mode-btn-bg: #53515b; + .tanuki-logo-container { + &:hover, &:focus, &:active { - @include gl-focus; + background-color: $light-mode-btn-bg; + + .gl-dark & { + background-color: $dark-mode-btn-bg; + } } } } diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 02b8cf9ee28..78668d25c63 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -100,283 +100,288 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord chronic_duration_attr :project_runner_token_expiration_interval_human_readable, :project_runner_token_expiration_interval validates :grafana_url, - system_hook_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ - blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}" - }), - if: :grafana_url_absolute? + system_hook_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ + blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}" + }), + if: :grafana_url_absolute? validate :validate_grafana_url validates :uuid, presence: true validates :outbound_local_requests_whitelist, - length: { maximum: 1_000, message: N_('is too long (maximum is 1000 entries)') }, - allow_nil: false, - qualified_domain_array: true + length: { maximum: 1_000, message: N_('is too long (maximum is 1000 entries)') }, + allow_nil: false, + qualified_domain_array: true validates :session_expire_delay, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :minimum_password_length, - presence: true, - numericality: { only_integer: true, - greater_than_or_equal_to: DEFAULT_MINIMUM_PASSWORD_LENGTH, - less_than_or_equal_to: Devise.password_length.max } + presence: true, + numericality: { + only_integer: true, + greater_than_or_equal_to: DEFAULT_MINIMUM_PASSWORD_LENGTH, + less_than_or_equal_to: Devise.password_length.max + } validates :home_page_url, - allow_blank: true, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, - if: :home_page_url_column_exists? + allow_blank: true, + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, + if: :home_page_url_column_exists? validates :help_page_support_url, - allow_blank: true, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, - if: :help_page_support_url_column_exists? + allow_blank: true, + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, + if: :help_page_support_url_column_exists? validates :help_page_documentation_base_url, - length: { maximum: 255, message: N_("is too long (maximum is %{count} characters)") }, - allow_blank: true, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS + length: { maximum: 255, message: N_("is too long (maximum is %{count} characters)") }, + allow_blank: true, + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS validates :after_sign_out_path, - allow_blank: true, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS + allow_blank: true, + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS validates :abuse_notification_email, - devise_email: true, - allow_blank: true + devise_email: true, + allow_blank: true validates :two_factor_grace_period, - numericality: { greater_than_or_equal_to: 0 } + numericality: { greater_than_or_equal_to: 0 } validates :recaptcha_site_key, - presence: true, - if: :recaptcha_or_login_protection_enabled + presence: true, + if: :recaptcha_or_login_protection_enabled validates :recaptcha_private_key, - presence: true, - if: :recaptcha_or_login_protection_enabled + presence: true, + if: :recaptcha_or_login_protection_enabled validates :akismet_api_key, - presence: true, - if: :akismet_enabled + presence: true, + if: :akismet_enabled validates :spam_check_api_key, - length: { maximum: 2000, message: N_('is too long (maximum is %{count} characters)') }, - allow_blank: true + length: { maximum: 2000, message: N_('is too long (maximum is %{count} characters)') }, + allow_blank: true validates :unique_ips_limit_per_user, - numericality: { greater_than_or_equal_to: 1 }, - presence: true, - if: :unique_ips_limit_enabled + numericality: { greater_than_or_equal_to: 1 }, + presence: true, + if: :unique_ips_limit_enabled validates :unique_ips_limit_time_window, - numericality: { greater_than_or_equal_to: 0 }, - presence: true, - if: :unique_ips_limit_enabled + numericality: { greater_than_or_equal_to: 0 }, + presence: true, + if: :unique_ips_limit_enabled - validates :kroki_url, - presence: { if: :kroki_enabled } + validates :kroki_url, presence: { if: :kroki_enabled } validate :validate_kroki_url, if: :kroki_enabled validates :kroki_formats, json_schema: { filename: 'application_setting_kroki_formats' } validates :metrics_method_call_threshold, - numericality: { greater_than_or_equal_to: 0 }, - presence: true, - if: :prometheus_metrics_enabled + numericality: { greater_than_or_equal_to: 0 }, + presence: true, + if: :prometheus_metrics_enabled - validates :plantuml_url, - presence: true, - if: :plantuml_enabled + validates :plantuml_url, presence: true, if: :plantuml_enabled - validates :sourcegraph_url, - presence: true, - if: :sourcegraph_enabled + validates :sourcegraph_url, presence: true, if: :sourcegraph_enabled validates :gitpod_url, - presence: true, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ enforce_sanitization: true }), - if: :gitpod_enabled + presence: true, + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ enforce_sanitization: true }), + if: :gitpod_enabled validates :mailgun_signing_key, - presence: true, - length: { maximum: 255 }, - if: :mailgun_events_enabled + presence: true, + length: { maximum: 255 }, + if: :mailgun_events_enabled validates :snowplow_collector_hostname, - presence: true, - hostname: true, - if: :snowplow_enabled + presence: true, + hostname: true, + if: :snowplow_enabled validates :max_attachment_size, - presence: true, - numericality: { only_integer: true, greater_than: 0 } + presence: true, + numericality: { only_integer: true, greater_than: 0 } validates :max_artifacts_size, - presence: true, - numericality: { only_integer: true, greater_than: 0 } + presence: true, + numericality: { only_integer: true, greater_than: 0 } validates :max_export_size, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :max_import_size, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :max_pages_size, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0, - less_than: ::Gitlab::Pages::MAX_SIZE / 1.megabyte } + presence: true, + numericality: { + only_integer: true, greater_than_or_equal_to: 0, + less_than: ::Gitlab::Pages::MAX_SIZE / 1.megabyte + } validates :max_pages_custom_domains_per_project, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :jobs_per_stage_page_size, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :max_terraform_state_size_bytes, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :default_artifacts_expire_in, presence: true, duration: true validates :container_expiration_policies_enable_historic_entries, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :container_registry_token_expire_delay, - presence: true, - numericality: { only_integer: true, greater_than: 0 } + presence: true, + numericality: { only_integer: true, greater_than: 0 } validates :repository_storages, presence: true validate :check_repository_storages validate :check_repository_storages_weighted validates :auto_devops_domain, - allow_blank: true, - hostname: { allow_numeric_hostname: true, require_valid_tld: true }, - if: :auto_devops_enabled? + allow_blank: true, + hostname: { allow_numeric_hostname: true, require_valid_tld: true }, + if: :auto_devops_enabled? validates :enabled_git_access_protocol, - inclusion: { in: %w(ssh http), allow_blank: true } + inclusion: { in: %w(ssh http), allow_blank: true } validates :domain_denylist, - presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' }, - if: :domain_denylist_enabled? + presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' }, + if: :domain_denylist_enabled? validates :housekeeping_optimize_repository_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } + presence: true, + numericality: { only_integer: true, greater_than: 0 } validates :terminal_max_session_time, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :polling_interval_multiplier, - presence: true, - numericality: { greater_than_or_equal_to: 0 } + presence: true, + numericality: { greater_than_or_equal_to: 0 } validates :gitaly_timeout_default, - presence: true, - if: :gitaly_timeout_default_changed?, - numericality: { - only_integer: true, - greater_than_or_equal_to: 0, - less_than_or_equal_to: Settings.gitlab.max_request_duration_seconds - } + presence: true, + if: :gitaly_timeout_default_changed?, + numericality: { + only_integer: true, + greater_than_or_equal_to: 0, + less_than_or_equal_to: Settings.gitlab.max_request_duration_seconds + } validates :gitaly_timeout_medium, - presence: true, - if: :gitaly_timeout_medium_changed?, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + if: :gitaly_timeout_medium_changed?, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :gitaly_timeout_medium, - numericality: { less_than_or_equal_to: :gitaly_timeout_default }, - if: :gitaly_timeout_default + numericality: { less_than_or_equal_to: :gitaly_timeout_default }, + if: :gitaly_timeout_default validates :gitaly_timeout_medium, - numericality: { greater_than_or_equal_to: :gitaly_timeout_fast }, - if: :gitaly_timeout_fast + numericality: { greater_than_or_equal_to: :gitaly_timeout_fast }, + if: :gitaly_timeout_fast validates :gitaly_timeout_fast, - presence: true, - if: :gitaly_timeout_fast_changed?, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + if: :gitaly_timeout_fast_changed?, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :gitaly_timeout_fast, - numericality: { less_than_or_equal_to: :gitaly_timeout_default }, - if: :gitaly_timeout_default + numericality: { less_than_or_equal_to: :gitaly_timeout_default }, + if: :gitaly_timeout_default validates :diff_max_patch_bytes, - presence: true, - numericality: { only_integer: true, - greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, - less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND } + presence: true, + numericality: { + only_integer: true, + greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, + less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND + } validates :diff_max_files, - presence: true, - numericality: { only_integer: true, - greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_FILES_SETTING, - less_than_or_equal_to: Commit::MAX_DIFF_FILES_SETTING_UPPER_BOUND } + presence: true, + numericality: { + only_integer: true, + greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_FILES_SETTING, + less_than_or_equal_to: Commit::MAX_DIFF_FILES_SETTING_UPPER_BOUND + } validates :diff_max_lines, - presence: true, - numericality: { only_integer: true, - greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_LINES_SETTING, - less_than_or_equal_to: Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND } + presence: true, + numericality: { + only_integer: true, + greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_LINES_SETTING, + less_than_or_equal_to: Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND + } validates :user_default_internal_regex, js_regex: true, allow_nil: true validates :default_preferred_language, presence: true, inclusion: { in: Gitlab::I18n.available_locales } validates :personal_access_token_prefix, - format: { with: %r{\A[a-zA-Z0-9_+=/@:.-]+\z}, - message: N_("can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'") }, - length: { maximum: 20, message: N_('is too long (maximum is %{count} characters)') }, - allow_blank: true + format: { with: %r{\A[a-zA-Z0-9_+=/@:.-]+\z}, + message: N_("can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'") }, + length: { maximum: 20, message: N_('is too long (maximum is %{count} characters)') }, + allow_blank: true validates :commit_email_hostname, format: { with: /\A[^@]+\z/ } validates :archive_builds_in_seconds, - allow_nil: true, - numericality: { - only_integer: true, - greater_than_or_equal_to: 1.day.seconds, - message: N_('must be at least 1 day') - } + allow_nil: true, + numericality: { + only_integer: true, + greater_than_or_equal_to: 1.day.seconds, + message: N_('must be at least 1 day') + } validates :local_markdown_version, - allow_nil: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } + allow_nil: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } validates :asset_proxy_url, - presence: true, - allow_blank: false, - url: true, - if: :asset_proxy_enabled? + presence: true, + allow_blank: false, + url: true, + if: :asset_proxy_enabled? validates :asset_proxy_secret_key, - presence: true, - allow_blank: false, - if: :asset_proxy_enabled? + presence: true, + allow_blank: false, + if: :asset_proxy_enabled? validates :static_objects_external_storage_url, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true validates :static_objects_external_storage_auth_token, - presence: true, - if: :static_objects_external_storage_url? + presence: true, + if: :static_objects_external_storage_url? validates :protected_paths, - length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, - allow_nil: false + length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, + allow_nil: false validates :push_event_hooks_limit, - numericality: { greater_than_or_equal_to: 0 } + numericality: { greater_than_or_equal_to: 0 } validates :push_event_activities_limit, - numericality: { greater_than_or_equal_to: 0 } + numericality: { greater_than_or_equal_to: 0 } validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 } validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes } @@ -388,59 +393,59 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :hashed_storage_enabled, inclusion: { in: [true], message: N_("Hashed storage can't be disabled anymore for new projects") } validates :container_registry_delete_tags_service_timeout, - :container_registry_cleanup_tags_service_max_list_size, - :container_registry_expiration_policies_worker_capacity, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + :container_registry_cleanup_tags_service_max_list_size, + :container_registry_expiration_policies_worker_capacity, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :container_registry_expiration_policies_caching, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :container_registry_import_max_tags_count, - :container_registry_import_max_retries, - :container_registry_import_start_max_retries, - :container_registry_import_max_step_duration, - :container_registry_pre_import_timeout, - :container_registry_import_timeout, - allow_nil: false, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + :container_registry_import_max_retries, + :container_registry_import_start_max_retries, + :container_registry_import_max_step_duration, + :container_registry_pre_import_timeout, + :container_registry_import_timeout, + allow_nil: false, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :container_registry_pre_import_tags_rate, - allow_nil: false, - numericality: { greater_than_or_equal_to: 0 } + allow_nil: false, + numericality: { greater_than_or_equal_to: 0 } validates :container_registry_import_target_plan, presence: true validates :container_registry_import_created_before, presence: true validates :dependency_proxy_ttl_group_policy_worker_capacity, - allow_nil: false, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + allow_nil: false, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :packages_cleanup_package_file_worker_capacity, - :package_registry_cleanup_policies_worker_capacity, - allow_nil: false, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + :package_registry_cleanup_policies_worker_capacity, + allow_nil: false, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :invisible_captcha_enabled, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :deactivate_dormant_users_period, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 90, message: N_("'%{value}' days of inactivity must be greater than or equal to 90") }, - if: :deactivate_dormant_users? + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 90, message: N_("'%{value}' days of inactivity must be greater than or equal to 90") }, + if: :deactivate_dormant_users? validates :allow_possible_spam, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :deny_all_requests_except_allowed, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :silent_mode_enabled, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } Gitlab::SSHPublicKey.supported_types.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } @@ -469,93 +474,90 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validate :terms_exist, if: :enforce_terms? validates :external_authorization_service_default_label, - presence: true, - if: :external_authorization_service_enabled + presence: true, + if: :external_authorization_service_enabled validates :external_authorization_service_url, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true, - if: :external_authorization_service_enabled + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true, + if: :external_authorization_service_enabled validates :external_authorization_service_timeout, - numericality: { greater_than: 0, less_than_or_equal_to: 10 }, - if: :external_authorization_service_enabled + numericality: { greater_than: 0, less_than_or_equal_to: 10 }, + if: :external_authorization_service_enabled validates :spam_check_endpoint_url, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ schemes: %w(tls grpc) }), allow_blank: true + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ schemes: %w(tls grpc) }), allow_blank: true validates :spam_check_endpoint_url, - presence: true, - if: :spam_check_endpoint_enabled + presence: true, + if: :spam_check_endpoint_enabled validates :external_auth_client_key, - presence: true, - if: ->(setting) { setting.external_auth_client_cert.present? } + presence: true, + if: ->(setting) { setting.external_auth_client_cert.present? } validates :lets_encrypt_notification_email, - devise_email: true, - format: { without: /@example\.(com|org|net)\z/, - message: N_("Let's Encrypt does not accept emails on example.com") }, - allow_blank: true + devise_email: true, + format: { without: /@example\.(com|org|net)\z/, message: N_("Let's Encrypt does not accept emails on example.com") }, + allow_blank: true validates :lets_encrypt_notification_email, - presence: true, - if: :lets_encrypt_terms_of_service_accepted? + presence: true, + if: :lets_encrypt_terms_of_service_accepted? validates :eks_integration_enabled, - inclusion: { in: [true, false] } + inclusion: { in: [true, false] } validates :eks_account_id, - format: { with: Gitlab::Regex.aws_account_id_regex, - message: Gitlab::Regex.aws_account_id_message }, - if: :eks_integration_enabled? + format: { with: Gitlab::Regex.aws_account_id_regex, message: Gitlab::Regex.aws_account_id_message }, + if: :eks_integration_enabled? validates :eks_access_key_id, - length: { in: 16..128 }, - if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } + length: { in: 16..128 }, + if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } validates :eks_secret_access_key, - presence: true, - if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } + presence: true, + if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } validates_with X509CertificateCredentialsValidator, - certificate: :external_auth_client_cert, - pkey: :external_auth_client_key, - pass: :external_auth_client_key_pass, - if: ->(setting) { setting.external_auth_client_cert.present? } + certificate: :external_auth_client_cert, + pkey: :external_auth_client_key, + pass: :external_auth_client_key_pass, + if: ->(setting) { setting.external_auth_client_cert.present? } validates :default_ci_config_path, - format: { without: %r{(\.{2}|\A/)}, - message: N_('cannot include leading slash or directory traversal.') }, + format: { without: %r{(\.{2}|\A/)}, message: N_('cannot include leading slash or directory traversal.') }, length: { maximum: 255 }, allow_blank: true validates :issues_create_limit, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :raw_blob_request_limit, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :pipeline_limit_per_project_user_sha, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :ci_jwt_signing_key, - rsa_key: true, allow_nil: true + rsa_key: true, allow_nil: true validates :customers_dot_jwt_signing_key, - rsa_key: true, allow_nil: true + rsa_key: true, allow_nil: true validates :rate_limiting_response_text, - length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, - allow_blank: true + length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, + allow_blank: true validates :jira_connect_application_key, - length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, - allow_blank: true + length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, + allow_blank: true validates :jira_connect_proxy_url, - length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, - allow_blank: true, - public_url: ADDRESSABLE_URL_VALIDATION_OPTIONS + length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') }, + allow_blank: true, + public_url: ADDRESSABLE_URL_VALIDATION_OPTIONS with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do validates :throttle_unauthenticated_api_requests_per_period @@ -592,36 +594,36 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord end validates :notes_create_limit_allowlist, - length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, - allow_nil: false + length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, + allow_nil: false validates :admin_mode, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :external_pipeline_validation_service_url, - addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true + addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS, allow_blank: true validates :external_pipeline_validation_service_timeout, - allow_nil: true, - numericality: { only_integer: true, greater_than: 0 } + allow_nil: true, + numericality: { only_integer: true, greater_than: 0 } validates :whats_new_variant, - inclusion: { in: ApplicationSetting.whats_new_variants.keys } + inclusion: { in: ApplicationSetting.whats_new_variants.keys } validates :floc_enabled, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } enum sidekiq_job_limiter_mode: { - Gitlab::SidekiqMiddleware::SizeLimiter::Validator::TRACK_MODE => 0, - Gitlab::SidekiqMiddleware::SizeLimiter::Validator::COMPRESS_MODE => 1 # The default - } + Gitlab::SidekiqMiddleware::SizeLimiter::Validator::TRACK_MODE => 0, + Gitlab::SidekiqMiddleware::SizeLimiter::Validator::COMPRESS_MODE => 1 # The default + } validates :sidekiq_job_limiter_mode, - inclusion: { in: self.sidekiq_job_limiter_modes } + inclusion: { in: self.sidekiq_job_limiter_modes } validates :sidekiq_job_limiter_compression_threshold_bytes, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :sidekiq_job_limiter_limit_bytes, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :sentry_enabled, inclusion: { in: [true, false], message: N_('must be a boolean value') } @@ -646,8 +648,8 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :users_get_by_id_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :users_get_by_id_limit_allowlist, - length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, - allow_nil: false + length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, + allow_nil: false validates :update_runner_versions_enabled, inclusion: { in: [true, false], message: N_('must be a boolean value') } @@ -657,21 +659,21 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord if: :update_runner_versions_enabled? validates :inactive_projects_min_size_mb, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :inactive_projects_delete_after_months, - numericality: { only_integer: true, greater_than: 0 } + numericality: { only_integer: true, greater_than: 0 } validates :inactive_projects_send_warning_email_after_months, - numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months } + numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months } validates :database_apdex_settings, json_schema: { filename: 'application_setting_database_apdex_settings' }, allow_nil: true attr_encrypted :asset_proxy_secret_key, - mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, - algorithm: 'aes-256-cbc', - insecure_mode: true + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc', + insecure_mode: true private_class_method def self.encryption_options_base_32_aes_256_gcm { @@ -713,27 +715,27 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord attr_encrypted :openai_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) validates :disable_feed_token, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :disable_admin_oauth_scopes, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :bulk_import_enabled, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :allow_runner_registration_token, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :default_syntax_highlighting_theme, - allow_nil: false, - numericality: { only_integer: true, greater_than: 0 }, - inclusion: { in: Gitlab::ColorSchemes.valid_ids, message: N_('must be a valid syntax highlighting theme ID') } + allow_nil: false, + numericality: { only_integer: true, greater_than: 0 }, + inclusion: { in: Gitlab::ColorSchemes.valid_ids, message: N_('must be a valid syntax highlighting theme ID') } validates :gitlab_dedicated_instance, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } + allow_nil: false, + inclusion: { in: [true, false], message: N_('must be a boolean value') } before_validation :ensure_uuid! before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed? diff --git a/config/feature_flags/development/real_time_issue_weight.yml b/config/feature_flags/development/real_time_issue_weight.yml deleted file mode 100644 index 8fbc7c2bb13..00000000000 --- a/config/feature_flags/development/real_time_issue_weight.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: real_time_issue_weight -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115059 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/398619 -milestone: '15.11' -type: development -group: group::application performance -default_enabled: false diff --git a/data/deprecations/15-9-insecure-ci-job-token.yml b/data/deprecations/15-9-insecure-ci-job-token.yml index b0cb643290d..5664ffee008 100644 --- a/data/deprecations/15-9-insecure-ci-job-token.yml +++ b/data/deprecations/15-9-insecure-ci-job-token.yml @@ -9,11 +9,15 @@ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395708 # (required) Link to the deprecation issue in GitLab body: | # (required) Do not modify this line, instead modify the lines below. - In GitLab 14.4 we introduced the ability to limit the "outbound" scope of the CI/CD job token (`CI_JOB_TOKEN`) to make it more secure. You can prevent job tokens from your project's pipelines from being used to access other projects. If needed, you can list specific projects that you want to access with your project's job tokens. + In GitLab 14.4 we introduced the ability to [limit your project's CI/CD job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#limit-your-projects-job-token-access) (`CI_JOB_TOKEN`) access to make it more secure. You can prevent job tokens **from your project's** pipelines from being used to **access other projects**. When enabled with no other configuration, your pipelines cannot access other projects. To use the job token to access other projects from your pipeline, you must list those projects explicitly in the **Limit CI_JOB_TOKEN access** setting's allowlist, and you must be a maintainer in all the projects. - In 15.9 we extended this functionality with a better solution, an "inbound" scope limit. You can prevent the job tokens from _other_ projects from being used to access your project. With this feature, you can optionally list specific projects that you want to allow to access your project with _their_ job token. + The job token functionality was updated in 15.9 with a better security setting to [allow access to your project with a job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-access-to-your-project-with-a-job-token). When enabled with no other configuration, job tokens **from other projects** cannot **access your project**. Similar to the older setting, you can optionally allow other projects to access your project with a job token if you list those projects explicitly in the **Allow access to this project with a CI_JOB_TOKEN** setting's allowlist. With this new setting, you must be a maintainer in your own project, but only need to have the Guest role in the other projects. - In 16.0, this inbound scope limit will be the only option available for all projects, and the outbound limit setting will be disabled. To prepare for this change, you can enable the ["inbound" CI/CD job token limit](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#configure-the-job-token-scope-limit) feature now, and list any projects that need to access your project. + As a result, the **Limit** setting is deprecated in preference of the better **Allow access** setting. In GitLab 16.0 the **Limit** setting will be disabled by default for all new projects. In projects with this setting currently enabled, it will continue to function as expected, but you will not be able to add any more projects to the allowlist. If the setting is disabled in any project, it will not be possible to re-enable this setting in 16.0 or later. + + In 17.0, we plan to remove the **Limit** setting completely, and set the **Allow access** setting to enabled for all projects. This change ensures a higher level of security between projects. If you currently use the **Limit** setting, you should update your projects to use the **Allow access** setting instead. If other projects access your project with a job token, you must add them to the **Allow access** allowlist. + + To prepare for this change, users on GitLab.com or self-managed GitLab 15.9 or later can enable the **Allow access** setting now and add the other projects. It will not be possible to disable the setting in 17.0 or later. # # OPTIONAL END OF SUPPORT FIELDS # diff --git a/doc/integration/saml.md b/doc/integration/saml.md index ad7f35d2aff..b6e001d57d5 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -408,7 +408,7 @@ You can configure GitLab to use multiple SAML IdPs if: - The `strategy_class` is explicitly set because it cannot be inferred from provider name. -[SAML group memberships](#configure-users-based-on-saml-group-membership) and [Group Sync](../user/group/saml_sso/group_sync.md) do not support multiple IdPs. For more information, see [issue 386605](https://gitlab.com/gitlab-org/gitlab/-/issues/386605). +[SAML group memberships](#configure-users-based-on-saml-group-membership) and [Group Sync](../user/group/saml_sso/group_sync.md) do not support multiple IdPs. For more information, see [issue 386605](https://gitlab.com/gitlab-org/gitlab/-/issues/386605). This also includes `required_groups`, as mentioned in [issue 391926](https://gitlab.com/gitlab-org/gitlab/-/issues/391926). To set up multiple SAML IdPs: diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 53d1d356a77..1eb1241d8b8 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -787,11 +787,15 @@ These three variables will be removed in GitLab 16.0. - [Breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/) </div> -In GitLab 14.4 we introduced the ability to limit the "outbound" scope of the CI/CD job token (`CI_JOB_TOKEN`) to make it more secure. You can prevent job tokens from your project's pipelines from being used to access other projects. If needed, you can list specific projects that you want to access with your project's job tokens. +In GitLab 14.4 we introduced the ability to [limit your project's CI/CD job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#limit-your-projects-job-token-access) (`CI_JOB_TOKEN`) access to make it more secure. You can prevent job tokens **from your project's** pipelines from being used to **access other projects**. When enabled with no other configuration, your pipelines cannot access other projects. To use the job token to access other projects from your pipeline, you must list those projects explicitly in the **Limit CI_JOB_TOKEN access** setting's allowlist, and you must be a maintainer in all the projects. -In 15.9 we extended this functionality with a better solution, an "inbound" scope limit. You can prevent the job tokens from _other_ projects from being used to access your project. With this feature, you can optionally list specific projects that you want to allow to access your project with _their_ job token. +The job token functionality was updated in 15.9 with a better security setting to [allow access to your project with a job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-access-to-your-project-with-a-job-token). When enabled with no other configuration, job tokens **from other projects** cannot **access your project**. Similar to the older setting, you can optionally allow other projects to access your project with a job token if you list those projects explicitly in the **Allow access to this project with a CI_JOB_TOKEN** setting's allowlist. With this new setting, you must be a maintainer in your own project, but only need to have the Guest role in the other projects. -In 16.0, this inbound scope limit will be the only option available for all projects, and the outbound limit setting will be disabled. To prepare for this change, you can enable the ["inbound" CI/CD job token limit](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#configure-the-job-token-scope-limit) feature now, and list any projects that need to access your project. +As a result, the **Limit** setting is deprecated in preference of the better **Allow access** setting. In GitLab 16.0 the **Limit** setting will be disabled by default for all new projects. In projects with this setting currently enabled, it will continue to function as expected, but you will not be able to add any more projects to the allowlist. If the setting is disabled in any project, it will not be possible to re-enable this setting in 16.0 or later. + +In 17.0, we plan to remove the **Limit** setting completely, and set the **Allow access** setting to enabled for all projects. This change ensures a higher level of security between projects. If you currently use the **Limit** setting, you should update your projects to use the **Allow access** setting instead. If other projects access your project with a job token, you must add them to the **Allow access** allowlist. + +To prepare for this change, users on GitLab.com or self-managed GitLab 15.9 or later can enable the **Allow access** setting now and add the other projects. It will not be possible to disable the setting in 17.0 or later. </div> diff --git a/doc/user/admin_area/license_file.md b/doc/user/admin_area/license_file.md index 69edb4551da..e60b78f1139 100644 --- a/doc/user/admin_area/license_file.md +++ b/doc/user/admin_area/license_file.md @@ -183,6 +183,10 @@ License.current.license_id # License data in Base64-encoded ASCII format License.current.data + +# Confirm the current billable seat count excluding guest users. This is useful for customers who use an Ultimate subscription tier where Guest seats are not counted. +User.active.without_bots.excluding_guests.count + ``` #### Interaction with licenses that start in the future diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index d96acc36b2e..fa01b9ced17 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -215,7 +215,10 @@ module QA end def edit! - click_element(:edit_button) + # Click by JS is needed to bypass the Moved MR actions popover + # Change back to regular click_element when moved_mr_sidebar FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 + click_by_javascript(find_element(:edit_button)) end def fast_forward_not_possible? @@ -390,12 +393,18 @@ module QA end def view_email_patches - click_element(:mr_code_dropdown) + # Click by JS is needed to bypass the Moved MR actions popover + # Change back to regular click_element when moved_mr_sidebar FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 + click_by_javascript(find_element(:mr_code_dropdown)) visit_link_in_element(:download_email_patches_menu_item) end def view_plain_diff - click_element(:mr_code_dropdown) + # Click by JS is needed to bypass the Moved MR actions popover + # Change back to regular click_element when moved_mr_sidebar FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 + click_by_javascript(find_element(:mr_code_dropdown)) visit_link_in_element(:download_plain_diff_menu_item) end @@ -406,7 +415,10 @@ module QA end def click_open_in_web_ide - click_element(:mr_code_dropdown) + # Click by JS is needed to bypass the Moved MR actions popover + # Change back to regular click_element when moved_mr_sidebar FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 + click_by_javascript(find_element(:mr_code_dropdown)) click_element(:open_in_web_ide_button) page.driver.browser.switch_to.window(page.driver.browser.window_handles.last) wait_for_requests diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 20dce1b3639..05f38968718 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -66,7 +66,10 @@ module QA end def click_close_issue_button - click_element :close_issue_button + # Click by JS is needed to bypass the Moved MR actions popover + # Change back to regular click_element when moved_mr_sidebar FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 + click_by_javascript(find_element(:close_issue_button)) end def has_reopen_issue_button? @@ -74,12 +77,15 @@ module QA end def has_delete_issue_button? - click_element(:issue_actions_ellipsis_dropdown) + # Click by JS is needed to bypass the Moved MR actions popover + # Change back to regular click_element when moved_mr_sidebar FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 + click_by_javascript(find('[data-qa-selector="issue_actions_ellipsis_dropdown"] > button')) has_element?(:delete_issue_button) end def delete_issue - click_element(:issue_actions_ellipsis_dropdown) + has_delete_issue_button? click_element(:delete_issue_button, Page::Modal::DeleteIssue, diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 1c43faebd78..0620221051e 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -132,7 +132,7 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do def open_actions_dropdown(report_row) within(report_row) do - find('.dropdown-toggle').click + find('[data-testid="base-dropdown-toggle"]').click end end diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js index e72d0c24d5e..2d0f00ea585 100644 --- a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js +++ b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js @@ -1,10 +1,10 @@ -import { mount, shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { GlButton, GlModal } from '@gitlab/ui'; -import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue'; +import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; +import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { sprintf } from '~/locale'; @@ -16,26 +16,29 @@ jest.mock('~/alert'); describe('AbuseReportActions', () => { let wrapper; - const findRemoveUserAndReportButton = () => wrapper.findAllComponents(GlButton).at(0); - const findBlockUserButton = () => wrapper.findAllComponents(GlButton).at(1); - const findRemoveReportButton = () => wrapper.findAllComponents(GlButton).at(2); + const findRemoveUserAndReportButton = () => wrapper.findByText('Remove user & report'); + const findBlockUserButton = () => wrapper.findByTestId('block-user-button'); + const findRemoveReportButton = () => wrapper.findByText('Remove report'); const findConfirmationModal = () => wrapper.findComponent(GlModal); const report = mockAbuseReports[0]; - const createComponent = ({ props, mountFn } = { props: {}, mountFn: mount }) => { - wrapper = mountFn(AbuseReportActions, { + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(AbuseReportActions, { propsData: { report, ...props, }, + stubs: { + GlDisclosureDropdown, + GlDisclosureDropdownItem, + }, }); }; - const createShallowComponent = (props) => createComponent({ props, mountFn: shallowMount }); describe('default', () => { beforeEach(() => { - createShallowComponent(); + createComponent(); }); it('displays "Block user", "Remove user & report", and "Remove report" buttons', () => { @@ -55,11 +58,11 @@ describe('AbuseReportActions', () => { describe('block button when user is already blocked', () => { it('is disabled and has the correct text', () => { - createShallowComponent({ report: { ...report, userBlocked: true } }); + createComponent({ report: { ...report, userBlocked: true } }); const button = findBlockUserButton(); expect(button.text()).toBe(ACTIONS_I18N.alreadyBlocked); - expect(button.attributes('disabled')).toBe('true'); + expect(button.attributes('disabled')).toBe('disabled'); }); }); @@ -127,7 +130,7 @@ describe('AbuseReportActions', () => { blockButtonDisabled: undefined, }, ])( - 'when reponse JSON is $responseData', + 'when response JSON is $responseData', ({ responseData, createAlertArgs, blockButtonText, blockButtonDisabled }) => { beforeEach(async () => { axiosMock.onPut(report.blockUserPath).reply(HTTP_STATUS_OK, responseData); diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap index e2f1d6e4b10..3b407d11041 100644 --- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap +++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap @@ -58,6 +58,7 @@ exports[`Design note component should match the snapshot 1`] = ` > <time-ago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2019-07-26T15:02:20Z" tooltipplacement="bottom" /> diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap index 3517c0f7a44..7773950708f 100644 --- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap +++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap @@ -59,6 +59,7 @@ exports[`Design management list item component with notes renders item with mult Updated <timeago-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="01-01-2019" tooltipplacement="bottom" /> @@ -138,6 +139,7 @@ exports[`Design management list item component with notes renders item with sing Updated <timeago-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="01-01-2019" tooltipplacement="bottom" /> diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js index c13d55f978e..74ce8175357 100644 --- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js +++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js @@ -1,3 +1,4 @@ +import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/constants'; import { getTimeago, localTimeAgo, timeFor, duration } from '~/lib/utils/datetime/timeago_utility'; import { s__ } from '~/locale'; import '~/commons/bootstrap'; @@ -24,15 +25,37 @@ describe('TimeAgo utils', () => { window.gon = { time_display_relative: false }; }); - it.each([ + const defaultFormatExpectations = [ [new Date().toISOString(), 'Jul 6, 2020, 12:00 AM'], [new Date(), 'Jul 6, 2020, 12:00 AM'], [new Date().getTime(), 'Jul 6, 2020, 12:00 AM'], // Slightly different behaviour when `null` is passed :see_no_evil` [null, 'Jan 1, 1970, 12:00 AM'], - ])('formats date `%p` as `%p`', (date, result) => { + ]; + + it.each(defaultFormatExpectations)('formats date `%p` as `%p`', (date, result) => { expect(getTimeago().format(date)).toEqual(result); }); + + describe('with unknown format', () => { + it.each(defaultFormatExpectations)( + 'uses default format and formats date `%p` as `%p`', + (date, result) => { + expect(getTimeago('non_existent').format(date)).toEqual(result); + }, + ); + }); + + describe('with DATE_ONLY_FORMAT', () => { + it.each([ + [new Date().toISOString(), 'Jul 6, 2020'], + [new Date(), 'Jul 6, 2020'], + [new Date().getTime(), 'Jul 6, 2020'], + [null, 'Jan 1, 1970'], + ])('formats date `%p` as `%p`', (date, result) => { + expect(getTimeago(DATE_ONLY_FORMAT).format(date)).toEqual(result); + }); + }); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap index 047fa04947c..2cfe7bdab64 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap @@ -38,6 +38,7 @@ exports[`PackageTitle renders with tags 1`] = ` published <time-ago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2020-08-17T14:23:32Z" tooltipplacement="top" /> @@ -139,6 +140,7 @@ exports[`PackageTitle renders without tags 1`] = ` published <time-ago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2020-08-17T14:23:32Z" tooltipplacement="top" /> diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap index ec8e77fa923..57452bada19 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap @@ -95,6 +95,7 @@ exports[`packages_list_row renders 1`] = ` Created <timeago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2020-08-17T14:23:32Z" tooltipplacement="top" /> diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap index a0b545add27..6825d4afecf 100644 --- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap +++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap @@ -48,6 +48,7 @@ exports[`Repository last commit component renders commit widget 1`] = ` <timeago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2019-01-01" tooltipplacement="bottom" /> diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap index b99d741e984..85bf683fdf6 100644 --- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap +++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap @@ -56,6 +56,7 @@ exports[`Repository table row component renders a symlink table row 1`] = ` > <timeago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2019-01-01" tooltipplacement="top" /> @@ -121,6 +122,7 @@ exports[`Repository table row component renders table row 1`] = ` > <timeago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2019-01-01" tooltipplacement="top" /> @@ -186,6 +188,7 @@ exports[`Repository table row component renders table row for path with special > <timeago-tooltip-stub cssclass="" + datetimeformat="DATE_WITH_TIME_FORMAT" time="2019-01-01" tooltipplacement="top" /> diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js index eb8801f68c6..f78e141afad 100644 --- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js @@ -1,4 +1,4 @@ -import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; +import { GlModal, GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; import Vue from 'vue'; import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -87,6 +87,8 @@ describe('GlobalSearchModal', () => { ); }; + const findGlobalSearchModal = () => wrapper.findComponent(GlModal); + const findGlobalSearchForm = () => wrapper.findByTestId('global-search-form'); const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findScopeToken = () => wrapper.findComponent(GlToken); @@ -350,5 +352,21 @@ describe('GlobalSearchModal', () => { }); }); }); + + describe('Modal events', () => { + beforeEach(() => { + createComponent(); + }); + + it('should emit `shown` event when modal shown`', () => { + findGlobalSearchModal().vm.$emit('shown'); + expect(wrapper.emitted('shown')).toHaveLength(1); + }); + + it('should emit `hidden` event when modal hidden`', () => { + findGlobalSearchModal().vm.$emit('hidden'); + expect(wrapper.emitted('hidden')).toHaveLength(1); + }); + }); }); }); diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js index 2b75fb27972..7abd64ca108 100644 --- a/spec/frontend/super_sidebar/components/user_bar_spec.js +++ b/spec/frontend/super_sidebar/components/user_bar_spec.js @@ -173,6 +173,22 @@ describe('UserBar component', () => { it('should render search modal', () => { expect(findSearchModal().exists()).toBe(true); }); + + describe('Search tooltip', () => { + it('should hide search tooltip when modal is shown', async () => { + findSearchModal().vm.$emit('shown'); + await nextTick(); + const tooltip = getBinding(findSearchButton().element, 'gl-tooltip'); + expect(tooltip.value).toBe(''); + }); + + it('should add search tooltip when modal is hidden', async () => { + findSearchModal().vm.$emit('hidden'); + await nextTick(); + const tooltip = getBinding(findSearchButton().element, 'gl-tooltip'); + expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`); + }); + }); }); describe('While impersonating a user', () => { diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js index a1757952dc0..17a363ad8b1 100644 --- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import timezoneMock from 'timezone-mock'; import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; +import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/constants'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; describe('Time ago with tooltip component', () => { @@ -49,12 +50,36 @@ describe('Time ago with tooltip component', () => { expect(vm.attributes('datetime')).toEqual(timestamp); }); + it('should render with the timestamp provided as Date', () => { + buildVm({ time: new Date(timestamp) }); + + expect(vm.text()).toEqual(timeAgoTimestamp); + }); + it('should render provided scope content with the correct timeAgo string', () => { buildVm(null, { default: `<span>The time is {{ props.timeAgo }}</span>` }); expect(vm.text()).toEqual(`The time is ${timeAgoTimestamp}`); }); + describe('with User Setting timeDisplayRelative: false', () => { + beforeEach(() => { + window.gon = { time_display_relative: false }; + }); + + it('should render with the correct absolute datetime in the default format', () => { + buildVm(); + + expect(vm.text()).toEqual('May 8, 2017, 2:57 PM'); + }); + + it('should render with the correct absolute datetime in the requested dateTimeFormat', () => { + buildVm({ dateTimeFormat: DATE_ONLY_FORMAT }); + + expect(vm.text()).toEqual('May 8, 2017'); + }); + }); + describe('number based timestamps', () => { // Store a date object before we mock the TZ const date = new Date(); diff --git a/workhorse/go.mod b/workhorse/go.mod index dac0d22433b..9482fd19c4d 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -26,7 +26,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/smartystreets/goconvey v1.7.2 github.com/stretchr/testify v1.8.2 - gitlab.com/gitlab-org/gitaly/v15 v15.10.2 + gitlab.com/gitlab-org/gitaly/v15 v15.10.3 gitlab.com/gitlab-org/golang-archive-zip v0.1.1 gitlab.com/gitlab-org/labkit v1.18.0 gocloud.dev v0.29.0 diff --git a/workhorse/go.sum b/workhorse/go.sum index fa74abbd7d3..e1974f9a5be 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -1921,8 +1921,8 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/gitlab-org/gitaly/v15 v15.10.2 h1:e3uqhqYAsl20VddIBpGngY3t6+nK6QnZjY7MhufhV2k= -gitlab.com/gitlab-org/gitaly/v15 v15.10.2/go.mod h1:ZsyTd8zGxT4WuSZJ0G8ydJb60mauzEP0XJtZJEy/7bc= +gitlab.com/gitlab-org/gitaly/v15 v15.10.3 h1:TiSfXoBeLGvHSEUdtyg4HYGEOJk0Y51m7fMlfiWD0Sk= +gitlab.com/gitlab-org/gitaly/v15 v15.10.3/go.mod h1:ZsyTd8zGxT4WuSZJ0G8ydJb60mauzEP0XJtZJEy/7bc= gitlab.com/gitlab-org/golang-archive-zip v0.1.1 h1:35k9giivbxwF03+8A05Cm8YoxoakU8FBCj5gysjCTCE= gitlab.com/gitlab-org/golang-archive-zip v0.1.1/go.mod h1:ZDtqpWPGPB9qBuZnZDrKQjIdJtkN7ZAoVwhT6H2o2kE= gitlab.com/gitlab-org/labkit v1.18.0 h1:uYCIqDt/5V1hLIecTR4UNc1sD2+xiYplyKeyfpNN26A= |