Commit 4219e0fc authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents b52b4279 f532e9b6
......@@ -83,6 +83,9 @@ const Api = {
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
notificationSettingsPath: '/api/:version/notification_settings',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
......@@ -906,6 +909,34 @@ const Api = {
return { data, headers };
});
},
async updateNotificationSettings(projectId, groupId, data = {}) {
let url = Api.buildUrl(this.notificationSettingsPath);
if (projectId) {
url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
} else if (groupId) {
url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
}
const result = await axios.put(url, data);
return result;
},
async getNotificationSettings(projectId, groupId) {
let url = Api.buildUrl(this.notificationSettingsPath);
if (projectId) {
url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
} else if (groupId) {
url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
}
const result = await axios.get(url);
return result;
},
};
export default Api;
<script>
import {
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
import { sprintf } from '~/locale';
import Api from '~/api';
import NotificationsDropdownItem from './notifications_dropdown_item.vue';
import { CUSTOM_LEVEL, i18n } from '../constants';
export default {
name: 'NotificationsDropdown',
components: {
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownDivider,
NotificationsDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
containerClass: {
default: '',
},
disabled: {
default: false,
},
dropdownItems: {
default: [],
},
buttonSize: {
default: 'medium',
},
initialNotificationLevel: {
default: '',
},
projectId: {
default: null,
},
groupId: {
default: null,
},
},
data() {
return {
selectedNotificationLevel: this.initialNotificationLevel,
isLoading: false,
};
},
computed: {
notificationLevels() {
return this.dropdownItems.map((level) => ({
level,
title: this.$options.i18n.notificationTitles[level] || '',
description: this.$options.i18n.notificationDescriptions[level] || '',
}));
},
isCustomNotification() {
return this.selectedNotificationLevel === CUSTOM_LEVEL;
},
buttonIcon() {
if (this.isLoading) {
return null;
}
return this.selectedNotificationLevel === 'disabled' ? 'notifications-off' : 'notifications';
},
buttonTooltip() {
const notificationTitle =
this.$options.i18n.notificationTitles[this.selectedNotificationLevel] ||
this.selectedNotificationLevel;
return this.disabled
? this.$options.i18n.notificationDescriptions.owner_disabled
: sprintf(this.$options.i18n.notificationTooltipTitle, {
notification_title: notificationTitle,
});
},
},
methods: {
selectItem(level) {
if (level !== this.selectedNotificationLevel) {
this.updateNotificationLevel(level);
}
},
async updateNotificationLevel(level) {
this.isLoading = true;
try {
await Api.updateNotificationSettings(this.projectId, this.groupId, { level });
this.selectedNotificationLevel = level;
} catch (error) {
this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
} finally {
this.isLoading = false;
}
},
},
customLevel: CUSTOM_LEVEL,
i18n,
};
</script>
<template>
<div :class="containerClass">
<gl-button-group
v-if="isCustomNotification"
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
:size="buttonSize"
>
<gl-button :size="buttonSize" :icon="buttonIcon" :loading="isLoading" :disabled="disabled" />
<gl-dropdown :size="buttonSize" :disabled="disabled">
<notifications-dropdown-item
v-for="item in notificationLevels"
:key="item.level"
:level="item.level"
:title="item.title"
:description="item.description"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
<gl-dropdown-divider />
<notifications-dropdown-item
:key="$options.customLevel"
:level="$options.customLevel"
:title="$options.i18n.notificationTitles.custom"
:description="$options.i18n.notificationDescriptions.custom"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
</gl-dropdown>
</gl-button-group>
<gl-dropdown
v-else
v-gl-tooltip="{ title: buttonTooltip }"
data-testid="notificationButton"
:icon="buttonIcon"
:loading="isLoading"
:size="buttonSize"
:disabled="disabled"
>
<notifications-dropdown-item
v-for="item in notificationLevels"
:key="item.level"
:level="item.level"
:title="item.title"
:description="item.description"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
<gl-dropdown-divider />
<notifications-dropdown-item
:key="$options.customLevel"
:level="$options.customLevel"
:title="$options.i18n.notificationTitles.custom"
:description="$options.i18n.notificationDescriptions.custom"
:notification-level="selectedNotificationLevel"
@item-selected="selectItem"
/>
</gl-dropdown>
</div>
</template>
<script>
import { GlDropdownItem } from '@gitlab/ui';
export default {
name: 'NotificationsDropdownItem',
components: {
GlDropdownItem,
},
props: {
level: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
notificationLevel: {
type: String,
required: true,
},
},
computed: {
isActive() {
return this.notificationLevel === this.level;
},
},
};
</script>
<template>
<gl-dropdown-item is-check-item :is-checked="isActive" @click="$emit('item-selected', level)">
<div class="gl-display-flex gl-flex-direction-column">
<span class="gl-font-weight-bold">{{ title }}</span>
<span class="gl-text-gray-500">{{ description }}</span>
</div>
</gl-dropdown-item>
</template>
import { __, s__ } from '~/locale';
export const CUSTOM_LEVEL = 'custom';
export const i18n = {
notificationTitles: {
participating: s__('NotificationLevel|Participate'),
mention: s__('NotificationLevel|On mention'),
watch: s__('NotificationLevel|Watch'),
global: s__('NotificationLevel|Global'),
disabled: s__('NotificationLevel|Disabled'),
custom: s__('NotificationLevel|Custom'),
},
notificationTooltipTitle: __('Notification setting - %{notification_title}'),
notificationDescriptions: {
participating: __('You will only receive notifications for threads you have participated in'),
mention: __('You will receive notifications only for comments in which you were @mentioned'),
watch: __('You will receive notifications for any activity'),
disabled: __('You will not get any notifications via email'),
global: __('Use your global notification setting'),
custom: __('You will only receive notifications for the events you choose'),
owner_disabled: __('Notifications have been disabled by the project or group owner'),
},
updateNotificationLevelErrorMessage: __(
'An error occured while updating the notification settings. Please try again.',
),
};
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotificationsDropdown from './components/notifications_dropdown.vue';
Vue.use(GlToast);
export default () => {
const el = document.querySelector('.js-vue-notification-dropdown');
if (!el) return false;
const {
containerClass,
buttonSize,
disabled,
dropdownItems,
notificationLevel,
projectId,
groupId,
} = el.dataset;
return new Vue({
el,
provide: {
containerClass,
buttonSize,
disabled: parseBoolean(disabled),
dropdownItems: JSON.parse(dropdownItems),
initialNotificationLevel: notificationLevel,
projectId,
groupId,
},
render(h) {
return h(NotificationsDropdown);
},
});
};
......@@ -12,6 +12,7 @@ import notificationsDropdown from '../../../notifications_dropdown';
import { showLearnGitLabProjectPopover } from '~/onboarding_issues';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initVueNotificationsDropdown from '~/notifications';
initReadMore();
new Star(); // eslint-disable-line no-new
......@@ -42,7 +43,14 @@ leaveByUrl('project');
showLearnGitLabProjectPopover();
notificationsDropdown();
if (gon.features?.vueNotificationDropdown) {
initVueNotificationsDropdown();
} else {
notificationsDropdown();
}
initVueNotificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
initInviteMembersTrigger();
......
......@@ -31,6 +31,10 @@ class ProjectsController < Projects::ApplicationController
# Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
before_action do
push_frontend_feature_flag(:vue_notification_dropdown, @project, default_enabled: :yaml)
end
before_action only: [:edit] do
push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:allow_editing_commit_messages, @project)
......
......@@ -125,4 +125,13 @@ module NotificationsHelper
def can_read_project?(project)
can?(current_user, :read_project, project)
end
def notification_dropdown_items(notification_setting)
NotificationSetting.levels.each_key.map do |level|
next if level == "custom"
next if level == "global" && notification_setting.source.nil?
level
end.compact
end
end
......@@ -46,6 +46,10 @@
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
- if Feature.enabled?(:vue_notification_dropdown, @project, default_enabled: :yaml)
- if @notification_setting
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, container_class: 'gl-mr-3 gl-mt-5 gl-vertical-align-top' } }
- else
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled
.count-buttons.d-inline-flex
......
---
name: vue_notification_dropdown
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52068
rollout_issue_url:
milestone: '13.8'
type: development
group: group::optimize
default_enabled: false
......@@ -60,6 +60,10 @@ boolean
booleans
Bootsnap
browsable
bugfix
bugfixed
bugfixes
bugfixing
Bugzilla
Buildkite
buildpack
......@@ -668,6 +672,7 @@ unverify
unverifying
uploader
uploaders
upstreams
upvoted
upvotes
URIs
......
......@@ -15,9 +15,9 @@ relevant compliance standards.
|Feature |GitLab tier |GitLab.com | Product level |
| ---------| :--------: | :-------: | :-----------: |
|**[Restrict SSH Keys](../security/ssh_keys_restrictions.md)**<br>Control the technology and key length of SSH keys used to access GitLab|Core+||Instance|
|**[Granular user roles and flexible permissions](../user/permissions.md)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Core+|✓|Instance, Group, Project|
|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Core+||Instance|
|**[Restrict SSH Keys](../security/ssh_keys_restrictions.md)**<br>Control the technology and key length of SSH keys used to access GitLab|Free+||Instance|
|**[Granular user roles and flexible permissions](../user/permissions.md)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Free+|✓|Instance, Group, Project|
|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Free+||Instance|
|**[Email all users of a project, group, or entire server](../tools/email.md)**<br>An admin can email groups of users based on project or group membership, or email everyone using the GitLab instance. This is great for scheduled maintenance or upgrades.|Starter+|||Instance
|**[Omnibus package supports log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-forwarding)**<br>Forward your logs to a central system.|Starter+||Instance|
|**[Lock project membership to group](../user/group/index.md#member-lock)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓|Group|
......
......@@ -17,7 +17,7 @@ GitLab has two product distributions available through [different subscriptions]
You can [install either GitLab CE or GitLab EE](https://about.gitlab.com/install/ce-or-ee/).
However, the features you have access to depend on your chosen [subscription](https://about.gitlab.com/pricing/).
GitLab Community Edition installations have access only to Core features.
GitLab Community Edition installations have access only to Free features.
Non-administrator users can't access GitLab administration tools and settings.
......
......@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Instance Review
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Core](https://about.gitlab.com/pricing/) 11.3.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Free](https://about.gitlab.com/pricing/) 11.3.
If you run a medium-sized self-managed instance (50+ users) of a free version of GitLab,
[either Community Edition or unlicensed Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/),
......
......@@ -91,7 +91,7 @@ _The artifacts are stored by default in
> [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
> - Since version 9.5, artifacts are [browsable](../ci/pipelines/job_artifacts.md#browsing-artifacts),
> when object storage is enabled. 9.4 lacks this feature.
> - Since version 10.6, available in [GitLab Core](https://about.gitlab.com/pricing/)
> - Since version 10.6, available in [GitLab Free](https://about.gitlab.com/pricing/).
> - Since version 11.0, we support `direct_upload` to S3.
If you don't want to use the local disk where GitLab is installed to store the
......
......@@ -27,7 +27,7 @@ can be started.
## Start multiple processes
> - [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4006) in GitLab 12.10, starting multiple processes with Sidekiq cluster.
> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Free](https://about.gitlab.com/pricing/) in GitLab 12.10.
> - [Sidekiq cluster became default](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4140) in GitLab 13.0.
To start multiple processes:
......@@ -113,7 +113,7 @@ you list:
## Queue selector
> - [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Free](https://about.gitlab.com/pricing/) in GitLab 12.10.
> - [Renamed from `experimental_queue_selector` to `queue_selector`](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/147) in GitLab 13.6.
In addition to selecting queues by name, as above, the `queue_selector`
......
......@@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# GitLab Dependency Proxy administration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Free](https://about.gitlab.com/pricing/) in GitLab 13.6.
GitLab can be used as a dependency proxy for a variety of common package managers.
......
......@@ -161,7 +161,8 @@ When avatar is replaced, `Upload` model is destroyed and a new one takes place w
#### CI artifacts
CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Core since **10.6**.
CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Free since
**10.6**.
#### LFS objects
......
......@@ -54,7 +54,7 @@ _The uploads are stored by default in
> **Notes:**
>
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Core](https://about.gitlab.com/pricing/) 10.7.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Free](https://about.gitlab.com/pricing/) 10.7.
> - Since version 11.1, we support direct_upload to S3.
If you don't want to use the local disk where GitLab is installed to store the
......
......@@ -153,7 +153,7 @@ cd $indexer_path && sudo make install
The `gitlab-elasticsearch-indexer` will be installed to `/usr/local/bin`.
You can change the installation path with the `PREFIX` env variable.
You can change the installation path with the `PREFIX` environment variable.
Please remember to pass the `-E` flag to `sudo` if you do so.
Example:
......@@ -218,7 +218,7 @@ The following Elasticsearch settings are available:
| Parameter | Description |
|-------------------------------------------------------|-------------|
| `Elasticsearch indexing` | Enables or disables Elasticsearch indexing and creates an empty index if one does not already exist. You may want to enable indexing but disable search in order to give the index time to be fully completed, for example. Also, keep in mind that this option doesn't have any impact on existing data, this only enables/disables the background indexer which tracks data changes and ensures new data is indexed. |
| `Pause Elasticsearch indexing` | Enables or disables temporary indexing pause. This is useful for cluster migration/reindexing. All changes are still tracked, but they are not committed to the Elasticsearch index until unpaused. |
| `Pause Elasticsearch indexing` | Enables or disables temporary indexing pause. This is useful for cluster migration/reindexing. All changes are still tracked, but they are not committed to the Elasticsearch index until resumed. |
| `Search with Elasticsearch enabled` | Enables or disables using Elasticsearch in search. |
| `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., `http://host1, https://host2:9200`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). |
| `Number of Elasticsearch shards` | Elasticsearch indexes are split into multiple shards for performance reasons. In general, larger indexes need to have more shards. Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html). |
......@@ -260,7 +260,7 @@ from the Elasticsearch index as expected.
## Enabling custom language analyzers
You can improve the language support for Chinese and Japanese languages by utilizing [smartcn](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) and/or [kuromoji](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) analysis plugins from Elastic.
You can improve the language support for Chinese and Japanese languages by utilizing [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) and/or [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) analysis plugins from Elastic.
To enable language(s) support:
......@@ -276,10 +276,10 @@ For guidance on what to install, see the following Elasticsearch language plugin
| Parameter | Description |
|-------------------------------------------------------|-------------|
| `Enable Chinese (smartcn) custom analyzer: Indexing` | Enables or disables Chinese language support using [smartcn](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) custom analyzer for newly created indices.|
| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [smartcn](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.|
| `Enable Japanese (kuromoji) custom analyzer: Indexing` | Enables or disables Japanese language support using [kuromoji](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) custom analyzer for newly created indices.|
| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [kuromoji](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.|
| `Enable Chinese (smartcn) custom analyzer: Indexing` | Enables or disables Chinese language support using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) custom analyzer for newly created indices.|
| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.|
| `Enable Japanese (kuromoji) custom analyzer: Indexing` | Enables or disables Japanese language support using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) custom analyzer for newly created indices.|
| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.|
## Disabling Advanced Search
......@@ -317,9 +317,9 @@ used by the GitLab Advanced Search integration.
In the **Admin Area > Settings > Advanced Search** section, select the
**Pause Elasticsearch Indexing** setting, and then save your change.
With this, all updates that should happen on your Elasticsearch index will be
buffered and caught up once unpaused.
buffered and caught up after resuming.
The indexing will also be automatically paused when the [**Trigger cluster reindexing**](#trigger-the-reindex-via-the-advanced-search-administration) button is used, and unpaused when the reindexing completes or aborts.
The indexing will also be automatically paused when the [**Trigger cluster reindexing**](#trigger-the-reindex-via-the-advanced-search-administration) button is used, and resumes when the reindexing completes or aborts.
### Setup
......@@ -430,7 +430,7 @@ To trigger the re-index from `primary` index:
The reindexing is now completed. Your GitLab instance is now ready to use the [automated in-cluster reindexing](#trigger-the-reindex-via-the-advanced-search-administration) feature for future reindexing.
1. Unpause the indexing
1. Resume the indexing
Under **Admin Area > Settings > Advanced Search**, uncheck the **Pause Elasticsearch Indexing** setting and save.
......@@ -448,9 +448,9 @@ After the reindexing is completed, the original index will be scheduled to be de
While the reindexing is running, you will be able to follow its progress under that same section.
### Mark the most recent reindex job as failed and unpause the indexing
### Mark the most recent reindex job as failed and resume the indexing
Sometimes, you might want to abandon the unfinished reindex job and unpause the indexing. You can achieve this via the following steps:
Sometimes, you might want to abandon the unfinished reindex job and resume the indexing. You can achieve this via the following steps:
1. Mark the most recent reindex job as failed:
......@@ -598,7 +598,7 @@ For basic guidance on choosing a cluster configuration you may refer to [Elastic
- Number of CPUs (CPU cores) per node usually corresponds to the `Number of Elasticsearch shards` setting described below.
- A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This will generally help the cluster stay in good health.
- Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB. Another consideration is the number of documents, you should aim for this simple formula for the number of shards: `number of expected documents / 5M +1`.
- `refresh_interval` is a per index setting. You may want to adjust that from default `1s` to a bigger value if you don't need data in realtime. This will change how soon you will see fresh results. If that's important for you, you should leave it as close as possible to the default value.
- `refresh_interval` is a per index setting. You may want to adjust that from default `1s` to a bigger value if you don't need data in real-time. This will change how soon you will see fresh results. If that's important for you, you should leave it as close as possible to the default value.
- You might want to raise [`indices.memory.index_buffer_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indexing-buffer.html) to 30% or 40% if you have a lot of heavy indexing operations.
### Advanced Search integration settings guidance
......@@ -993,7 +993,7 @@ Sometimes there may be issues with your Elasticsearch index data and as such
GitLab will allow you to revert to "basic search" when there are no search
results and assuming that basic search is supported in that scope. This "basic
search" will behave as though you don't have Advanced Search enabled at all for
your instance and search using other data sources (ie. PostgreSQL data and Git
your instance and search using other data sources (such as PostgreSQL data and Git
data).
### Data recovery: Elasticsearch is a secondary data store only
......
......@@ -94,7 +94,7 @@ with the **Add Panel** page:
## Duplicate a GitLab-defined dashboard
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/37238) in GitLab 12.7.
> - From [GitLab 12.8 onwards](https://gitlab.com/gitlab-org/gitlab/-/issues/39505), custom metrics are also duplicated when you duplicate a dashboard.
> - [GitLab versions 12.8 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/39505), custom metrics are also duplicated when you duplicate a dashboard.
You can save a complete copy of a GitLab-defined dashboard along with all custom metrics added to it.
The resulting `.yml` file can be customized and adapted to your project.
......@@ -128,7 +128,7 @@ any chart on a dashboard:
The options are:
- **Expand panel** - Displays a larger version of a visualization. To return to
the dashboard, click the **Back** button in your browser, or press the <kbd>Esc</kbd> key.
the dashboard, click the **Back** button in your browser, or press the <kbd>Escape</kbd> key.
([Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3100) in GitLab 13.0.)
- **View logs** **(ULTIMATE)** - Displays [Logs](../../../user/project/clusters/kubernetes_pod_logs.md),
if they are enabled. If used in conjunction with the [timeline zoom](#timeline-zoom-and-url-sharing)
......@@ -147,7 +147,7 @@ The options are:
You can use the **Timeline zoom** function at the bottom of a chart to zoom in
on a date and time of your choice. When you click and drag the sliders to select
a different beginning or end date of data to display, GitLab adds your selected start
and end times to the URL, enabling you to share specific timeframes more easily.
and end times to the URL, enabling you to share specific time frames more easily.
## Dashboard Annotations
......
......@@ -40,7 +40,7 @@ To unlock a locked user:
user.unlock_access!
```
1. Exit the console with <kbd>Ctrl</kbd>+<kbd>d</kbd>
1. Exit the console with <kbd>Control</kbd>+<kbd>d</kbd>
The user should now be able to log in.
......
......@@ -28,9 +28,13 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.1. Version Control and Git
<!-- vale gitlab.Spelling = NO -->
1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
1. [Katacoda: Learn Git Version Control using Interactive Browser-Based Scenarios](https://www.katacoda.com/courses/git)
<!-- vale gitlab.Spelling = YES -->
### 1.2. GitLab Basics
1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
......@@ -55,11 +59,14 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 1.5. Migrating from other Source Control
<!-- vale gitlab.Spelling = NO -->
1. [Migrating from Bitbucket/Stash](../user/project/import/bitbucket.md)
1. [Migrating from GitHub](../user/project/import/github.md)
1. [Migrating from SVN](../user/project/import/svn.md)
1. [Migrating from Fogbugz](../user/project/import/fogbugz.md)
<!-- vale gitlab.Spelling = YES -->
### 1.6. The GitLab team
1. [About GitLab](https://about.gitlab.com/company/)
......@@ -185,6 +192,8 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 3.9. Integrations
<!-- vale gitlab.Spelling = NO -->
1. [How to Integrate Jira and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
1. [How to Integrate Jira with GitLab](../user/project/integrations/jira.md)
1. [How to Integrate Jenkins with GitLab](../integration/jenkins.md)
......@@ -193,9 +202,11 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [How to Integrate Convox with GitLab](https://about.gitlab.com/blog/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/blog/2016/05/05/getting-started-gitlab-and-shippable/)
<!-- vale gitlab.Spelling = YES -->
## 4. External Articles
1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2011 Wall Street Journal article - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](https://cdixon.org/2014/04/13/software-eats-software-development/)
1. [2015 Venture Beat article - Actually, Open Source is Eating the World](https://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
......
......@@ -130,6 +130,10 @@ module Types
object.finding&.identifiers
end
def description
object.description || object.finding_description
end
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
......
---
title: Properly populate description in an issue created from a vulnerability
merge_request: 52619
author:
type: fixed
......@@ -29,7 +29,10 @@ RSpec.describe 'Kerberos clone instructions', :js do
it 'shows the Kerberos clone information' do
resize_screen_xs
visit_project
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy KRB5 clone URL')
end
......
......@@ -106,4 +106,44 @@ RSpec.describe GitlabSchema.types['Vulnerability'] do
end
end
end
describe '#description' do
let_it_be(:vulnerability_with_finding) { create(:vulnerability, :with_findings, project: project) }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
name
vulnerabilities {
nodes {
description
}
}
}
}
)
end
context 'when the vulnerability description field is populated' do
it 'returns the description for the vulnerability' do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(vulnerabilities.first['description']).to eq(vulnerability_with_finding.description)
end
end
context 'when the vulnerability description field is empty' do
before do
vulnerability_with_finding.description = nil
vulnerability_with_finding.save!
end
it 'returns the description for the vulnerability finding' do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(vulnerabilities.first['description']).to eq(vulnerability_with_finding.finding.description)
end
end
end
end
......@@ -3062,6 +3062,9 @@ msgstr ""
msgid "An error occured while saving changes: %{error}"
msgstr ""
msgid "An error occured while updating the notification settings. Please try again."
msgstr ""
msgid "An error occurred adding a draft to the thread."
msgstr ""
......
......@@ -37,7 +37,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows only the SSH clone information' do
resize_screen_xs
visit_project
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy SSH clone URL')
expect(page).not_to have_content('Copy HTTP clone URL')
......@@ -66,7 +69,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows only the HTTP clone information' do
resize_screen_xs
visit_project
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy HTTP clone URL')
expect(page).not_to have_content('Copy SSH clone URL')
......@@ -97,7 +103,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows both SSH and HTTP clone information' do
resize_screen_xs
visit_project
within('.js-mobile-git-clone') do
find('.dropdown-toggle').click
end
expect(page).to have_content('Copy HTTP clone URL')
expect(page).to have_content('Copy SSH clone URL')
......
......@@ -6,6 +6,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
let(:project) { create(:project, :public, :repository) }
before do
stub_feature_flags(vue_notification_dropdown: false)
sign_in(project.owner)
end
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import { GlButtonGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import httpStatus from '~/lib/utils/http_status';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue';
const mockDropdownItems = ['global', 'watch', 'participating', 'mention', 'disabled'];
const mockToastShow = jest.fn();
describe('NotificationsDropdown', () => {
let wrapper;
let mockAxios;
function createComponent(injectedProperties = {}) {
return shallowMount(NotificationsDropdown, {
stubs: {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
NotificationsDropdownItem,
},
directives: {
GlTooltip: createMockDirective(),
},
provide: {
...injectedProperties,
},
mocks: {
$toast: {
show: mockToastShow,
},
},
});
}
const findButtonGroup = () => wrapper.find(GlButtonGroup);
const findDropdown = () => wrapper.find(GlDropdown);
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
const findDropdownItemAt = (index) =>
findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
const clickDropdownItemAt = async (index) => {
const dropdownItem = findDropdownItemAt(index);
dropdownItem.vm.$emit('click');
await waitForPromises();
};
beforeEach(() => {
gon.api_version = 'v4';
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mockAxios.restore();
});
describe('template', () => {
describe('when notification level is "custom"', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'custom',
});
});
it('renders a button group', () => {
expect(findButtonGroup().exists()).toBe(true);
});
});
describe('when notification level is not "custom"', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
});
it('does not render a button group', () => {
expect(findButtonGroup().exists()).toBe(false);
});
});
describe('button tooltip', () => {
const tooltipTitlePrefix = 'Notification setting';
it.each`
level | title
${'global'} | ${'Global'}
${'watch'} | ${'Watch'}
${'participating'} | ${'Participate'}
${'mention'} | ${'On mention'}
${'disabled'} | ${'Disabled'}
${'custom'} | ${'Custom'}
`(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: level,
});
const tooltipElement = findByTestId('notificationButton');
const tooltip = getBinding(tooltipElement.element, 'gl-tooltip');
expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`);
});
});
describe('button icon', () => {
beforeEach(() => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'disabled',
});
});
it('renders the "notifications-off" icon when notification level is "disabled"', () => {
expect(findDropdown().props('icon')).toBe('notifications-off');
});
it('renders the "notifications" icon when notification level is not "disabled"', () => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
expect(findDropdown().props('icon')).toBe('notifications');
});
});
describe('dropdown items', () => {
it.each`
dropdownIndex | level | title | description
${0} | ${'global'} | ${'Global'} | ${'Use your global notification setting'}
${1} | ${'watch'} | ${'Watch'} | ${'You will receive notifications for any activity'}
${2} | ${'participating'} | ${'Participate'} | ${'You will only receive notifications for threads you have participated in'}
${3} | ${'mention'} | ${'On mention'} | ${'You will receive notifications only for comments in which you were @mentioned'}
${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'}
${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'}
`('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title);
expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe(
description,
);
});
});
});
describe('when selecting an item', () => {
beforeEach(() => {
jest.spyOn(axios, 'put');
});
it.each`
projectId | groupId | endpointUrl | endpointType | condition
${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project notifications'} | ${'a projectId is given'}
${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group notifications'} | ${'a groupId is given'}
${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global notifications'} | ${'when neither projectId nor groupId are given'}
`(
'calls the $endpointType endpoint when $condition',
async ({ projectId, groupId, endpointUrl }) => {
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
projectId,
groupId,
});
await clickDropdownItemAt(1);
expect(axios.put).toHaveBeenCalledWith(endpointUrl, {
level: 'watch',
});
},
);
it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
const dropdownItem = findDropdownItemAt(1);
await clickDropdownItemAt(1);
expect(wrapper.vm.selectedNotificationLevel).toBe('watch');
expect(dropdownItem.props('isChecked')).toBe(true);
});
it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent({
dropdownItems: mockDropdownItems,
initialNotificationLevel: 'global',
});
await clickDropdownItemAt(1);
expect(wrapper.vm.selectedNotificationLevel).toBe('global');
expect(
mockToastShow,
).toHaveBeenCalledWith(
'An error occured while updating the notification settings. Please try again.',
{ type: 'error' },
);
});
});
});
......@@ -9,6 +9,7 @@ RSpec.describe 'projects/_home_panel' do
let(:project) { create(:project) }
before do
stub_feature_flags(vue_notification_dropdown: false)
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
......
......@@ -3334,10 +3334,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^3.1.3, core-js@^3.6.4:
version "3.6.4"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647"
integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
core-js@^3.1.3, core-js@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
core-js@~2.3.0:
version "2.3.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment