Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-01 15:12:53 +00:00
parent 6c7d90ede4
commit 63fc59f6fd
35 changed files with 275 additions and 371 deletions

View File

@ -1 +1 @@
2.14.0
2.15.0

View File

@ -1,66 +0,0 @@
<script>
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import {
approximateDuration,
differenceInSeconds,
formatDate,
getDayDifference,
} from '~/lib/utils/datetime_utility';
import { DAYS_TO_EXPIRE_SOON } from '../../constants';
export default {
name: 'ExpiresAt',
components: { GlSprintf },
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
date: {
type: String,
required: false,
default: null,
},
},
computed: {
noExpirationSet() {
return this.date === null;
},
parsed() {
return new Date(this.date);
},
differenceInSeconds() {
return differenceInSeconds(new Date(), this.parsed);
},
isExpired() {
return this.differenceInSeconds <= 0;
},
inWords() {
return approximateDuration(this.differenceInSeconds);
},
formatted() {
return formatDate(this.parsed);
},
expiresSoon() {
return getDayDifference(new Date(), this.parsed) < DAYS_TO_EXPIRE_SOON;
},
cssClass() {
return {
'gl-text-red-500': this.isExpired,
'gl-text-orange-500': this.expiresSoon,
};
},
},
};
</script>
<template>
<span v-if="noExpirationSet">{{ s__('Members|No expiration set') }}</span>
<span v-else v-gl-tooltip.hover :title="formatted" :class="cssClass">
<template v-if="isExpired">{{ s__('Members|Expired') }}</template>
<gl-sprintf v-else :message="s__('Members|in %{time}')">
<template #time>
{{ inWords }}
</template>
</gl-sprintf>
</span>
</template>

View File

@ -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 {
<created-at :date="createdAt" />
</template>
<template #cell(expires)="{ item: { expiresAt } }">
<expires-at :date="expiresAt" />
</template>
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
<role-dropdown v-if="permissions.canUpdate" :permissions="permissions" :member="member" />

View File

@ -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'),

View File

@ -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]: {

View File

@ -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) {

View File

@ -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']),

View File

@ -21,6 +21,7 @@ export default class Profile {
$inputEl: this.$inputEl,
$dropdownEl: $('.js-timezone-dropdown'),
displayFormat: (selectedItem) => formatTimezone(selectedItem),
allowEmpty: true,
});
}

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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).

View File

@ -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 |

View File

@ -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.

View File

@ -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: "<your-full-group-path>",
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

View File

@ -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],

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
}
});
});
});

View File

@ -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 }) => {

View File

@ -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');

View File

@ -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')

View File

@ -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.

View File

@ -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