diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
index edcfe40d198..68e69e405ee 100644
--- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -1 +1 @@
-2.14.0
+2.15.0
diff --git a/app/assets/javascripts/members/components/table/expires_at.vue b/app/assets/javascripts/members/components/table/expires_at.vue
deleted file mode 100644
index c91de061b50..00000000000
--- a/app/assets/javascripts/members/components/table/expires_at.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
- {{ s__('Members|No expiration set') }}
-
- {{ s__('Members|Expired') }}
-
-
- {{ inWords }}
-
-
-
-
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index debc3fc31f6..3caecdd1d4b 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -10,7 +10,6 @@ import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
-import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
@@ -24,7 +23,6 @@ export default {
GlPagination,
MemberAvatar,
CreatedAt,
- ExpiresAt,
MembersTableCell,
MemberSource,
MemberActionButtons,
@@ -182,10 +180,6 @@ export default {
-
-
-
-
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 6f465245d20..54d4442d5ce 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -37,12 +37,6 @@ export const FIELDS = [
thClass: 'col-meta',
tdClass: 'col-meta',
},
- {
- key: 'expires',
- label: __('Access expires'),
- thClass: 'col-meta',
- tdClass: 'col-meta',
- },
{
key: 'maxRole',
label: __('Max role'),
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 0137ff87979..01a371920f8 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -11,7 +11,7 @@ import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
-const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
+const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-group-members-list-app'), {
[MEMBER_TYPES.user]: {
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index 16c4a6191b2..e92b9b30fa4 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -27,19 +27,22 @@ export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
};
export default class TimezoneDropdown {
- constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = defaults) {
+ constructor({
+ $dropdownEl,
+ $inputEl,
+ onSelectTimezone,
+ displayFormat,
+ allowEmpty = false,
+ } = defaults) {
this.$dropdown = $dropdownEl;
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $inputEl;
- this.timezoneData = this.$dropdown.data('data');
+ this.timezoneData = this.$dropdown.data('data') || [];
this.onSelectTimezone = onSelectTimezone;
this.displayFormat = displayFormat || defaults.displayFormat;
+ this.allowEmpty = allowEmpty;
- this.initialTimezone =
- findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone;
-
- this.initDefaultTimezone();
this.initDropdown();
}
@@ -52,24 +55,25 @@ export default class TimezoneDropdown {
search: {
fields: ['name'],
},
- clicked: (cfg) => this.updateInputValue(cfg),
+ clicked: (cfg) => this.handleDropdownChange(cfg),
text: (item) => formatTimezone(item),
});
- this.setDropdownToggle(this.displayFormat(this.initialTimezone));
- }
+ const initialTimezone = findTimezoneByIdentifier(this.timezoneData, this.$input.val());
- initDefaultTimezone() {
- if (!this.$input.val()) {
- this.$input.val(defaultTimezone.name);
+ if (initialTimezone !== null) {
+ this.setDropdownValue(initialTimezone);
+ } else if (!this.allowEmpty) {
+ this.setDropdownValue(defaultTimezone);
}
}
- setDropdownToggle(dropdownText) {
- this.$dropdownToggle.text(dropdownText);
+ setDropdownValue(timezone) {
+ this.$dropdownToggle.text(this.displayFormat(timezone));
+ this.$input.val(timezone.name);
}
- updateInputValue({ selectedObj, e }) {
+ handleDropdownChange({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
if (this.onSelectTimezone) {
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 0b662c945c6..947bbdacf2c 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -26,7 +26,7 @@ initInviteMembersForm();
new UsersSelect(); // eslint-disable-line no-new
-const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
+const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list-app'), {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index c49ade2bbb8..ff9b47cdcd6 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -21,6 +21,7 @@ export default class Profile {
$inputEl: this.$inputEl,
$dropdownEl: $('.js-timezone-dropdown'),
displayFormat: (selectedItem) => formatTimezone(selectedItem),
+ allowEmpty: true,
});
}
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index da936215ad4..2dca9385da6 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -37,7 +37,7 @@ class Import::BulkImportsController < ApplicationController
end
def create
- response = BulkImportService.new(current_user, create_params, credentials).execute
+ response = ::BulkImports::CreateService.new(current_user, create_params, credentials).execute
if response.success?
render json: response.payload.to_json(only: [:id])
diff --git a/app/helpers/time_zone_helper.rb b/app/helpers/time_zone_helper.rb
index f92e32ff9b6..a0d9c8403e8 100644
--- a/app/helpers/time_zone_helper.rb
+++ b/app/helpers/time_zone_helper.rb
@@ -33,6 +33,8 @@ module TimeZoneHelper
end
def local_time(timezone)
+ return if timezone.blank?
+
time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone
time_zone_instance.now.strftime("%-l:%M %p")
end
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 337ae7125f3..7687430cfd1 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -23,7 +23,6 @@ class UserPreference < ApplicationRecord
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
- default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
default_value_for :time_display_relative, value: true, allows_nil: false
default_value_for :time_format_in_24h, value: false, allows_nil: false
default_value_for :render_whitespace_in_code, value: false, allows_nil: false
diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb
deleted file mode 100644
index 4e13e967dbd..00000000000
--- a/app/services/bulk_import_service.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-# Entry point of the BulkImport feature.
-# This service receives a Gitlab Instance connection params
-# and a list of groups to be imported.
-#
-# Process topography:
-#
-# sync | async
-# |
-# User +--> P1 +----> Pn +---+
-# | ^ | Enqueue new job
-# | +-----+
-#
-# P1 (sync)
-#
-# - Create a BulkImport record
-# - Create a BulkImport::Entity for each group to be imported
-# - Enqueue a BulkImportWorker job (P2) to import the given groups (entities)
-#
-# Pn (async)
-#
-# - For each group to be imported (BulkImport::Entity.with_status(:created))
-# - Import the group data
-# - Create entities for each subgroup of the imported group
-# - Enqueue a BulkImportService job (Pn) to import the new entities (subgroups)
-#
-class BulkImportService
- attr_reader :current_user, :params, :credentials
-
- def initialize(current_user, params, credentials)
- @current_user = current_user
- @params = params
- @credentials = credentials
- end
-
- def execute
- bulk_import = create_bulk_import
-
- BulkImportWorker.perform_async(bulk_import.id)
-
- ServiceResponse.success(payload: bulk_import)
- rescue ActiveRecord::RecordInvalid => e
- ServiceResponse.error(
- message: e.message,
- http_status: :unprocessable_entity
- )
- end
-
- private
-
- def create_bulk_import
- BulkImport.transaction do
- bulk_import = BulkImport.create!(user: current_user, source_type: 'gitlab')
- bulk_import.create_configuration!(credentials.slice(:url, :access_token))
-
- params.each do |entity|
- BulkImports::Entity.create!(
- bulk_import: bulk_import,
- source_type: entity[:source_type],
- source_full_path: entity[:source_full_path],
- destination_name: entity[:destination_name],
- destination_namespace: entity[:destination_namespace]
- )
- end
-
- bulk_import
- end
- end
-end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
new file mode 100644
index 00000000000..1cea7632aa1
--- /dev/null
+++ b/app/services/bulk_imports/create_service.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+# Entry point of the BulkImport feature.
+# This service receives a Gitlab Instance connection params
+# and a list of groups to be imported.
+#
+# Process topography:
+#
+# sync | async
+# |
+# User +--> P1 +----> Pn +---+
+# | ^ | Enqueue new job
+# | +-----+
+#
+# P1 (sync)
+#
+# - Create a BulkImport record
+# - Create a BulkImport::Entity for each group to be imported
+# - Enqueue a BulkImportWorker job (P2) to import the given groups (entities)
+#
+# Pn (async)
+#
+# - For each group to be imported (BulkImport::Entity.with_status(:created))
+# - Import the group data
+# - Create entities for each subgroup of the imported group
+# - Enqueue a BulkImports::CreateService job (Pn) to import the new entities (subgroups)
+#
+module BulkImports
+ class CreateService
+ attr_reader :current_user, :params, :credentials
+
+ def initialize(current_user, params, credentials)
+ @current_user = current_user
+ @params = params
+ @credentials = credentials
+ end
+
+ def execute
+ bulk_import = create_bulk_import
+
+ BulkImportWorker.perform_async(bulk_import.id)
+
+ ServiceResponse.success(payload: bulk_import)
+ rescue ActiveRecord::RecordInvalid => e
+ ServiceResponse.error(
+ message: e.message,
+ http_status: :unprocessable_entity
+ )
+ end
+
+ private
+
+ def create_bulk_import
+ BulkImport.transaction do
+ bulk_import = BulkImport.create!(user: current_user, source_type: 'gitlab')
+ bulk_import.create_configuration!(credentials.slice(:url, :access_token))
+
+ params.each do |entity|
+ BulkImports::Entity.create!(
+ bulk_import: bulk_import,
+ source_type: entity[:source_type],
+ source_full_path: entity[:source_full_path],
+ destination_name: entity[:destination_name],
+ destination_namespace: entity[:destination_namespace]
+ )
+ end
+
+ bulk_import
+ end
+ end
+ end
+end
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index ad8cde689f9..3e41f107e04 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -80,7 +80,7 @@
%p= s_("Profiles|Set your local time zone")
.col-lg-8
%h5= _("Time zone")
- = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
+ = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
.col-lg-12
%hr
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 20cbe08225e..522f0f771cd 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -84,10 +84,12 @@
= sprite_icon('location', css_class: 'fgray')
%span{ itemprop: 'addressLocality' }
= @user.location
- = render 'middle_dot_divider', stacking: true do
- = sprite_icon('clock', css_class: 'fgray')
- %span
- = local_time(@user.timezone)
+ - user_local_time = local_time(@user.timezone)
+ - unless user_local_time.nil?
+ = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
+ = sprite_icon('clock', css_class: 'fgray')
+ %span
+ = user_local_time
- unless work_information(@user).blank?
= render 'middle_dot_divider', stacking: true do
= sprite_icon('work', css_class: 'fgray')
diff --git a/doc/administration/timezone.md b/doc/administration/timezone.md
index 87bd999ddf1..f4339263d34 100644
--- a/doc/administration/timezone.md
+++ b/doc/administration/timezone.md
@@ -48,8 +48,12 @@ gitlab-ctl restart
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/29669) in GitLab 13.9.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/29669) in GitLab 14.1.
-A user can set their time zone in their profile. If a user has not set their time zone, it defaults
-to the time zone [configured at the instance level](#changing-your-time-zone). On GitLab.com, the
-default time zone is UTC.
+Users can set their time zone in their profile. On GitLab.com, the default time zone is UTC.
+
+New users do not have a default time zone in [GitLab 14.4 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/340795). New users must
+explicitly set their time zone before it displays on their profile.
+
+In GitLab 14.3 and earlier, users with no configured time zone default to the time zone
+[configured at the instance level](#changing-your-time-zone).
For more information, see [Set your time zone](../user/profile/index.md#set-your-time-zone).
diff --git a/doc/api/integrations.md b/doc/api/integrations.md
index d59b0c8b4c1..6d0d7cfb3af 100644
--- a/doc/api/integrations.md
+++ b/doc/api/integrations.md
@@ -726,8 +726,6 @@ GET /projects/:id/integrations/github
## Hangouts Chat
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20290) in GitLab 11.2.
-
Google Workspace team collaboration tool.
### Create/Edit Hangouts Chat integration
@@ -738,9 +736,6 @@ Set Hangouts Chat integration for a project.
PUT /projects/:id/integrations/hangouts-chat
```
-NOTE:
-Specific event parameters (for example, `push_events` flag) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
-
Parameters:
| Parameter | Type | Required | Description |
@@ -832,10 +827,6 @@ GET /projects/:id/integrations/jira
Set Jira integration for a project.
-> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and
-> `project_url` are replaced by `url`. If you are using an
-> older version, [follow this documentation](https://gitlab.com/gitlab-org/gitlab/-/blob/8-13-stable-ee/doc/api/services.md#jira).
-
```plaintext
PUT /projects/:id/integrations/jira
```
@@ -1198,9 +1189,6 @@ Set Slack integration for a project.
PUT /projects/:id/integrations/slack
```
-NOTE:
-Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
-
Parameters:
| Parameter | Type | Required | Description |
@@ -1308,9 +1296,6 @@ Set Mattermost notifications integration for a project.
PUT /projects/:id/integrations/mattermost
```
-NOTE:
-Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435) in GitLab 10.4.
-
Parameters:
| Parameter | Type | Required | Description |
diff --git a/doc/user/clusters/agent/ci_cd_tunnel.md b/doc/user/clusters/agent/ci_cd_tunnel.md
index 1ea5168f30c..6c8b7c95771 100644
--- a/doc/user/clusters/agent/ci_cd_tunnel.md
+++ b/doc/user/clusters/agent/ci_cd_tunnel.md
@@ -10,6 +10,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - The pre-configured `KUBECONFIG` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324275) in GitLab 14.2.
> - The ability to authorize groups was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
+WARNING:
+The CI/CD Tunnel is not supported for GitLab self-managed instances installed via Omnibus. We
+plan to [add support for Omnibus](https://gitlab.com/gitlab-org/gitlab/-/issues/324272) in the future.
+
The CI/CD Tunnel enables users to access Kubernetes clusters from GitLab CI/CD jobs even if there is no network
connectivity between GitLab Runner and a cluster. GitLab Runner does not have to be running in the same cluster.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index 45227638317..1e09559c7fc 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -171,8 +171,51 @@ from the GitLab server.
Blobs are kept forever on the GitLab server, and there is no hard limit on how much data can be
stored.
+### Using the API to clear the cache
+
To reclaim disk space used by image blobs that are no longer needed, use
-the [Dependency Proxy API](../../../api/dependency_proxy.md).
+the [Dependency Proxy API](../../../api/dependency_proxy.md) to clear the entire
+cache.
+
+### Cleanup policies
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294187) in GitLab Free 14.4.
+
+The cleanup policy is a scheduled job you can use to clear cached images that are no longer used,
+freeing up additional storage space. The policies use time-to-live (TTL) logic:
+
+- The number of days is configured.
+- All cached dependency proxy files that have not been pulled in that many days are deleted.
+
+Use the [GraphQL API](../../../api/graphql/reference/index.md#mutationupdatedependencyproxyimagettlgrouppolicy)
+to enable and configure cleanup policies:
+
+```graphql
+mutation {
+ updateDependencyProxyImageTtlGroupPolicy(input:
+ {
+ groupPath: "",
+ enabled: true,
+ ttl: 90
+ }
+ ) {
+ dependencyProxyImageTtlPolicy {
+ enabled
+ ttl
+ }
+ errors
+ }
+}
+```
+
+See the [Getting started with GraphQL](../../../api/graphql/getting_started.md)
+guide to learn how to make GraphQL queries. Support for enabling and configuring cleanup policies in
+the UI is tracked in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340777).
+
+When the policy is initially enabled, the default TTL setting is 90 days. Once enabled, stale
+dependency proxy files are queued for deletion each day. Deletion may not occur right away due to
+processing time. If the image is pulled after the cached files are marked as expired, the expired
+files are ignored and new files are downloaded and cached from the external registry.
## Docker Hub rate limits and the Dependency Proxy
diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb
index 0705a8285c1..c732da17166 100644
--- a/lib/api/bulk_imports.rb
+++ b/lib/api/bulk_imports.rb
@@ -51,7 +51,7 @@ module API
end
end
post do
- response = BulkImportService.new(
+ response = ::BulkImports::CreateService.new(
current_user,
params[:entities],
url: params[:configuration][:url],
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 469cf57afce..d58c676d686 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -21212,9 +21212,6 @@ msgstr ""
msgid "Members|Expiration date updated successfully."
msgstr ""
-msgid "Members|Expired"
-msgstr ""
-
msgid "Members|Filter members"
msgstr ""
@@ -21230,9 +21227,6 @@ msgstr ""
msgid "Members|Membership"
msgstr ""
-msgid "Members|No expiration set"
-msgstr ""
-
msgid "Members|Remove \"%{groupName}\""
msgstr ""
@@ -21254,9 +21248,6 @@ msgstr ""
msgid "Members|Search invited"
msgstr ""
-msgid "Members|in %{time}"
-msgstr ""
-
msgid "Member|Deny access"
msgstr ""
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index 3b2ed2c63ed..1f4b119a058 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -199,9 +199,9 @@ RSpec.describe Import::BulkImportsController do
session[:bulk_import_gitlab_url] = instance_url
end
- it 'executes BulkImportService' do
+ it 'executes BulkImpors::CreatetService' do
expect_next_instance_of(
- BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
+ ::BulkImports::CreateService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: bulk_import))
end
@@ -214,7 +214,7 @@ RSpec.describe Import::BulkImportsController do
it 'returns error when validation fails' do
error_response = ServiceResponse.error(message: 'Record invalid', http_status: :unprocessable_entity)
expect_next_instance_of(
- BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
+ ::BulkImports::CreateService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
allow(service).to receive(:execute).and_return(error_response)
end
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 2dfcd941b4f..d822a5ea871 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
context 'when group link exists' do
let_it_be(:shared_with_group) { create(:group) }
let_it_be(:shared_group) { create(:group) }
+ let_it_be(:expiration_date) { 5.days.from_now.to_date }
let(:additional_link_attrs) { {} }
@@ -115,29 +116,29 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
click_groups_tab
page.within first_row do
- fill_in 'Expiration date', with: 5.days.from_now.to_date
+ fill_in 'Expiration date', with: expiration_date
find_field('Expiration date').native.send_keys :enter
wait_for_requests
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
end
end
context 'when expiry date is set' do
- let(:additional_link_attrs) { { expires_at: 5.days.from_now.to_date } }
+ let(:additional_link_attrs) { { expires_at: expiration_date } }
it 'clears expiry date' do
click_groups_tab
page.within first_row do
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
find('[data-testid="clear-button"]').click
wait_for_requests
- expect(page).to have_content('No expiration set')
+ expect(page).to have_field('Expiration date', with: '')
end
end
end
diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
index ddf3c6d8f9b..86185b8dd32 100644
--- a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js
let_it_be(:user1) { create(:user, name: 'John Doe') }
let_it_be(:group) { create(:group) }
+ let_it_be(:expiration_date) { 5.days.from_now.to_date }
let(:new_member) { create(:user, name: 'Mary Jane') }
@@ -19,10 +20,10 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js
it 'expiration date is displayed in the members list' do
visit group_group_members_path(group)
- invite_member(new_member.name, role: 'Guest', expires_at: 5.days.from_now.to_date)
+ invite_member(new_member.name, role: 'Guest', expires_at: expiration_date)
page.within second_row do
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
end
end
@@ -31,27 +32,27 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js
visit group_group_members_path(group)
page.within second_row do
- fill_in 'Expiration date', with: 5.days.from_now.to_date
+ fill_in 'Expiration date', with: expiration_date
find_field('Expiration date').native.send_keys :enter
wait_for_requests
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
end
end
it 'clears expiration date' do
- create(:group_member, :developer, user: new_member, group: group, expires_at: 5.days.from_now.to_date)
+ create(:group_member, :developer, user: new_member, group: group, expires_at: expiration_date)
visit group_group_members_path(group)
page.within second_row do
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
find('[data-testid="clear-button"]').click
wait_for_requests
- expect(page).to have_content('No expiration set')
+ expect(page).to have_field('Expiration date', with: '')
end
end
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index afb6fa26138..026da5814e3 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -561,14 +561,11 @@ RSpec.describe 'User edit profile' do
page.find("a", text: "Nuku'alofa").click
- tz = page.find('.user-time-preferences #user_timezone', visible: false)
-
- expect(tz.value).to eq('Pacific/Tongatapu')
+ expect(page).to have_field(:user_timezone, with: 'Pacific/Tongatapu', type: :hidden)
end
- it 'timezone defaults to servers default' do
- timezone_name = Time.zone.tzinfo.name
- expect(page.find('.user-time-preferences #user_timezone', visible: false).value).to eq(timezone_name)
+ it 'timezone defaults to empty' do
+ expect(page).to have_field(:user_timezone, with: '', type: :hidden)
end
end
end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 84a972b3027..eb32570448b 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public) }
+ let_it_be(:expiration_date) { 5.days.from_now.to_date }
let(:additional_link_attrs) { {} }
let!(:group_link) { create(:project_group_link, project: project, group: group, **additional_link_attrs) }
@@ -37,27 +38,27 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
it 'updates expiry date' do
page.within find_group_row(group) do
- fill_in 'Expiration date', with: 5.days.from_now.to_date
+ fill_in 'Expiration date', with: expiration_date
find_field('Expiration date').native.send_keys :enter
wait_for_requests
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
end
end
context 'when link has expiry date set' do
- let(:additional_link_attrs) { { expires_at: 5.days.from_now.to_date } }
+ let(:additional_link_attrs) { { expires_at: expiration_date } }
it 'clears expiry date' do
page.within find_group_row(group) do
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: expiration_date)
find('[data-testid="clear-button"]').click
wait_for_requests
- expect(page).to have_content('No expiration set')
+ expect(page).to have_field('Expiration date', with: '')
end
end
end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index 8c3646125a5..b674cad0312 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -165,6 +165,8 @@ RSpec.describe 'Project > Members > Invite group', :js do
let(:project) { create(:project) }
let!(:group) { create(:group) }
+ let_it_be(:expiration_date) { 5.days.from_now.to_date }
+
around do |example|
freeze_time { example.run }
end
@@ -176,15 +178,14 @@ RSpec.describe 'Project > Members > Invite group', :js do
visit project_project_members_path(project)
- invite_group(group.name, role: 'Guest', expires_at: 5.days.from_now)
+ invite_group(group.name, role: 'Guest', expires_at: expiration_date)
end
it 'the group link shows the expiration time with a warning class' do
setup
click_link 'Groups'
- expect(find_group_row(group)).to have_content(/in \d days/)
- expect(find_group_row(group)).to have_selector('.gl-text-orange-500')
+ expect(page).to have_field('Expiration date', with: expiration_date)
end
end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index c1b14cf60e7..830ada29a2e 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
let_it_be(:maintainer) { create(:user) }
let_it_be(:project) { create(:project) }
+ let_it_be(:three_days_from_now) { 3.days.from_now.to_date }
+ let_it_be(:five_days_from_now) { 5.days.from_now.to_date }
let(:new_member) { create(:user) }
@@ -22,39 +24,39 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
it 'expiration date is displayed in the members list' do
visit project_project_members_path(project)
- invite_member(new_member.name, role: 'Guest', expires_at: 5.days.from_now.to_date)
+ invite_member(new_member.name, role: 'Guest', expires_at: five_days_from_now)
page.within find_member_row(new_member) do
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: five_days_from_now)
end
end
it 'changes expiration date' do
- project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
+ project.team.add_users([new_member.id], :developer, expires_at: three_days_from_now)
visit project_project_members_path(project)
page.within find_member_row(new_member) do
- fill_in 'Expiration date', with: 5.days.from_now.to_date
+ fill_in 'Expiration date', with: five_days_from_now
find_field('Expiration date').native.send_keys :enter
wait_for_requests
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: five_days_from_now)
end
end
it 'clears expiration date' do
- project.team.add_users([new_member.id], :developer, expires_at: 5.days.from_now.to_date)
+ project.team.add_users([new_member.id], :developer, expires_at: five_days_from_now)
visit project_project_members_path(project)
page.within find_member_row(new_member) do
- expect(page).to have_content(/in \d days/)
+ expect(page).to have_field('Expiration date', with: five_days_from_now)
find('[data-testid="clear-button"]').click
wait_for_requests
- expect(page).to have_content('No expiration set')
+ expect(page).to have_field('Expiration date', with: '')
end
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index e629d329033..61672662fbe 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -81,6 +81,7 @@ RSpec.describe 'User page' do
context 'timezone' do
let_it_be(:timezone) { 'America/Los_Angeles' }
+ let_it_be(:local_time_selector) { '[data-testid="user-local-time"]' }
before do
travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45)
@@ -92,7 +93,19 @@ RSpec.describe 'User page' do
it 'shows local time' do
subject
- expect(page).to have_content('3:30 PM')
+ within local_time_selector do
+ expect(page).to have_content('3:30 PM')
+ end
+ end
+ end
+
+ context 'when timezone is not set' do
+ let_it_be(:user) { create(:user, timezone: nil) }
+
+ it 'does not show local time' do
+ subject
+
+ expect(page).not_to have_selector(local_time_selector)
end
end
@@ -102,7 +115,9 @@ RSpec.describe 'User page' do
it 'shows local time using the configured default timezone (UTC in this case)' do
subject
- expect(page).to have_content('10:30 PM')
+ within local_time_selector do
+ expect(page).to have_content('10:30 PM')
+ end
end
end
end
diff --git a/spec/frontend/members/components/table/expires_at_spec.js b/spec/frontend/members/components/table/expires_at_spec.js
deleted file mode 100644
index 2b8e6ab8f2a..00000000000
--- a/spec/frontend/members/components/table/expires_at_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
-import { useFakeDate } from 'helpers/fake_date';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ExpiresAt from '~/members/components/table/expires_at.vue';
-
-describe('ExpiresAt', () => {
- // March 15th, 2020
- useFakeDate(2020, 2, 15);
-
- let wrapper;
-
- const createComponent = (propsData) => {
- wrapper = mount(ExpiresAt, {
- propsData,
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).getByText(text, options));
-
- const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when no expiration date is set', () => {
- it('displays "No expiration set"', () => {
- createComponent({ date: null });
-
- expect(getByText('No expiration set').exists()).toBe(true);
- });
- });
-
- describe('when expiration date is in the past', () => {
- let expiredText;
-
- beforeEach(() => {
- createComponent({ date: '2019-03-15T00:00:00.000' });
-
- expiredText = getByText('Expired');
- });
-
- it('displays "Expired"', () => {
- expect(expiredText.exists()).toBe(true);
- expect(expiredText.classes()).toContain('gl-text-red-500');
- });
-
- it('displays tooltip with formatted date', () => {
- const tooltipDirective = getTooltipDirective(expiredText);
-
- expect(tooltipDirective).not.toBeUndefined();
- expect(expiredText.attributes('title')).toBe('Mar 15, 2019 12:00am UTC');
- });
- });
-
- describe('when expiration date is in the future', () => {
- it.each`
- date | expected | warningColor
- ${'2020-03-23T00:00:00.000'} | ${'in 8 days'} | ${false}
- ${'2020-03-20T00:00:00.000'} | ${'in 5 days'} | ${true}
- ${'2020-03-16T00:00:00.000'} | ${'in 1 day'} | ${true}
- ${'2020-03-15T05:00:00.000'} | ${'in about 5 hours'} | ${true}
- ${'2020-03-15T01:00:00.000'} | ${'in about 1 hour'} | ${true}
- ${'2020-03-15T00:30:00.000'} | ${'in 30 minutes'} | ${true}
- ${'2020-03-15T00:01:15.000'} | ${'in 1 minute'} | ${true}
- ${'2020-03-15T00:00:15.000'} | ${'in less than a minute'} | ${true}
- `('displays "$expected"', ({ date, expected, warningColor }) => {
- createComponent({ date });
-
- const expiredText = getByText(expected);
-
- expect(expiredText.exists()).toBe(true);
-
- if (warningColor) {
- expect(expiredText.classes()).toContain('gl-text-orange-500');
- } else {
- expect(expiredText.classes()).not.toContain('gl-text-orange-500');
- }
- });
- });
-});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 6885da53b26..8503d9f0fc2 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -10,7 +10,6 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
-import ExpiresAt from '~/members/components/table/expires_at.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
@@ -68,7 +67,6 @@ describe('MembersTable', () => {
stubs: [
'member-avatar',
'member-source',
- 'expires-at',
'created-at',
'member-action-buttons',
'role-dropdown',
@@ -119,7 +117,6 @@ describe('MembersTable', () => {
${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'expires'} | ${'Access expires'} | ${memberMock} | ${ExpiresAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index 2a3b07f95f2..53c1733eab9 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -10,7 +10,17 @@ describe('Timezone Dropdown', () => {
let $dropdownEl = null;
let $wrapper = null;
const tzListSel = '.dropdown-content ul li a.is-active';
- const tzDropdownToggleText = '.dropdown-toggle-text';
+
+ const initTimezoneDropdown = (options = {}) => {
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ ...options,
+ });
+ };
+
+ const findDropdownToggleText = () => $wrapper.find('.dropdown-toggle-text');
describe('Initialize', () => {
describe('with dropdown already loaded', () => {
@@ -18,16 +28,13 @@ describe('Timezone Dropdown', () => {
loadFixtures('pipeline_schedules/edit.html');
$wrapper = $('.dropdown');
$inputEl = $('#schedule_cron_timezone');
+ $inputEl.val('');
$dropdownEl = $('.js-timezone-dropdown');
-
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- });
});
it('can take an $inputEl in the constructor', () => {
+ initTimezoneDropdown();
+
const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
const tzValue = 'Asia/Colombo';
@@ -42,6 +49,8 @@ describe('Timezone Dropdown', () => {
});
it('will format data array of timezones into a list of offsets', () => {
+ initTimezoneDropdown();
+
const data = $dropdownEl.data('data');
const formatted = $wrapper.find(tzListSel).text();
@@ -50,10 +59,28 @@ describe('Timezone Dropdown', () => {
});
});
- it('will default the timezone to UTC', () => {
- const tz = $inputEl.val();
+ describe('when `allowEmpty` property is `false`', () => {
+ beforeEach(() => {
+ initTimezoneDropdown();
+ });
- expect(tz).toBe('UTC');
+ it('will default the timezone to UTC', () => {
+ const tz = $inputEl.val();
+
+ expect(tz).toBe('UTC');
+ });
+ });
+
+ describe('when `allowEmpty` property is `true`', () => {
+ beforeEach(() => {
+ initTimezoneDropdown({
+ allowEmpty: true,
+ });
+ });
+
+ it('will default the value of the input to an empty string', () => {
+ expect($inputEl.val()).toBe('');
+ });
});
});
@@ -68,23 +95,15 @@ describe('Timezone Dropdown', () => {
it('will populate the list of UTC offsets after the dropdown is loaded', () => {
expect($wrapper.find(tzListSel).length).toEqual(0);
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- });
+ initTimezoneDropdown();
expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
});
it('will call a provided handler when a new timezone is selected', () => {
const onSelectTimezone = jest.fn();
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- onSelectTimezone,
- });
+
+ initTimezoneDropdown({ onSelectTimezone });
$wrapper.find(tzListSel).first().trigger('click');
@@ -94,24 +113,15 @@ describe('Timezone Dropdown', () => {
it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
$inputEl.val('America/St_Johns');
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- displayFormat: (selectedItem) => formatTimezone(selectedItem),
- });
+ initTimezoneDropdown({ displayFormat: (selectedItem) => formatTimezone(selectedItem) });
- expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland');
+ expect(findDropdownToggleText().html()).toEqual('[UTC - 2.5] Newfoundland');
});
it('will call a provided `displayFormat` handler to format the dropdown value', () => {
const displayFormat = jest.fn();
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- displayFormat,
- });
+
+ initTimezoneDropdown({ displayFormat });
$wrapper.find(tzListSel).first().trigger('click');
diff --git a/spec/helpers/time_zone_helper_spec.rb b/spec/helpers/time_zone_helper_spec.rb
index e6cb20b5800..43ad130c4b5 100644
--- a/spec/helpers/time_zone_helper_spec.rb
+++ b/spec/helpers/time_zone_helper_spec.rb
@@ -76,6 +76,18 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45)
end
+ context 'when timezone is `nil`' do
+ it 'returns `nil`' do
+ expect(helper.local_time(nil)).to eq(nil)
+ end
+ end
+
+ context 'when timezone is blank' do
+ it 'returns `nil`' do
+ expect(helper.local_time('')).to eq(nil)
+ end
+ end
+
context 'when a valid timezone is passed' do
it 'returns local time' do
expect(helper.local_time(timezone)).to eq('3:30 PM')
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 5806f123871..d4491aacd9f 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -80,12 +80,6 @@ RSpec.describe UserPreference do
end
end
- describe '#timezone' do
- it 'returns server time as default' do
- expect(user_preference.timezone).to eq(Time.zone.tzinfo.name)
- end
- end
-
describe '#tab_width' do
it 'is set to 8 by default' do
# Intentionally not using factory here to test the constructor.
diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
similarity index 97%
rename from spec/services/bulk_import_service_spec.rb
rename to spec/services/bulk_imports/create_service_spec.rb
index 1b60a5cb0f8..2f07387451a 100644
--- a/spec/services/bulk_import_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImportService do
+RSpec.describe BulkImports::CreateService do
let(:user) { create(:user) }
let(:credentials) { { url: 'http://gitlab.example', access_token: 'token' } }
let(:params) do