Commit d42d8a84 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'master' into 30769-keys-on-protected-branch-be

parents 01fd0f78 381213cc
2eb4db13dab06b87382582f5fcddab0c8397463e
44b902da883a92ec1f19c760a083f9bf51698e41
fragment Count on InstanceStatisticsMeasurement {
count
recordedAt
}
<script>
import { __ } from '~/locale';
import { GlModal } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
......@@ -19,10 +19,28 @@ const boardDefaults = {
hide_closed_list: false,
};
const formType = {
new: 'new',
delete: 'delete',
edit: 'edit',
};
export default {
i18n: {
[formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
[formType.delete]: { title: s__('Board|Delete board'), btnText: __('Delete') },
[formType.edit]: { title: s__('Board|Edit board'), btnText: __('Save changes') },
scopeModalTitle: s__('Board|Board scope'),
cancelButtonText: __('Cancel'),
deleteErrorMessage: s__('Board|Failed to delete board. Please try again.'),
saveErrorMessage: __('Unable to save your changes. Please try again.'),
deleteConfirmationMessage: s__('Board|Are you sure you want to delete this board?'),
titleFieldLabel: __('Title'),
titleFieldPlaceholder: s__('Board|Enter board name'),
},
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
DeprecatedModal,
GlModal,
BoardConfigurationOptions,
},
props: {
......@@ -74,25 +92,16 @@ export default {
},
computed: {
isNewForm() {
return this.currentPage === 'new';
return this.currentPage === formType.new;
},
isDeleteForm() {
return this.currentPage === 'delete';
return this.currentPage === formType.delete;
},
isEditForm() {
return this.currentPage === 'edit';
},
isVisible() {
return this.currentPage !== '';
return this.currentPage === formType.edit;
},
buttonText() {
if (this.isNewForm) {
return __('Create board');
}
if (this.isDeleteForm) {
return __('Delete');
}
return __('Save changes');
return this.$options.i18n[this.currentPage].btnText;
},
buttonKind() {
if (this.isNewForm) {
......@@ -104,16 +113,11 @@ export default {
return 'info';
},
title() {
if (this.isNewForm) {
return __('Create new board');
}
if (this.isDeleteForm) {
return __('Delete board');
}
if (this.readonly) {
return __('Board scope');
return this.$options.i18n.scopeModalTitle;
}
return __('Edit board');
return this.$options.i18n[this.currentPage].title;
},
readonly() {
return !this.canAdminBoard;
......@@ -121,6 +125,24 @@ export default {
submitDisabled() {
return this.isLoading || this.board.name.length === 0;
},
primaryProps() {
return {
text: this.buttonText,
attributes: [
{
variant: this.buttonKind,
disabled: this.submitDisabled,
loading: this.isLoading,
'data-qa-selector': 'save_changes_button',
},
],
};
},
cancelProps() {
return {
text: this.$options.i18n.cancelButtonText,
};
},
},
mounted() {
this.resetFormState();
......@@ -136,10 +158,11 @@ export default {
boardsStore
.deleteBoard(this.currentBoard)
.then(() => {
this.isLoading = false;
visitUrl(boardsStore.rootPath);
})
.catch(() => {
Flash(__('Failed to delete board. Please try again.'));
Flash(this.$options.i18n.deleteErrorMessage);
this.isLoading = false;
});
} else {
......@@ -157,10 +180,11 @@ export default {
return resp.data ? resp.data : resp;
})
.then(data => {
this.isLoading = false;
visitUrl(data.board_path);
})
.catch(() => {
Flash(__('Unable to save your changes. Please try again.'));
Flash(this.$options.i18n.saveErrorMessage);
this.isLoading = false;
});
}
......@@ -181,53 +205,56 @@ export default {
</script>
<template>
<deprecated-modal
v-show="isVisible"
<gl-modal
modal-id="board-config-modal"
modal-class="board-config-modal"
content-class="gl-absolute gl-top-7"
visible
:hide-footer="readonly"
:title="title"
:primary-button-label="buttonText"
:kind="buttonKind"
:submit-disabled="submitDisabled"
modal-dialog-class="board-config-modal"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="submit"
@cancel="cancel"
@submit="submit"
@close="cancel"
@hide.prevent
>
<template #body>
<p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="gl-mb-5">
<label class="label-bold gl-font-lg" for="board-new-name">{{ __('Title') }}</label>
<input
id="board-new-name"
ref="name"
v-model="board.name"
class="form-control"
data-qa-selector="board_name_field"
type="text"
:placeholder="__('Enter board name')"
@keyup.enter="submit"
/>
</div>
<board-configuration-options
:is-new-form="isNewForm"
:board="board"
:current-board="currentBoard"
<p v-if="isDeleteForm">{{ $options.i18n.deleteConfirmationMessage }}</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="gl-mb-5">
<label class="gl-font-weight-bold gl-font-lg" for="board-new-name">
{{ $options.i18n.titleFieldLabel }}
</label>
<input
id="board-new-name"
ref="name"
v-model="board.name"
class="form-control"
data-qa-selector="board_name_field"
type="text"
:placeholder="$options.i18n.titleFieldPlaceholder"
@keyup.enter="submit"
/>
</div>
<board-scope
v-if="scopedIssueBoardFeatureEnabled"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
:weights="weights"
/>
</form>
</template>
</deprecated-modal>
<board-configuration-options
:is-new-form="isNewForm"
:board="board"
:current-board="currentBoard"
/>
<board-scope
v-if="scopedIssueBoardFeatureEnabled"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
:weights="weights"
/>
</form>
</gl-modal>
</template>
......@@ -7,6 +7,7 @@ import {
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownItem,
GlModalDirective,
} from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
......@@ -31,6 +32,9 @@ export default {
GlDropdownSectionHeader,
GlDropdownItem,
},
directives: {
GlModalDirective,
},
props: {
currentBoard: {
type: Object,
......@@ -313,6 +317,7 @@ export default {
<gl-dropdown-item
v-if="multipleIssueBoardsAvailable"
v-gl-modal-directive="'board-config-modal'"
data-qa-selector="create_new_board_button"
@click.prevent="showPage('new')"
>
......@@ -321,6 +326,7 @@ export default {
<gl-dropdown-item
v-if="showDelete"
v-gl-modal-directive="'board-config-modal'"
class="text-danger js-delete-board"
@click.prevent="showPage('delete')"
>
......
......@@ -283,10 +283,7 @@ export default {
},
created() {
this.adjustView();
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
eventHub.$on(EVT_VIEW_FILE_BY_FILE, this.fileByFileListener);
this.subscribeToEvents();
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
......@@ -307,11 +304,7 @@ export default {
},
beforeDestroy() {
diffsApp.deinstrument();
eventHub.$off(EVT_VIEW_FILE_BY_FILE, this.fileByFileListener);
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
notesEventHub.$off('fetchDiffData', this.fetchData);
this.unsubscribeFromEvents();
this.removeEventListeners();
},
methods: {
......@@ -331,6 +324,16 @@ export default {
'navigateToDiffFileIndex',
'setFileByFile',
]),
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
eventHub.$on(EVT_VIEW_FILE_BY_FILE, this.fileByFileListener);
},
unsubscribeFromEvents() {
eventHub.$off(EVT_VIEW_FILE_BY_FILE, this.fileByFileListener);
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
notesEventHub.$off('fetchDiffData', this.fetchData);
},
fileByFileListener({ setting } = {}) {
this.setFileByFile({ fileByFile: setting });
},
......
......@@ -114,7 +114,8 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
}
if (
(diffsGradualLoad && totalLoaded === pagination.total_pages) ||
(diffsGradualLoad &&
(totalLoaded === pagination.total_pages || pagination.total_pages === null)) ||
(!diffsGradualLoad && !pagination.next_page)
) {
commit(types.SET_RETRIEVING_BATCHES, false);
......
......@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
import store from '../store';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
......@@ -11,7 +10,6 @@ import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin';
export default {
store,
components: {
FrequentItemsSearchInput,
FrequentItemsList,
......
<script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
import { mapState } from 'vuex';
import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default {
components: {
Identicon,
},
mixins: [trackingMixin],
props: {
matcher: {
type: String,
......@@ -37,6 +42,7 @@ export default {
},
},
computed: {
...mapState(['dropdownType']),
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
......@@ -49,7 +55,11 @@ export default {
<template>
<li class="frequent-items-list-item-container">
<a :href="webUrl" class="clearfix">
<a
:href="webUrl"
class="clearfix"
@click="track('click_link', { label: `${dropdownType}_dropdown_frequent_items_list_item` })"
>
<div
ref="frequentItemsItemAvatarContainer"
class="frequent-items-item-avatar-container avatar-container rect-avatar s32"
......
<script>
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default {
components: {
GlIcon,
},
mixins: [frequentItemsMixin],
mixins: [frequentItemsMixin, trackingMixin],
data() {
return {
searchQuery: '',
};
},
computed: {
...mapState(['dropdownType']),
translations() {
return this.getTranslations(['searchInputPlaceholder']);
},
},
watch: {
searchQuery: debounce(function debounceSearchQuery() {
this.track('type_search_query', {
label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
});
this.setSearchQuery(this.searchQuery);
}, 500),
},
......
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import eventHub from './event_hub';
import { createStore } from '~/frequent_items/store';
Vue.use(Translate);
......@@ -28,11 +29,15 @@ export default function initFrequentItemDropdowns() {
return;
}
const dropdownType = namespace;
const store = createStore({ dropdownType });
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
store,
data() {
const { dataset } = this.$options.el;
const item = {
......
......@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
export const createStore = (initState = {}) => {
return new Vuex.Store({
actions,
getters,
mutations,
state: state(),
state: state(initState),
});
};
export default () => ({
export default ({ dropdownType = '' } = {}) => ({
namespace: '',
dropdownType,
storageKey: '',
searchQuery: '',
isLoadingItems: false,
......
......@@ -82,7 +82,7 @@ export default {
mergeInfo2() {
return this.isFork
? `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff "${this.sourceProjectPath}-${this.sourceBranch}"`
: `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff " ${this.sourceBranch}"`;
: `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff "${this.sourceBranch}"`;
},
mergeInfo3() {
return this.canMerge
......
......@@ -341,7 +341,8 @@
}
.droplab-dropdown {
.dropdown-toggle > i {
.dropdown-toggle > i,
.dropdown-toggle > svg {
pointer-events: none;
}
......
......@@ -91,6 +91,7 @@
body.modal-open {
overflow: hidden;
padding-right: 0 !important;
}
.modal-no-backdrop {
......
.notification-list-item {
line-height: 34px;
.dropdown-menu {
@extend .dropdown-menu-right;
}
......
......@@ -198,6 +198,7 @@ module ApplicationSettingsHelper
:default_project_visibility,
:default_projects_limit,
:default_snippet_visibility,
:disable_feed_token,
:disabled_oauth_sign_in_sources,
:domain_denylist,
:domain_denylist_enabled,
......
......@@ -426,6 +426,9 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm
validates :disable_feed_token,
inclusion: { in: [true, false], message: 'must be a boolean value' }
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
......
......@@ -58,6 +58,7 @@ module ApplicationSettingImplementation
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
disable_feed_token: false,
disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
domain_allowlist: Settings.gitlab['domain_allowlist'],
......
......@@ -591,13 +591,7 @@ class User < ApplicationRecord
sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query]))
search_query = if Feature.enabled?(:user_search_secondary_email)
search_with_secondary_emails(query)
else
search_without_secondary_emails(query)
end
search_query.reorder(sanitized_order_sql, :name)
search_with_secondary_emails(query).reorder(sanitized_order_sql, :name)
end
# Limits the result set to users _not_ in the given query/list of IDs.
......@@ -1659,7 +1653,7 @@ class User < ApplicationRecord
# we do this on read since migrating all existing users is not a feasible
# solution.
def feed_token
ensure_feed_token!
Gitlab::CurrentSettings.disable_feed_token ? nil : ensure_feed_token!
end
# Each existing user needs to have a `static_object_token`.
......
# frozen_string_literal: true
class CodequalityDegradationEntity < Grape::Entity
expose :description
expose :severity
expose :file_path do |degradation|
degradation.dig(:location, :path)
end
expose :line do |degradation|
degradation.dig(:location, :lines, :begin) || degradation.dig(:location, :positions, :begin, :line)
end
end
# frozen_string_literal: true
class CodequalityReportsComparerEntity < Grape::Entity
expose :status
expose :new_errors, using: CodequalityDegradationEntity
expose :resolved_errors, using: CodequalityDegradationEntity
expose :existing_errors, using: CodequalityDegradationEntity
expose :summary do
expose :total_count, as: :total
expose :resolved_count, as: :resolved
expose :errors_count, as: :errored
end
end
# frozen_string_literal: true
class CodequalityReportsComparerSerializer < BaseSerializer
entity CodequalityReportsComparerEntity
end
......@@ -36,6 +36,7 @@ module Projects
def log_response(response)
log_data = LOG_DATA_BASE.merge(
container_repository_id: @container_repository.id,
project_id: @container_repository.project_id,
message: 'deleted tags',
deleted_tags_count: response[:deleted]&.size
).compact
......
......@@ -66,4 +66,12 @@
.form-group
= f.label field_name, "#{type.upcase} SSH keys", class: 'label-bold'
= f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
.form-group
%label.label-bold= s_('AdminSettings|Feed token')
.form-check
= f.check_box :disable_feed_token, class: 'form-check-input'
= f.label :disable_feed_token, class: 'form-check-label' do
= s_('AdminSettings|Disable feed token')
= f.submit _('Save changes'), class: "gl-button btn btn-success"
......@@ -3,10 +3,10 @@
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/groups#index') do
= link_to dashboard_groups_path, class: 'qa-your-groups-link' do
= link_to dashboard_groups_path, class: 'qa-your-groups-link', data: { track_label: "groups_dropdown_your_groups", track_event: "click_link" } do
= _('Your groups')
= nav_link(path: 'groups#explore') do
= link_to explore_groups_path do
= link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do
= _('Explore groups')
.frequent-items-dropdown-content
#js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
......@@ -3,13 +3,13 @@
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path, class: 'qa-your-projects-link' do
= link_to dashboard_projects_path, class: 'qa-your-projects-link', data: { track_label: "projects_dropdown_your_projects", track_event: "click_link" } do
= _('Your projects')
= nav_link(path: 'projects#starred') do
= link_to starred_dashboard_projects_path do
= link_to starred_dashboard_projects_path, data: { track_label: "projects_dropdown_starred_projects", track_event: "click_link" } do
= _('Starred projects')
= nav_link(path: 'projects#trending') do
= link_to explore_root_path do
= link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
= _('Explore projects')
.frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
......@@ -12,5 +12,5 @@
= render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
.table-section.section-30
= form_for setting, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
= form_for setting, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications gl-display-flex' } do |f|
= f.select :notification_email, @user.public_verified_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'
......@@ -32,22 +32,23 @@
active_tokens: @active_personal_access_tokens,
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
%hr
.row.gl-mt-3
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Feed token')
%p
= s_('AccessTokens|Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
= reset_message.html_safe
- unless Gitlab::CurrentSettings.disable_feed_token
%hr
.row.gl-mt-3
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Feed token')
%p
= s_('AccessTokens|Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if incoming_email_token_enabled?
%hr
......
......@@ -5,7 +5,7 @@
.dropdown
%button.dropdown.dropdown-new.btn.gl-button.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') }
= sprite_icon('play')
= icon('caret-down')
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right
- actions.each do |action|
- next unless can?(current_user, :update_build, action)
......
......@@ -4,7 +4,7 @@
Showing
%button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }<
= pluralize(diff_files.size, "changed file")
= icon("caret-down", class: "gl-ml-2")
= sprite_icon("chevron-down", css_class: "gl-ml-2")
%span.diff-stats-additions-deletions-expanded#diff-stats
with
%strong.cgreen= pluralize(sum_added_lines, 'addition')
......
......@@ -8,7 +8,7 @@
%a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
%span.js-clone-dropdown-label
= default_clone_protocol.upcase
= icon('caret-down')
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
%li
= ssh_clone_button(container)
......
......@@ -5,7 +5,7 @@
- if @note.can_be_discussion_note?
= button_tag type: 'button', class: 'btn btn-nr dropdown-toggle btn-success js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
= icon('caret-down', class: 'toggle-icon')
= sprite_icon('chevron-down')
%ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => _('Comment'), 'close-text' => _("Comment & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Comment & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
......
......@@ -20,8 +20,8 @@
%button.dropdown-new.btn.btn-default.btn-icon.gl-button.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= sprite_icon("notifications", css_class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle.d-flex{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
%button.btn.dropdown-toggle.gl-display-flex.gl-align-items-center{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= sprite_icon('chevron-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.btn-icon.gl-button.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
......@@ -29,7 +29,7 @@
= sprite_icon("notifications", css_class: "js-notification-loading")
= notification_title(notification_setting.level)
.float-right
= icon("caret-down")
= sprite_icon("chevron-down")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
......
......@@ -5,7 +5,7 @@
.hook-test-button.dropdown.inline>
%button.btn{ 'data-toggle' => 'dropdown', class: button_class }
= _('Test')
= icon('caret-down')
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- triggers.each_value do |event|
%li
......
---
title: Convert bootstrap carets to svg chevrons
merge_request: 48492
author:
type: other
---
title: Fix typo on merge locally step
merge_request: 49330
author:
type: fixed
---
title: Update gitlab-kas to v13.7.0
merge_request: 49318
author:
type: changed
---
title: Remove user_search_secondary_email feature flag
merge_request: 49312
author:
type: changed
---
title: Disable auto admin mode on requests and views specs
merge_request: 48700
author: Diego Louzán
type: other
---
title: Add Setting to disable feed_tokens
merge_request: 48600
author:
type: added
---
title: Convert fa-caret-down icons to chevron-down SVG
merge_request: 49332
author:
type: changed
---
title: Add usage data rake tasks to prettify JSON output
merge_request: 49137
author:
type: added
---
name: user_search_secondary_email
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47587
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282137
milestone: '13.7'
type: development
group: group::access
default_enabled: false
# frozen_string_literal: true
class AddFeedTokenOffToSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :disable_feed_token, :boolean, null: false, default: false
end
end
65dcc2a53d48acc83dbfc5276e8cfc1eee5f20ffea8355d86df1f2d5b329061b
\ No newline at end of file
......@@ -9371,6 +9371,7 @@ CREATE TABLE application_settings (
encrypted_cloud_license_auth_token_iv text,
secret_detection_revocation_token_types_url text,
cloud_license_enabled boolean DEFAULT false NOT NULL,
disable_feed_token boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
......
......@@ -257,6 +257,16 @@ For more information on tuning Geo, see [Tuning Geo](replication/tuning.md).
For an example of how to set up a location-aware Git remote URL with AWS Route53, see [Location-aware Git remote URL with AWS Route53](replication/location_aware_git_url.md).
### Backfill
Once a **secondary** node is set up, it will start replicating missing data from
the **primary** node in a process known as **backfill**. You can monitor the
synchronization process on each Geo node from the **primary** node's **Geo Nodes**
dashboard in your browser.
Failures that happen during a backfill are scheduled to be retried at the end
of the backfill.
## Remove Geo node
For more information on removing a Geo node, see [Removing **secondary** Geo nodes](replication/remove_geo_node.md).
......
......@@ -425,6 +425,11 @@ GitLab you are running. GitLab versions 11.11.x or 12.0.x are affected by
To resolve the issue, upgrade to GitLab 12.1 or newer.
### Failures during backfill
During a [backfill](../index.md#backfill), failures are scheduled to be retried at the end
of the backfill queue, therefore these failures only clear up **after** the backfill completes.
### Resetting Geo **secondary** node replication
If you get a **secondary** node in a broken state and want to reset the replication state,
......
......@@ -434,11 +434,6 @@ data before running `pg_basebackup`.
NOTE:
Replication slot names must only contain lowercase letters, numbers, and the underscore character.
NOTE:
In GitLab 13.4, a seed project is added when GitLab is first installed. This makes it necessary to pass `--force` even
on a new Geo secondary node. There is an [issue to account for seed projects](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5618)
when checking the database.
When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
user in the first step.
......
......@@ -3290,6 +3290,11 @@ type ComplianceFrameworkEdge {
node: ComplianceFramework
}
"""
Identifier of ComplianceManagement::Framework
"""
scalar ComplianceManagementFrameworkID
"""
Autogenerated input type of ConfigureSast
"""
......@@ -6545,6 +6550,36 @@ type DestroyBoardPayload {
errors: [String!]!
}
"""
Autogenerated input type of DestroyComplianceFramework
"""
input DestroyComplianceFrameworkInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global ID of the compliance framework to destroy
"""
id: ComplianceManagementFrameworkID!
}
"""
Autogenerated return type of DestroyComplianceFramework
"""
type DestroyComplianceFrameworkPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Autogenerated input type of DestroyContainerRepository
"""
......@@ -14247,6 +14282,7 @@ type Mutation {
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyBoard(input: DestroyBoardInput!): DestroyBoardPayload
destroyBoardList(input: DestroyBoardListInput!): DestroyBoardListPayload
destroyComplianceFramework(input: DestroyComplianceFrameworkInput!): DestroyComplianceFrameworkPayload
destroyContainerRepository(input: DestroyContainerRepositoryInput!): DestroyContainerRepositoryPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
......
......@@ -9045,6 +9045,16 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"description": "Identifier of ComplianceManagement::Framework",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ConfigureSastInput",
......@@ -18130,6 +18140,94 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DestroyComplianceFrameworkInput",
"description": "Autogenerated input type of DestroyComplianceFramework",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global ID of the compliance framework to destroy",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DestroyComplianceFrameworkPayload",
"description": "Autogenerated return type of DestroyComplianceFramework",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DestroyContainerRepositoryInput",
......@@ -40589,6 +40687,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyComplianceFramework",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DestroyComplianceFrameworkInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DestroyComplianceFrameworkPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyContainerRepository",
"description": null,
......@@ -1099,6 +1099,15 @@ Autogenerated return type of DestroyBoard.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DestroyComplianceFrameworkPayload
Autogenerated return type of DestroyComplianceFramework.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DestroyContainerRepositoryPayload
Autogenerated return type of DestroyContainerRepository.
......
......@@ -241,6 +241,10 @@ Unlike other API endpoints, billable members is updated once per day at 12:00 UT
This function takes [pagination](README.md#pagination) parameters `page` and `per_page` to restrict the list of users.
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/262875) in GitLab 13.7, the `search` and
`sort` parameters allow you to search for billable group members by name and sort the results,
respectively.
```plaintext
GET /groups/:id/billable_members
```
......@@ -248,6 +252,21 @@ GET /groups/:id/billable_members
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `search` | string | no | A query string to search for group members by name, username, or email. |
| `sort` | string | no | A query string containing parameters that specify the sort attribute and order. See supported values below.|
The supported values for the `sort` attribute are:
| Value | Description |
| ------------------- | ------------------------ |
| `access_level_asc` | Access level, ascending |
| `access_level_desc` | Access level, descending |
| `last_joined` | Last joined |
| `name_asc` | Name, ascending |
| `name_desc` | Name, descending |
| `oldest_joined` | Oldest joined |
| `oldest_sign_in` | Oldest sign in |
| `recent_sign_in` | Recent sign in |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/billable_members"
......
......@@ -233,6 +233,7 @@ listed in the descriptions of the relevant settings.
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). |
| `disable_feed_token` | boolean | no | Disable display of RSS/Atom and calendar feed tokens ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/231493) in GitLab 13.7) |
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |
| `domain_denylist_enabled` | boolean | no | (**If enabled, requires:** `domain_denylist`) Allows blocking sign-ups from emails from specific domains. |
......
......@@ -19,9 +19,10 @@ but also across projects with multi-project pipelines.
Multi-project pipelines are useful for larger products that require cross-project inter-dependencies, such as those
adopting a [microservices architecture](https://about.gitlab.com/blog/2016/08/16/trends-in-version-control-land-microservices/).
For a demonstration of how cross-functional development teams can use cross-pipeline
triggering to trigger multiple pipelines for different microservices projects, see
[Cross-project Pipeline Triggering and Visualization](https://about.gitlab.com/handbook/marketing/product-marketing/demo/#cross-project-pipeline-triggering-and-visualization-may-2019---1110).
Cross-functional development teams can use cross-pipeline
triggering to trigger multiple pipelines for different microservices projects. Learn more
in the [Cross-project Pipeline Triggering and Visualization demo](https://about.gitlab.com/learn/)
at GitLab@learn, in the Continuous Integration (CI) section.
Additionally, it's possible to visualize the entire pipeline, including all cross-project
inter-dependencies. **(PREMIUM)**
......
......@@ -261,6 +261,18 @@ The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time
(4 - 1) + (7 - 6) => 4
```
#### How pipeline quota usage is calculated
Pipeline quota usage is calculated as the sum of the duration of each individual job. This is slightly different to how pipeline _duration_ is [calculated](#how-pipeline-duration-is-calculated). Pipeline quota usage doesn't consider any overlap of jobs running in parallel.
For example, a pipeline consists of the following jobs:
- Job A takes 3 minutes.
- Job B takes 3 minutes.
- Job C takes 2 minutes.
The pipeline quota usage is the sum of each job's duration. In this example, 8 runner minutes would be used, calculated as: 3 + 3 + 2.
### Pipeline security on protected branches
A strict security model is enforced when pipelines are executed on
......
......@@ -629,17 +629,21 @@ that are scoped to a single [configuration keyword](../ci/yaml/README.md#job-key
| Job definitions | Description |
|------------------|-------------|
| `.default-tags` | Ensures a job has the `gitlab-org` tag to ensure it's using our dedicated runners. |
| `.default-retry` | Allows a job to [retry](../ci/yaml/README.md#retry) upon `unknown_failure`, `api_failure`, `runner_system_failure`, `job_execution_timeout`, or `stuck_or_timeout_failure`. |
| `.default-before_script` | Allows a job to use a default `before_script` definition suitable for Ruby/Rails tasks that may need a database running (e.g. tests). |
| `.rails-cache` | Allows a job to use a default `cache` definition suitable for Ruby/Rails tasks. |
| `.static-analysis-cache` | Allows a job to use a default `cache` definition suitable for static analysis tasks. |
| `.coverage-cache` | Allows a job to use a default `cache` definition suitable for coverage tasks. |
| `.qa-cache` | Allows a job to use a default `cache` definition suitable for QA tasks. |
| `.yarn-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that do a `yarn install`. |
| `.assets-compile-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that compile assets. |
| `.use-pg11` | Allows a job to use the `postgres:11.6` and `redis:4.0-alpine` services. |
| `.use-pg11-ee` | Same as `.use-pg11` but also use the `docker.elastic.co/elasticsearch/elasticsearch:6.4.2` services. |
| `.use-pg11-ee` | Same as `.use-pg11` but also use the `docker.elastic.co/elasticsearch/elasticsearch:7.9.2` services. |
| `.use-pg12` | Allows a job to use the `postgres:12` and `redis:4.0-alpine` services. |
| `.use-pg12-ee` | Same as `.use-pg12` but also use the `docker.elastic.co/elasticsearch/elasticsearch:7.9.2` services. |
| `.use-kaniko` | Allows a job to use the `kaniko` tool to build Docker images. |
| `.as-if-foss` | Simulate the FOSS project by setting the `FOSS_ONLY='1'` environment variable. |
| `.use-docker-in-docker` | Allows a job to use Docker in Docker. |
### `rules`, `if:` conditions and `changes:` patterns
......@@ -656,6 +660,7 @@ and included in `rules` definitions via [YAML anchors](../ci/yaml/README.md#anch
#### `if:` conditions
<!-- vale gitlab.Substitutions = NO -->
| `if:` conditions | Description | Notes |
|------------------|-------------|-------|
| `if-not-canonical-namespace` | Matches if the project isn't in the canonical (`gitlab-org/`) or security (`gitlab-org/security`) namespace. | Use to create a job for forks (by using `when: on_success\|manual`), or **not** create a job for forks (by using `when: never`). |
......@@ -663,26 +668,45 @@ and included in `rules` definitions via [YAML anchors](../ci/yaml/README.md#anch
| `if-not-foss` | Matches if the project isn't FOSS (i.e. project name isn't `gitlab-foss`, `gitlab-ce`, or `gitlabhq`). | Use to create a job only in the EE project (by using `when: on_success|manual`), or **not** create a job if the project is FOSS (by using `when: never`). |
| `if-default-refs` | Matches if the pipeline is for `master`, `/^[\d-]+-stable(-ee)?$/` (stable branches), `/^\d+-\d+-auto-deploy-\d+$/` (auto-deploy branches), `/^security\//` (security branches), merge requests, and tags. | Note that jobs aren't created for branches with this default configuration. |
| `if-master-refs` | Matches if the current branch is `master`. | |
| `if-master-push` | Matches if the current branch is `master` and pipeline source is `push`. | |
| `if-master-schedule-2-hourly` | Matches if the current branch is `master` and pipeline runs on a 2-hourly schedule. | |
| `if-master-schedule-2-nightly` | Matches if the current branch is `master` and pipeline runs on a nightly schedule. | |
| `if-auto-deploy-branches` | Matches if the current branch is an auto-deploy one. | |
| `if-master-or-tag` | Matches if the pipeline is for the `master` branch or for a tag. | |
| `if-merge-request` | Matches if the pipeline is for a merge request. | |
| `if-merge-request-title-as-if-foss` | Matches if the pipeline is for a merge request and the MR title includes "RUN AS-IF-FOSS". | |
| `if-merge-request-title-update-caches` | Matches if the pipeline is for a merge request and the MR title includes "UPDATE CACHE". | |
| `if-merge-request-title-run-all-rspec` | Matches if the pipeline is for a merge request and the MR title includes "RUN ALL RSPEC". | |
| `if-security-merge-request` | Matches if the pipeline is for a security merge request. | |
| `if-security-schedule` | Matches if the pipeline is for a security scheduled pipeline. | |
| `if-nightly-master-schedule` | Matches if the pipeline is for a `master` scheduled pipeline with `$NIGHTLY` set. | |
| `if-dot-com-gitlab-org-schedule` | Limits jobs creation to scheduled pipelines for the `gitlab-org` group on GitLab.com. | |
| `if-dot-com-gitlab-org-master` | Limits jobs creation to the `master` branch for the `gitlab-org` group on GitLab.com. | |
| `if-dot-com-gitlab-org-merge-request` | Limits jobs creation to merge requests for the `gitlab-org` group on GitLab.com. | |
| `if-dot-com-gitlab-org-and-security-tag` | Limits job creation to tags for the `gitlab-org` and `gitlab-org/security` groups on GitLab.com. | |
| `if-dot-com-gitlab-org-and-security-merge-request` | Limit jobs creation to merge requests for the `gitlab-org` and `gitlab-org/security` groups on GitLab.com. | |
| `if-dot-com-gitlab-org-and-security-tag` | Limit jobs creation to tags for the `gitlab-org` and `gitlab-org/security` groups on GitLab.com. | |
| `if-dot-com-ee-schedule` | Limits jobs to scheduled pipelines for the `gitlab-org/gitlab` project on GitLab.com. | |
| `if-cache-credentials-schedule` | Limits jobs to scheduled pipelines with the `$CI_REPO_CACHE_CREDENTIALS` variable set. | |
| `if-rspec-fail-fast-disabled` | Limits jobs to pipelines with `$RSPEC_FAIL_FAST_ENABLED` variable not set to `"true"`. | |
| `if-rspec-fail-fast-skipped` | Matches if the pipeline is for a merge request and the MR title includes "SKIP RSPEC FAIL-FAST". | |
| `if-security-pipeline-merge-result` | Matches if the pipeline is for a security merge request triggerred by `@gitlab-release-tools-bot`. | |
<!-- vale gitlab.Substitutions = YES -->
#### `changes:` patterns
| `changes:` patterns | Description |
|------------------------------|--------------------------------------------------------------------------|
| `ci-patterns` | Only create job for CI config-related changes. |
| `yaml-patterns` | Only create job for YAML-related changes. |
| `ci-build-images-patterns` | Only create job for CI config-related changes related to the `build-images` stage. |
| `ci-review-patterns` | Only create job for CI config-related changes related to the `review` stage. |
| `ci-qa-patterns` | Only create job for CI config-related changes related to the `qa` stage. |
| `yaml-lint-patterns` | Only create job for YAML-related changes. |
| `docs-patterns` | Only create job for docs-related changes. |
| `frontend-dependency-patterns` | Only create job when frontend dependencies are updated (i.e. `package.json`, and `yarn.lock`). changes. |
| `frontend-patterns` | Only create job for frontend-related changes. |
| `backend-patterns` | Only create job for backend-related changes. |
| `db-patterns` | Only create job for DB-related changes. |
| `backstage-patterns` | Only create job for backstage-related changes (i.e. Danger, fixtures, RuboCop, specs). |
| `code-patterns` | Only create job for code-related changes. |
| `qa-patterns` | Only create job for QA-related changes. |
......
......@@ -276,8 +276,13 @@ In its current state, the Rake task:
This uses some features from `graphql-docs` gem like its schema parser and helper methods.
The docs generator code comes from our side giving us more flexibility, like using Haml templates and generating Markdown files.
To edit the template used, please take a look at `lib/gitlab/graphql/docs/templates/default.md.haml`.
The actual renderer is at `Gitlab::Graphql::Docs::Renderer`.
To edit the content, you may need to edit the following:
- The template. You can edit the template at `lib/gitlab/graphql/docs/templates/default.md.haml`.
The actual renderer is at `Gitlab::Graphql::Docs::Renderer`.
- The applicable `description` field in the code, which
[Updates machine-readable schema files](#update-machine-readable-schema-files),
which is then used by the `rake` task described earlier.
`@parsed_schema` is an instance variable that the `graphql-docs` gem expects to have available.
`Gitlab::Graphql::Docs::Helper` defines the `object` method we currently use. This is also where you
......
......@@ -372,8 +372,10 @@ end
[See this merge request for a real example of adding a custom matcher](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46302).
We are creating custom negatable matchers in `qa/spec/support/matchers`.
NOTE:
We need to create custom negatable matchers only for the predicate methods we've added to the test framework, and only if we're using `not_to`. If we use `to have_no_*` a negatable matcher is not necessary.
We need to create custom negatable matchers only for the predicate methods we've added to the test framework, and only if we're using `not_to`. If we use `to have_no_*` a negatable matcher is not necessary but it increases code readability.
### Why we need negatable matchers
......
......@@ -441,6 +441,22 @@ 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
Sometimes, you might want to abandon the unfinished reindex job and unpause the indexing. You can achieve this via the following steps:
1. Mark the most recent reindex job as failed:
```shell
# Omnibus installations
sudo gitlab-rake gitlab:elastic:mark_reindex_failed
# Installations from source
bundle exec rake gitlab:elastic:mark_reindex_failed RAILS_ENV=production
```
1. Uncheck the "Pause Elasticsearch indexing" checkbox in **Admin Area > Settings > General > Advanced Search**.
## Background migrations
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6.
......@@ -511,7 +527,8 @@ The following are some available Rake tasks:
| [`sudo gitlab-rake gitlab:elastic:recreate_index[<TARGET_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:delete_index[<TARGET_NAME>]` and `gitlab:elastic:create_empty_index[<TARGET_NAME>]`. |
| [`sudo gitlab-rake gitlab:elastic:index_snippets`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Performs an Elasticsearch import that indexes the snippets data. |
| [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Displays which projects are not indexed. |
| [`sudo gitlab-rake gitlab:elastic:reindex_cluster`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Schedules a zero-downtime cluster reindexing task. This feature should be used with an index that was created after GitLab 13.0. |
| [`sudo gitlab-rake gitlab:elastic:reindex_cluster`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Schedules a zero-downtime cluster reindexing task. This feature should be used with an index that was created after GitLab 13.0. |
| [`sudo gitlab-rake gitlab:elastic:mark_reindex_failed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)`] | Mark the most recent re-index job as failed. |
NOTE:
The `TARGET_NAME` parameter is optional and will use the default index/alias name from the current `RAILS_ENV` if not set.
......@@ -789,7 +806,7 @@ There are a couple of ways to achieve that:
This is always correctly identifying whether the current project/namespace
being searched is using Elasticsearch.
- From the admin area under **Settings > General > Elasticsearch** check that the
- From the admin area under **Settings > General > Advanced Search** check that the
Advanced Search settings are checked.
Those same settings there can be obtained from the Rails console if necessary:
......
......@@ -42,6 +42,7 @@ The following are available Rake tasks:
| [Repository storage](../administration/raketasks/storage.md) | List and migrate existing projects and attachments from legacy storage to hashed storage. |
| [Uploads migrate](../administration/raketasks/uploads/migrate.md) | Migrate uploads between storage local and object storage. |
| [Uploads sanitize](../administration/raketasks/uploads/sanitize.md) | Remove EXIF data from images uploaded to earlier versions of GitLab. |
| [Usage data](../administration/troubleshooting/gitlab_rails_cheat_sheet.md#generate-usage-ping) | Generate and troubleshoot [Usage Ping](../development/product_analytics/usage_ping.md).|
| [User management](user_management.md) | Perform user management tasks. |
| [Webhooks administration](web_hooks.md) | Maintain project Webhooks. |
| [X.509 signatures](x509_signatures.md) | Update X.509 commit signatures, useful if certificate store has changed. |
......@@ -13,12 +13,12 @@ Notifications are sent via email.
## Receiving notifications
You will receive notifications for one of the following reasons:
You receive notifications for one of the following reasons:
- You participate in an issue, merge request, epic or design. In this context, _participate_ means comment, or edit.
- You enable notifications in an issue, merge request, or epic. To enable notifications, click the **Notifications** toggle in the sidebar to _on_.
While notifications are enabled, you will receive notification of actions occurring in that issue, merge request, or epic.
While notifications are enabled, you receive notification of actions occurring in that issue, merge request, or epic.
NOTE:
Notifications can be blocked by an admin, preventing them from being sent.
......@@ -50,7 +50,7 @@ These notification settings apply only to you. They do not affect the notificati
Your **Global notification settings** are the default settings unless you select different values for a project or a group.
- Notification email
- This is the email address your notifications will be sent to.
- This is the email address your notifications are sent to.
- Global notification level
- This is the default [notification level](#notification-levels) which applies to all your notifications.
- Receive notifications about your own activity.
......@@ -138,7 +138,7 @@ For each project and group you can select one of the following levels:
## Notification events
Users will be notified of the following events:
Users are notified of the following events:
| Event | Sent to | Settings level |
|------------------------------|---------------------|------------------------------|
......@@ -158,7 +158,7 @@ Users will be notified of the following events:
## Issue / Epics / Merge request events
In most of the below cases, the notification will be sent to:
In most of the below cases, the notification is sent to:
- Participants:
- the author and assignee of the issue/merge request
......@@ -193,23 +193,23 @@ To minimize the number of notifications that do not require any action, from [Gi
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The author of the pipeline |
| Fixed pipeline | The author of the pipeline. Enabled by default. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24309) in GitLab 13.1. |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set. If the pipeline failed previously, a `Fixed pipeline` message will be sent for the first successful pipeline after the failure, then a `Successful pipeline` message for any further successful pipelines. |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set. If the pipeline failed previously, a `Fixed pipeline` message is sent for the first successful pipeline after the failure, then a `Successful pipeline` message for any further successful pipelines. |
| New epic **(ULTIMATE)** | |
| Close epic **(ULTIMATE)** | |
| Reopen epic **(ULTIMATE)** | |
In addition, if the title or description of an Issue or Merge Request is
changed, notifications will be sent to any **new** mentions by `@username` as
changed, notifications are sent to any **new** mentions by `@username` as
if they had been mentioned in the original text.
You won't receive notifications for Issues, Merge Requests or Milestones created
by yourself (except when an issue is due). You will only receive automatic
You don't receive notifications for Issues, Merge Requests or Milestones created
by yourself (except when an issue is due). You only receive automatic
notifications when somebody else comments or adds changes to the ones that
you've created or mentions you.
If an open merge request becomes unmergeable due to conflict, its author will be notified about the cause.
If an open merge request becomes unmergeable due to conflict, its author is notified about the cause.
If a user has also set the merge request to automatically merge once pipeline succeeds,
then that user will also be notified.
then that user is also notified.
## Design email notifications
......@@ -252,7 +252,7 @@ The `X-GitLab-NotificationReason` header contains the reason for the notificatio
- `mentioned`
The reason for the notification is also included in the footer of the notification email. For example an email with the
reason `assigned` will have this sentence in the footer:
reason `assigned` has this sentence in the footer:
- `You are receiving this email because you have been assigned an item on <configured GitLab hostname>.`
......
......@@ -21,7 +21,7 @@ export const DEVOPS_ADOPTION_STRINGS = {
),
tableHeader: {
text: s__(
'DevopsAdoption|Feature adoption is based on usage over the last 30 days. Last updated: %{timestamp}.',
'DevopsAdoption|Feature adoption is based on usage in the last calendar month. Last updated: %{timestamp}.',
),
button: s__('DevopsAdoption|Add new segment'),
},
......
import Vue from 'vue';
import { GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
export default boardsStore => {
......@@ -8,8 +8,12 @@ export default boardsStore => {
if (configEl) {
gl.boardConfigToggle = new Vue({
el: configEl,
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
data() {
return {
......@@ -31,17 +35,16 @@ export default boardsStore => {
},
template: `
<div class="gl-ml-3">
<button
<gl-button
v-gl-modal-directive="'board-config-modal'"
v-gl-tooltip
:title="tooltipTitle"
class="btn btn-inverted"
:class="{ 'dot-highlight': hasScope }"
type="button"
data-qa-selector="boards_config_button"
@click.prevent="showPage('edit')"
>
{{ buttonText }}
</button>
</gl-button>
</div>
`,
});
......
<script>
import { GlLink, GlEmptyState } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlLink,
GlEmptyState,
},
props: {
imagePath: {
type: String,
......@@ -9,27 +14,24 @@ export default {
},
},
strings: {
heading: __(
"Merge requests are a place to propose changes you've made to a project and discuss those changes with others",
heading: __("A merge request hasn't yet been merged"),
subheading: __(
"The Compliance Dashboard gives you the ability to see a group's merge request activity by providing a high-level view for all projects in the group.",
),
subheading: __('Interested parties can even contribute by pushing commits if they want to.'),
alt: __('Merge Requests'),
documentation: __('View documentation'),
},
documentationPath: 'https://docs.gitlab.com/ee/user/compliance/compliance_dashboard/index.html',
};
</script>
<template>
<div class="row empty-state merge-requests">
<div class="col-12">
<div class="svg-content">
<img :src="imagePath" :alt="$options.strings.alt" />
</div>
</div>
<div class="col-12">
<div class="text-content">
<h4>{{ $options.strings.heading }}</h4>
<p>{{ $options.strings.subheading }}</p>
</div>
</div>
</div>
<gl-empty-state
:title="$options.strings.heading"
:description="$options.strings.subheading"
:svg-path="imagePath"
>
<template #actions>
<gl-link :href="$options.documentationPath">{{ $options.strings.documentation }}</gl-link>
</template>
</gl-empty-state>
</template>
......@@ -9,6 +9,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { getFormattedTimezone } from '../utils/common_utils';
export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
......@@ -90,7 +91,7 @@ export default {
},
methods: {
getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`);
return getFormattedTimezone(tz);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
......
......@@ -2,8 +2,10 @@
import { isEmpty } from 'lodash';
import { GlModal, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql';
import AddEditScheduleForm from './add_edit_schedule_form.vue';
import { updateStoreOnScheduleCreate } from '../utils/cache_updates';
export const i18n = {
cancel: __('Cancel'),
......@@ -65,23 +67,35 @@ export default {
methods: {
createSchedule() {
this.loading = true;
const { projectPath } = this;
this.$apollo
.mutate({
mutation: createOncallScheduleMutation,
variables: {
oncallScheduleCreateInput: {
projectPath: this.projectPath,
projectPath,
...this.form,
timezone: this.form.timezone.identifier,
},
},
update(
store,
{
data: { oncallScheduleCreate },
},
) {
updateStoreOnScheduleCreate(store, getOncallSchedulesQuery, oncallScheduleCreate, {
projectPath,
});
},
})
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.createScheduleModal.hide();
this.$emit('scheduleCreated');
})
.catch(error => {
this.error = error;
......
......@@ -47,14 +47,17 @@ export default {
},
methods: {
deleteSchedule() {
const { projectPath } = this;
const {
projectPath,
schedule: { iid },
} = this;
this.loading = true;
this.$apollo
.mutate({
mutation: destroyOncallScheduleMutation,
variables: {
id: this.schedule.id,
iid,
projectPath,
},
update(store, { data }) {
......@@ -87,6 +90,7 @@ export default {
ref="deleteScheduleModal"
modal-id="deleteScheduleModal"
size="sm"
:data-testid="`delete-schedule-modal-${schedule.iid}`"
:title="$options.i18n.deleteSchedule"
:action-primary="primaryProps"
:action-cancel="cancelProps"
......
......@@ -6,10 +6,9 @@ import DeleteScheduleModal from './delete_schedule_modal.vue';
import EditScheduleModal from './edit_schedule_modal.vue';
import { getTimeframeForWeeksView } from './schedule/utils';
import { PRESET_TYPES } from './schedule/constants';
import { getFormattedTimezone } from '../utils';
import { getFormattedTimezone } from '../utils/common_utils';
export const i18n = {
title: s__('OnCallSchedules|On-call schedule'),
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'),
updateScheduleLabel: s__('OnCallSchedules|Edit schedule'),
destroyScheduleLabel: s__('OnCallSchedules|Delete schedule'),
......@@ -51,7 +50,6 @@ export default {
<template>
<div>
<h2>{{ $options.i18n.title }}</h2>
<gl-card>
<template #header>
<div class="gl-display-flex gl-justify-content-space-between gl-m-0">
......
<script>
import { GlEmptyState, GlButton, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import AddScheduleModal from './add_schedule_modal.vue';
import OncallSchedule from './oncall_schedule.vue';
......@@ -10,11 +10,18 @@ import { fetchPolicies } from '~/lib/graphql';
const addScheduleModalId = 'addScheduleModal';
export const i18n = {
title: s__('OnCallSchedules|On-call schedule'),
emptyState: {
title: s__('OnCallSchedules|Create on-call schedules in GitLab'),
description: s__('OnCallSchedules|Route alerts directly to specific members of your team'),
button: s__('OnCallSchedules|Add a schedule'),
},
successNotification: {
title: s__('OnCallSchedules|Try adding a rotation'),
description: s__(
'OnCallSchedules|Your schedule has been successfully created and all alerts from this project will now be routed to this schedule. Currently, only one schedule can be created per project. More coming soon! To add individual users to this schedule, use the add a rotation button.',
),
},
};
export default {
......@@ -22,8 +29,9 @@ export default {
addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath', 'projectPath'],
components: {
GlEmptyState,
GlAlert,
GlButton,
GlEmptyState,
GlLoadingIcon,
AddScheduleModal,
OncallSchedule,
......@@ -34,6 +42,7 @@ export default {
data() {
return {
schedule: {},
showSuccessNotification: false,
};
},
apollo: {
......@@ -46,7 +55,8 @@ export default {
};
},
update(data) {
return data?.project?.incidentManagementOncallSchedules?.nodes?.[0] ?? null;
const nodes = data.project?.incidentManagementOncallSchedules?.nodes ?? [];
return nodes.length ? nodes[nodes.length - 1] : null;
},
error(error) {
Sentry.captureException(error);
......@@ -64,7 +74,21 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
<oncall-schedule v-else-if="schedule" :schedule="schedule" />
<template v-else-if="schedule">
<h2>{{ $options.i18n.title }}</h2>
<gl-alert
v-if="showSuccessNotification"
variant="tip"
:title="$options.i18n.successNotification.title"
class="gl-my-3"
@dismiss="showSuccessNotification = false"
>
{{ $options.i18n.successNotification.description }}
</gl-alert>
<oncall-schedule :schedule="schedule" />
</template>
<gl-empty-state
v-else
:title="$options.i18n.emptyState.title"
......@@ -77,6 +101,9 @@ export default {
</gl-button>
</template>
</gl-empty-state>
<add-schedule-modal :modal-id="$options.addScheduleModalId" />
<add-schedule-modal
:modal-id="$options.addScheduleModalId"
@scheduleCreated="showSuccessNotification = true"
/>
</div>
</template>
mutation oncallScheduleDestroy($oncallScheduleDestroyInput: OncallScheduleDestroyInput!) {
oncallScheduleDestroy(input: $oncallScheduleDestroyInput) {
mutation oncallScheduleDestroy($iid: String!, $projectPath: ID!) {
oncallScheduleDestroy(input: { iid: $iid, projectPath: $projectPath }) {
errors
oncallSchedule {
iid
......
......@@ -3,6 +3,27 @@ import createFlash from '~/flash';
import { DELETE_SCHEDULE_ERROR, UPDATE_SCHEDULE_ERROR } from './error_messages';
const addScheduleToStore = (store, query, { oncallSchedule: schedule }, variables) => {
if (!schedule) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, draftData => {
draftData.project.incidentManagementOncallSchedules.nodes.push(schedule);
});
store.writeQuery({
query,
variables,
data,
});
};
const deleteScheduleFromStore = (store, query, { oncallScheduleDestroy }, variables) => {
const schedule = oncallScheduleDestroy?.oncallSchedule;
if (!schedule) {
......@@ -61,6 +82,12 @@ const onError = (data, message) => {
export const hasErrors = ({ errors = [] }) => errors?.length;
export const updateStoreOnScheduleCreate = (store, query, data, variables) => {
if (!hasErrors(data)) {
addScheduleToStore(store, query, data, variables);
}
};
export const updateStoreAfterScheduleDelete = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, DELETE_SCHEDULE_ERROR);
......
......@@ -11,7 +11,7 @@ import { sprintf, __ } from '~/locale';
* @returns {String}
*/
export const getFormattedTimezone = tz => {
return sprintf(__('(UTC%{offset}) %{timezone}'), {
return sprintf(__('(UTC %{offset}) %{timezone}'), {
offset: tz.formatted_offset,
timezone: `${tz.abbr} ${tz.name}`,
});
......
import initLDAPGroupsSelect from 'ee/ldap_groups_select';
import initLDAPGroupLinks from 'ee/groups/ldap_group_links';
import { pipelineMinutes } from '../../users/pipeline_minutes';
initLDAPGroupsSelect();
initLDAPGroupLinks();
pipelineMinutes();
<script>
/**
* project_with_excess_storage.vue component is rendered behind
* `additional_repo_storage_by_namespace` feature flag. The component
* looks similar to project.vue component so that once the flag is
* lifted this component could replace and be used mainstream.
*/
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
......
......@@ -76,7 +76,7 @@
}
}
.board-config-modal {
.board-config-modal .modal-dialog {
width: 440px;
.block {
......
......@@ -10,6 +10,7 @@ module EE
mount_mutation ::Mutations::Clusters::Agents::Delete
mount_mutation ::Mutations::Clusters::AgentTokens::Create
mount_mutation ::Mutations::Clusters::AgentTokens::Delete
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Destroy
mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic
......
# frozen_string_literal: true
module Mutations
module ComplianceManagement
module Frameworks
class Destroy < ::Mutations::BaseMutation
graphql_name 'DestroyComplianceFramework'
authorize :manage_compliance_framework
argument :id,
::Types::GlobalIDType[::ComplianceManagement::Framework],
required: true,
description: 'The global ID of the compliance framework to destroy'
def resolve(id:)
framework = authorized_find!(id: id)
result = ::ComplianceManagement::Frameworks::DestroyService.new(framework: framework, current_user: current_user).execute
{ errors: result.success? ? [] : Array.wrap(result.message) }
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::ComplianceManagement::Framework)
end
end
end
end
end
......@@ -79,6 +79,21 @@ module BillingPlansHelper
_('Seats usage data is updated every day at 12:00pm UTC')
end
def upgrade_button_css_classes(namespace, plan, is_current_plan)
css_classes = %w[btn btn-success gl-button]
css_classes << 'disabled' if is_current_plan && !namespace.trial_active?
css_classes << 'invisible' if plan.deprecated?
css_classes.join(' ')
end
def available_plans(plans_data, current_plan)
return plans_data unless ::Feature.enabled?(:hide_deprecated_billing_plans)
plans_data.filter { |plan_data| !plan_data.deprecated? || plan_data.code == current_plan&.code }
end
private
def plan_purchase_url(group, plan)
......
......@@ -367,7 +367,6 @@ module EE
def additional_repo_storage_by_namespace_enabled?
!::Feature.enabled?(:namespace_storage_limit, self) &&
::Feature.enabled?(:additional_repo_storage_by_namespace, self) &&
::Gitlab::CurrentSettings.automatic_purchased_storage_allocation?
end
......
......@@ -5,7 +5,7 @@ module ComplianceManagement
delegate { @subject.namespace }
condition(:custom_compliance_frameworks_enabled) do
License.feature_available?(:custom_compliance_frameworks)
License.feature_available?(:custom_compliance_frameworks) && Feature.enabled?(:ff_custom_compliance_frameworks)
end
rule { can?(:owner_access) & custom_compliance_frameworks_enabled }.policy do
......
- purchase_link = plan.purchase_link
- plan_name = plan.name
- show_deprecated_plan = ::Feature.enabled?(:hide_deprecated_billing_plans) && plan.deprecated?
- if show_deprecated_plan
- plan_name += ' (Legacy)'
- faq_link_url = 'https://about.gitlab.com/gitlab-com/#faq'
- faq_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: faq_link_url }
.card.h-100{ class: ("card-active" if is_current) }
.card.h-100{ class: ("card-active" if is_current || show_deprecated_plan) }
.card-header.gl-font-weight-bold.d-flex.flex-row.justify-content-between.flex-wrap
%div
= plan.name
= plan_name
- if is_current
.text-muted
= _("Current Plan")
.card-body
.price-per-month
.gl-mr-2
= number_to_plan_currency(plan.price_per_month)
- if show_deprecated_plan
= s_("The %{plan_name} is no longer available to purchase. For more information about how this will impact you, check our %{faq_link_start}frequently asked questions%{faq_link_end}.").html_safe % { plan_name: plan.name, faq_link_start: faq_link_start, faq_link_end: '</a>'.html_safe }
- else
.price-per-month
.gl-mr-2
= number_to_plan_currency(plan.price_per_month)
%ul.conditions.gl-p-0.gl-my-auto
%li= s_("BillingPlans|per user")
%li= s_("BillingPlans|monthly")
.price-per-year.text-left{ class: ("invisible" unless plan.price_per_year > 0) }
- price_per_year = number_to_plan_currency(plan.price_per_year)
= s_("BillingPlans|billed annually at %{price_per_year}") % { price_per_year: price_per_year }
%ul.conditions.gl-p-0.gl-my-auto
%li= s_("BillingPlans|per user")
%li= s_("BillingPlans|monthly")
.price-per-year.text-left{ class: ("invisible" unless plan.price_per_year > 0) }
- price_per_year = number_to_plan_currency(plan.price_per_year)
= s_("BillingPlans|billed annually at %{price_per_year}") % { price_per_year: price_per_year }
%hr.gl-my-3
%hr.gl-my-3
%ul.unstyled-list
- plan_feature_short_list(plan).each do |feature|
- feature_class = "gl-p-0!"
- if feature.highlight
- feature_class += " gl-font-weight-bold"
%li{ class: "#{feature_class}" }
= feature.title
%li.gl-p-0.gl-pt-3
- if plan.about_page_href
= link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, EE::SUBSCRIPTIONS_COMPARISON_URL
%ul.unstyled-list
- plan_feature_short_list(plan).each do |feature|
- feature_class = "gl-p-0!"
- if feature.highlight
- feature_class += " gl-font-weight-bold"
%li{ class: "#{feature_class}" }
= feature.title
%li.gl-p-0.gl-pt-3
- if plan.about_page_href
= link_to s_("BillingPlans|See all %{plan_name} features") % { plan_name: plan.name }, EE::SUBSCRIPTIONS_COMPARISON_URL
- if purchase_link
.card-footer
.float-right{ class: ("invisible" unless purchase_link.action == 'upgrade' || is_current) }
- if show_contact_sales_button?(purchase_link.action)
= link_to s_('BillingPlan|Contact sales'), "#{contact_sales_url}?test=inappcontactsales#{plan.code}", class: "btn btn-success-secondary gl-button", data: { **experiment_tracking_data_for_button_click('contact_sales') }
- upgrade_button_class = "disabled" if is_current && !namespace.trial_active?
- cta_class = '-new' if use_new_purchase_flow?(namespace)
= link_to s_('BillingPlan|Upgrade'), plan_purchase_or_upgrade_url(namespace, plan), class: "btn btn-success gl-button #{upgrade_button_class} billing-cta-purchase#{cta_class}", data: { **experiment_tracking_data_for_button_click('upgrade') }
- upgrade_button_classes = upgrade_button_css_classes(namespace, plan, is_current)
= link_to s_('BillingPlan|Upgrade'), plan_purchase_or_upgrade_url(namespace, plan), class: "#{upgrade_button_classes} billing-cta-purchase#{cta_class}", data: { **experiment_tracking_data_for_button_click('upgrade') }
......@@ -5,8 +5,10 @@
= render 'shared/billings/billing_plan_header', namespace: namespace, plan: current_plan
- if show_plans?(namespace)
.billing-plans.gl-mt-5.row
- plans_data.each do |plan|
- plans = available_plans(plans_data, current_plan)
.billing-plans.gl-mt-5.row.justify-content-center
- plans.each do |plan|
- is_default_plan = current_plan.nil? && plan.default?
- is_current = plan.code == current_plan&.code || is_default_plan
.col-md-6.col-lg-3.gl-mb-5
......
---
title: Improve compliance dashboard empty state message
merge_request: 45273
author:
type: changed
---
title: Remove the additional_repo_storage_by_namespace feature flag
merge_request: 49055
author:
type: other
---
title: Add GraphQL mutation to destroy compliance framework
merge_request: 48912
author:
type: added
---
title: Create a rake command to mark reindex job failed
merge_request: 48938
author:
type: added
---
title: Only run fuzzing on commit events, not all events
merge_request: 48264
author:
type: changed
---
name: additional_repo_storage_by_namespace
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43188
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255166
milestone: '13.5'
type: development
group: group::fulfillment
default_enabled: false
---
name: hide_deprecated_billing_plans
introduced_by_url:
rollout_issue_url:
milestone: '13.7'
type: development
group: group::purchase
default_enabled: false
......@@ -7,6 +7,14 @@ module EE
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
class << self
include ::SortingHelper
def member_sort_options
member_sort_options_hash.keys
end
end
prepended do
params :optional_filter_params_ee do
optional :with_saml_identity, type: Grape::API::Boolean, desc: "List only members with linked SAML identity"
......@@ -76,14 +84,11 @@ module EE
).for_member(member).security_event
end
def paginate_billable_from_user_ids(user_ids)
paginated = paginate(::Kaminari.paginate_array(user_ids.sort))
users_as_hash = ::User.id_in(paginated).index_by(&:id)
def billed_users_for(group, search_term, order_by)
users = ::User.id_in(group.billed_user_ids)
users = users.search(search_term) if search_term
# map! ensures same paginatable array is manipulated
# instead of creating a new non-paginatable array
paginated.map! { |user_id| users_as_hash[user_id] }
users.sort_by_attribute(order_by || 'name_asc')
end
end
end
......
......@@ -47,6 +47,8 @@ module EE
end
params do
use :pagination
optional :search, type: String, desc: 'The exact name of the subscribed member'
optional :sort, type: String, desc: 'The sorting option', values: Helpers::MembersHelpers.member_sort_options
end
get ":id/billable_members" do
group = find_group!(params[:id])
......@@ -56,7 +58,8 @@ module EE
bad_request!(nil) if group.subgroup?
bad_request!(nil) unless ::Ability.allowed?(current_user, :admin_group_member, group)
users = paginate_billable_from_user_ids(group.billed_user_ids)
sorting = params[:sort] || 'id_asc'
users = paginate(billed_users_for(group, params[:search], sorting))
present users, with: ::API::Entities::UserBasic, current_user: current_user
end
......
......@@ -114,6 +114,16 @@ namespace :gitlab do
end
end
desc "GitLab | Elasticsearch | Mark last reindexing job as failed"
task mark_reindex_failed: :environment do
if Elastic::ReindexingTask.running?
Elastic::ReindexingTask.current.failure!
puts 'Marked the current reindexing job as failed.'.color(:green)
else
puts 'Did not find the current running reindexing job.'
end
end
def project_id_batches(&blk)
relation = Project.all
......
......@@ -21,32 +21,39 @@ RSpec.describe ProjectsController do
subject { get :show, params: { namespace_id: public_project.namespace.path, id: public_project.path } }
context 'additional repo storage by namespace' do
using RSpec::Parameterized::TableSyntax
let(:namespace) { public_project.namespace }
where(:automatic_purchased_storage_allocation, :additional_repo_storage_by_namespace, :expected_to_render) do
true | true | true
true | false | false
false | true | false
false | false | false
before do
allow_next_instance_of(EE::Namespace::RootExcessStorageSize) do |root_storage|
allow(root_storage).to receive(:usage_ratio).and_return(0.5)
allow(root_storage).to receive(:above_size_limit?).and_return(true)
end
stub_feature_flags(namespace_storage_limit: false)
namespace.add_owner(user)
end
with_them do
context 'when automatic_purchased_storage_allocation setting is enabled' do
before do
allow_next_instance_of(EE::Namespace::RootExcessStorageSize) do |root_storage|
allow(root_storage).to receive(:usage_ratio).and_return(0.5)
allow(root_storage).to receive(:above_size_limit?).and_return(true)
end
stub_application_setting(automatic_purchased_storage_allocation: automatic_purchased_storage_allocation)
stub_feature_flags(additional_repo_storage_by_namespace: additional_repo_storage_by_namespace, namespace_storage_limit: false)
stub_application_setting(automatic_purchased_storage_allocation: true)
end
namespace.add_owner(user)
it 'includes the CTA for additional purchased storage' do
subject
expect(response.body).to match(/Please purchase additional storage/)
end
end
context 'when automatic_purchased_storage_allocation setting is disabled' do
before do
stub_application_setting(automatic_purchased_storage_allocation: false)
end
it do
it 'does not include the CTA for additional purchased storage' do
subject
expect(response.body.include?("Please purchase additional storage")).to eq(expected_to_render)
expect(response.body).not_to match(/Please purchase additional storage/)
end
end
end
......
......@@ -18,6 +18,7 @@ RSpec.describe 'Billing plan pages', :feature do
end
before do
stub_feature_flags(hide_deprecated_billing_plans: false)
stub_experiment_for_subject(contact_sales_btn_in_app: true)
stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=#{plan.name}")
.to_return(status: 200, body: plans_data.to_json)
......@@ -479,4 +480,46 @@ RSpec.describe 'Billing plan pages', :feature do
end
end
end
context 'when ff purchase_deprecated_plans is enabled' do
before do
stub_feature_flags(hide_deprecated_billing_plans: true)
end
context 'when deprecated plan is active' do
let(:plan) { bronze_plan }
let!(:subscription) do
create(:gitlab_subscription, namespace: namespace, hosted_plan: plan, seats: 15)
end
let(:expected_card_header) { "#{plans_data[1][:name]} (Legacy)" }
it 'renders the plan card marked as Legacy' do
visit profile_billings_path
page.within('.billing-plans') do
panels = page.all('.card')
expect(panels.length).to eq(plans_data.length)
panel_with_legacy_plan = panels[1] # free [0], bronze [1]
expect(panel_with_legacy_plan.find('.card-header')).to have_content(expected_card_header)
expect(panel_with_legacy_plan.find('.card-body')).to have_link('frequently asked questions')
end
end
end
context 'when deprecated plan is inactive' do
let(:plan) { free_plan }
it 'does not render the card for that plan' do
visit profile_billings_path
page.within('.billing-plans') do
panels = page.all('.card')
expect(panels.length).to eq(plans_data.length - 1)
end
end
end
end
end
......@@ -21,7 +21,7 @@ RSpec.describe 'Group Boards', :js do
wait_for_requests
find(:css, '.js-delete-board button').click
find(:css, '.board-config-modal .js-primary-button').click
find(:css, '.board-config-modal .js-modal-action-primary').click
click_boards_dropdown
......
......@@ -297,6 +297,9 @@ RSpec.describe 'Scoped issue boards', :js do
visit project_boards_path(project)
update_board_label(label_title)
wait_for_all_requests
update_board_label(label_2_title)
expect(page).to have_css('.js-visual-token')
......@@ -455,7 +458,7 @@ RSpec.describe 'Scoped issue boards', :js do
it "doesn't show the input when creating a board" do
click_on_create_new_board
page.within '.js-boards-selector' do
page.within '.js-board-config-modal' do
# To make sure the form is shown
expect(page).to have_field('board-new-name')
......@@ -469,14 +472,14 @@ RSpec.describe 'Scoped issue boards', :js do
end
def expect_dot_highlight(button_title)
button = first('.filter-dropdown-container .btn.btn-inverted')
button = first('.filter-dropdown-container .btn.gl-button')
expect(button.text).to include(button_title)
expect(button[:class]).to include('dot-highlight')
expect(button['title']).to include('This board\'s scope is reduced')
end
def expect_no_dot_highlight(button_title)
button = first('.filter-dropdown-container .btn.btn-inverted')
button = first('.filter-dropdown-container .btn.gl-button')
expect(button.text).to include(button_title)
expect(button[:class]).not_to include('dot-highlight')
expect(button['title']).not_to include('This board\'s scope is reduced')
......@@ -540,7 +543,7 @@ RSpec.describe 'Scoped issue boards', :js do
click_on_board_modal
click_button 'Create'
click_button 'Create board'
wait_for_requests
......@@ -569,7 +572,7 @@ RSpec.describe 'Scoped issue boards', :js do
click_on_board_modal
click_button 'Save'
click_button 'Save changes'
wait_for_requests
......@@ -579,6 +582,6 @@ RSpec.describe 'Scoped issue boards', :js do
# Click on modal to make sure the dropdown is closed (e.g. label scenario)
#
def click_on_board_modal
find('.board-config-modal').click
find('.board-config-modal .modal-content').click
end
end
......@@ -90,7 +90,8 @@
"purchase_link": {
"action": "upgrade",
"href": "http://customers.gitlab.com/subscriptions/new?plan_id=2c92a0ff5a840412015aa3cde86f2ba6"
}
},
"deprecated": true
},
{
"id": "silver-external-id",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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