Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6de7d2c195
commit
bf1600d157
|
@ -49,6 +49,7 @@
|
|||
"Elasticsearch",
|
||||
"Facebook",
|
||||
"fastlane",
|
||||
"fluent-plugin-redis-slowlog",
|
||||
"GDK",
|
||||
"Geo",
|
||||
"Git LFS",
|
||||
|
|
|
@ -1 +1 @@
|
|||
ab2f2386ab69575cd0a58f7279be707a17d7a6c8
|
||||
b670554eae8643f2072d3b4f6f7c5cd2b9ec8776
|
||||
|
|
|
@ -123,7 +123,7 @@ export default {
|
|||
</div>
|
||||
<template v-if="!isEditing">
|
||||
<div
|
||||
class="note-text js-note-text"
|
||||
class="note-text js-note-text md"
|
||||
data-qa-selector="note_content"
|
||||
v-html="note.bodyHtml"
|
||||
></div>
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import TreeActionLink from './tree_action_link.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { webIDEUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeActionLink,
|
||||
},
|
||||
props: {
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
refSha: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canPushCode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
forkPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showLinkToFork() {
|
||||
return !this.canPushCode && this.forkPath;
|
||||
},
|
||||
text() {
|
||||
return this.showLinkToFork ? __('Edit fork in Web IDE') : __('Web IDE');
|
||||
},
|
||||
path() {
|
||||
const path = this.showLinkToFork ? this.forkPath : this.projectPath;
|
||||
return webIDEUrl(`/${path}/edit/${this.refSha}/-/${this.$route.params.path || ''}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tree-action-link :path="path" :text="text" data-qa-selector="web_ide_button" />
|
||||
</template>
|
|
@ -1,30 +1,22 @@
|
|||
import Vue from 'vue';
|
||||
import { escapeFileUrl } from '../lib/utils/url_utility';
|
||||
import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
import Breadcrumbs from './components/breadcrumbs.vue';
|
||||
import LastCommit from './components/last_commit.vue';
|
||||
import TreeActionLink from './components/tree_action_link.vue';
|
||||
import WebIdeLink from './components/web_ide_link.vue';
|
||||
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||
import DirectoryDownloadLinks from './components/directory_download_links.vue';
|
||||
import apolloProvider from './graphql';
|
||||
import { setTitle } from './utils/title';
|
||||
import { updateFormAction } from './utils/dom';
|
||||
import { parseBoolean } from '../lib/utils/common_utils';
|
||||
import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
|
||||
import { __ } from '../locale';
|
||||
|
||||
export default function setupVueRepositoryList() {
|
||||
const el = document.getElementById('js-tree-list');
|
||||
const { dataset } = el;
|
||||
const {
|
||||
canPushCode,
|
||||
projectPath,
|
||||
projectShortPath,
|
||||
forkPath,
|
||||
ref,
|
||||
escapedRef,
|
||||
fullName,
|
||||
} = dataset;
|
||||
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
|
||||
const router = createRouter(projectPath, escapedRef);
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
|
@ -121,6 +113,10 @@ export default function setupVueRepositoryList() {
|
|||
const webIdeLinkEl = document.getElementById('js-tree-web-ide-link');
|
||||
|
||||
if (webIdeLinkEl) {
|
||||
const { ideBasePath, ...options } = convertObjectPropsToCamelCase(
|
||||
JSON.parse(webIdeLinkEl.dataset.options),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: webIdeLinkEl,
|
||||
|
@ -128,10 +124,10 @@ export default function setupVueRepositoryList() {
|
|||
render(h) {
|
||||
return h(WebIdeLink, {
|
||||
props: {
|
||||
projectPath,
|
||||
refSha: ref,
|
||||
forkPath,
|
||||
canPushCode: parseBoolean(canPushCode),
|
||||
webIdeUrl: webIDEUrl(
|
||||
joinPaths('/', ideBasePath, 'edit', ref, '-', this.$route.params.path || '', '/'),
|
||||
),
|
||||
...options,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
<script>
|
||||
import {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlLink,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
GlLink,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectedKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasMultipleActions() {
|
||||
return this.actions.length > 1;
|
||||
},
|
||||
selectedAction() {
|
||||
return this.actions.find(x => x.key === this.selectedKey) || this.actions[0];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleItemClick(action) {
|
||||
this.$emit('select', action.key);
|
||||
},
|
||||
handleClick(action, evt) {
|
||||
return action.handle?.(evt);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown
|
||||
v-if="hasMultipleActions"
|
||||
v-gl-tooltip="selectedAction.tooltip"
|
||||
class="gl-button-deprecated-adapter"
|
||||
:text="selectedAction.text"
|
||||
:split-href="selectedAction.href"
|
||||
split
|
||||
@click="handleClick(selectedAction, $event)"
|
||||
>
|
||||
<template slot="button-content">
|
||||
<span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
|
||||
{{ selectedAction.text }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-for="(action, index) in actions">
|
||||
<gl-dropdown-item
|
||||
:key="action.key"
|
||||
class="gl-dropdown-item-deprecated-adapter"
|
||||
:is-check-item="true"
|
||||
:is-checked="action.key === selectedAction.key"
|
||||
:secondary-text="action.secondaryText"
|
||||
:data-testid="`action_${action.key}`"
|
||||
@click="handleItemClick(action)"
|
||||
>
|
||||
{{ action.text }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
<gl-link
|
||||
v-else-if="selectedAction"
|
||||
v-gl-tooltip="selectedAction.tooltip"
|
||||
v-bind="selectedAction.attrs"
|
||||
class="btn"
|
||||
:href="selectedAction.href"
|
||||
@click="handleClick(selectedAction, $event)"
|
||||
>
|
||||
{{ selectedAction.text }}
|
||||
</gl-link>
|
||||
</template>
|
|
@ -0,0 +1,118 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { __ } from '~/locale';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import ActionsButton from '~/vue_shared/components/actions_button.vue';
|
||||
|
||||
const KEY_WEB_IDE = 'webide';
|
||||
const KEY_GITPOD = 'gitpod';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActionsButton,
|
||||
LocalStorageSync,
|
||||
},
|
||||
props: {
|
||||
webIdeUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
needsToFork: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showWebIdeButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
showGitpodButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
gitpodUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
gitpodEnabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selection: KEY_WEB_IDE,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
actions() {
|
||||
return [this.webIdeAction, this.gitpodAction].filter(x => x);
|
||||
},
|
||||
webIdeAction() {
|
||||
if (!this.showWebIdeButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleOptions = this.needsToFork
|
||||
? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') }
|
||||
: { href: this.webIdeUrl };
|
||||
|
||||
return {
|
||||
key: KEY_WEB_IDE,
|
||||
text: __('Web IDE'),
|
||||
secondaryText: __('Quickly and easily edit multiple files in your project.'),
|
||||
tooltip: '',
|
||||
attrs: {
|
||||
'data-qa-selector': 'web_ide_button',
|
||||
},
|
||||
...handleOptions,
|
||||
};
|
||||
},
|
||||
gitpodAction() {
|
||||
if (!this.showGitpodButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleOptions = this.gitpodEnabled
|
||||
? { href: this.gitpodUrl }
|
||||
: { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') };
|
||||
|
||||
const secondaryText = __('Launch a ready-to-code development environment for your project.');
|
||||
|
||||
return {
|
||||
key: KEY_GITPOD,
|
||||
text: __('Gitpod'),
|
||||
secondaryText,
|
||||
tooltip: secondaryText,
|
||||
attrs: {
|
||||
'data-qa-selector': 'gitpod_button',
|
||||
},
|
||||
...handleOptions,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
select(key) {
|
||||
this.selection = key;
|
||||
},
|
||||
showModal(id) {
|
||||
$(id).modal('show');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<actions-button :actions="actions" :selected-key="selection" @select="select" />
|
||||
<local-storage-sync
|
||||
storage-key="gl-web-ide-button-selected"
|
||||
:value="selection"
|
||||
@input="select"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -542,3 +542,13 @@ fieldset[disabled] .btn,
|
|||
.btn-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// This class helps convert `.gl-button` children so that they consistently
|
||||
// match the style of `.btn` elements which might be around them. Ideally we
|
||||
// wouldn't need this class.
|
||||
//
|
||||
// Remove by upgrading all buttons in a container to use the new `.gl-button` style.
|
||||
.gl-button-deprecated-adapter .gl-button {
|
||||
box-shadow: none;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
|
|
@ -1135,3 +1135,17 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
|||
width: $gl-dropdown-width-wide;
|
||||
}
|
||||
}
|
||||
|
||||
.gl-dropdown-item-deprecated-adapter {
|
||||
.dropdown-item {
|
||||
align-items: flex-start;
|
||||
|
||||
.gl-new-dropdown-item-text-primary {
|
||||
@include gl-font-weight-bold;
|
||||
}
|
||||
|
||||
.gl-new-dropdown-item-text-secondary {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
|
|||
:view_diffs_file_by_file,
|
||||
:tab_width,
|
||||
:sourcegraph_enabled,
|
||||
:gitpod_enabled,
|
||||
:render_whitespace_in_code
|
||||
]
|
||||
end
|
||||
|
|
|
@ -104,6 +104,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
:bio,
|
||||
:email,
|
||||
:role,
|
||||
:gitpod_enabled,
|
||||
:hide_no_password,
|
||||
:hide_no_ssh_key,
|
||||
:hide_project_limit,
|
||||
|
|
|
@ -222,6 +222,8 @@ module ApplicationSettingsHelper
|
|||
:gitaly_timeout_default,
|
||||
:gitaly_timeout_medium,
|
||||
:gitaly_timeout_fast,
|
||||
:gitpod_enabled,
|
||||
:gitpod_url,
|
||||
:grafana_enabled,
|
||||
:grafana_url,
|
||||
:gravatar_enabled,
|
||||
|
|
|
@ -80,6 +80,13 @@ module PreferencesHelper
|
|||
)
|
||||
end
|
||||
|
||||
def integration_views
|
||||
[].tap do |views|
|
||||
views << 'gitpod' if Gitlab::Gitpod.feature_and_settings_enabled?
|
||||
views << 'sourcegraph' if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too
|
||||
|
|
|
@ -191,16 +191,46 @@ module TreeHelper
|
|||
|
||||
def vue_file_list_data(project, ref)
|
||||
{
|
||||
can_push_code: current_user&.can?(:push_code, project) && "true",
|
||||
project_path: project.full_path,
|
||||
project_short_path: project.path,
|
||||
fork_path: current_user&.fork_of(project)&.full_path,
|
||||
ref: ref,
|
||||
escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref),
|
||||
full_name: project.name_with_namespace
|
||||
}
|
||||
end
|
||||
|
||||
def ide_base_path(project)
|
||||
can_push_code = current_user&.can?(:push_code, project)
|
||||
fork_path = current_user&.fork_of(project)&.full_path
|
||||
|
||||
if can_push_code
|
||||
project.full_path
|
||||
else
|
||||
fork_path || project.full_path
|
||||
end
|
||||
end
|
||||
|
||||
def vue_ide_link_data(project, ref)
|
||||
can_collaborate = can_collaborate_with_project?(project)
|
||||
can_create_mr_from_fork = can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
|
||||
show_web_ide_button = (can_collaborate || current_user&.already_forked?(project) || can_create_mr_from_fork)
|
||||
|
||||
{
|
||||
ide_base_path: ide_base_path(project),
|
||||
needs_to_fork: !can_collaborate && !current_user&.already_forked?(project),
|
||||
show_web_ide_button: show_web_ide_button,
|
||||
show_gitpod_button: show_web_ide_button && Gitlab::Gitpod.feature_and_settings_enabled?(project),
|
||||
gitpod_url: full_gitpod_url(project, ref),
|
||||
gitpod_enabled: current_user&.gitpod_enabled
|
||||
}
|
||||
end
|
||||
|
||||
def full_gitpod_url(project, ref)
|
||||
return "" unless Gitlab::Gitpod.feature_and_settings_enabled?(project)
|
||||
|
||||
"#{Gitlab::CurrentSettings.gitpod_url}##{project_tree_url(project, tree_join(ref, @path || ''))}"
|
||||
end
|
||||
|
||||
def directory_download_links(project, ref, archive_prefix)
|
||||
Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt|
|
||||
{
|
||||
|
|
|
@ -132,6 +132,11 @@ class ApplicationSetting < ApplicationRecord
|
|||
presence: true,
|
||||
if: :sourcegraph_enabled
|
||||
|
||||
validates :gitpod_url,
|
||||
presence: true,
|
||||
addressable_url: { enforce_sanitization: true },
|
||||
if: :gitpod_enabled
|
||||
|
||||
validates :snowplow_collector_hostname,
|
||||
presence: true,
|
||||
hostname: true,
|
||||
|
|
|
@ -74,6 +74,8 @@ module ApplicationSettingImplementation
|
|||
gitaly_timeout_default: 55,
|
||||
gitaly_timeout_fast: 10,
|
||||
gitaly_timeout_medium: 30,
|
||||
gitpod_enabled: false,
|
||||
gitpod_url: 'https://gitpod.io/',
|
||||
gravatar_enabled: Settings.gravatar['enabled'],
|
||||
group_download_export_limit: 1,
|
||||
group_export_limit: 6,
|
||||
|
|
|
@ -11,6 +11,8 @@ module Ci
|
|||
|
||||
default_value_for :data_store, :redis
|
||||
|
||||
after_create { metrics.increment_trace_operation(operation: :chunked) }
|
||||
|
||||
CHUNK_SIZE = 128.kilobytes
|
||||
WRITE_LOCK_RETRY = 10
|
||||
WRITE_LOCK_SLEEP = 0.01.seconds
|
||||
|
@ -182,6 +184,8 @@ module Ci
|
|||
end
|
||||
|
||||
current_store.append_data(self, value, offset).then do |stored|
|
||||
metrics.increment_trace_operation(operation: :appended)
|
||||
|
||||
raise ArgumentError, 'Trace appended incorrectly' if stored != new_size
|
||||
end
|
||||
|
||||
|
@ -205,5 +209,9 @@ module Ci
|
|||
retries: WRITE_LOCK_RETRY,
|
||||
sleep_sec: WRITE_LOCK_SLEEP }]
|
||||
end
|
||||
|
||||
def metrics
|
||||
@metrics ||= ::Gitlab::Ci::Trace::Metrics.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,18 +27,7 @@
|
|||
#
|
||||
module RelativePositioning
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
STEPS = 10
|
||||
IDEAL_DISTANCE = 2**(STEPS - 1) + 1
|
||||
|
||||
MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
|
||||
START_POSITION = 0
|
||||
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
|
||||
|
||||
MAX_GAP = IDEAL_DISTANCE * 2
|
||||
MIN_GAP = 2
|
||||
|
||||
NoSpaceLeft = Class.new(StandardError)
|
||||
include ::Gitlab::RelativePositioning
|
||||
|
||||
class_methods do
|
||||
def move_nulls_to_end(objects)
|
||||
|
@ -49,56 +38,10 @@ module RelativePositioning
|
|||
move_nulls(objects, at_end: false)
|
||||
end
|
||||
|
||||
# This method takes two integer values (positions) and
|
||||
# calculates the position between them. The range is huge as
|
||||
# the maximum integer value is 2147483647.
|
||||
#
|
||||
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
|
||||
#
|
||||
# Then we handle one of three cases:
|
||||
# - If the gap is too small, we raise NoSpaceLeft
|
||||
# - If the gap is larger than MAX_GAP, we place the new position at most
|
||||
# IDEAL_DISTANCE from the edge of the gap.
|
||||
# - otherwise we place the new position at the midpoint.
|
||||
#
|
||||
# The new position will always satisfy: pos_before <= midpoint <= pos_after
|
||||
#
|
||||
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
|
||||
# If the gap is too small, NoSpaceLeft is raised.
|
||||
#
|
||||
# This class method should only be called by instance methods of this module, which
|
||||
# include handling for minimum gap size.
|
||||
#
|
||||
# @raises NoSpaceLeft
|
||||
# @api private
|
||||
def position_between(pos_before, pos_after)
|
||||
pos_before ||= MIN_POSITION
|
||||
pos_after ||= MAX_POSITION
|
||||
|
||||
pos_before, pos_after = [pos_before, pos_after].sort
|
||||
|
||||
gap_width = pos_after - pos_before
|
||||
midpoint = [pos_after - 1, pos_before + (gap_width / 2)].min
|
||||
|
||||
if gap_width < MIN_GAP
|
||||
raise NoSpaceLeft
|
||||
elsif gap_width > MAX_GAP
|
||||
if pos_before <= MIN_POSITION
|
||||
pos_after - IDEAL_DISTANCE
|
||||
elsif pos_after >= MAX_POSITION
|
||||
pos_before + IDEAL_DISTANCE
|
||||
else
|
||||
midpoint
|
||||
end
|
||||
else
|
||||
midpoint
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @api private
|
||||
def gap_size(object, gaps:, at_end:, starting_from:)
|
||||
def gap_size(context, gaps:, at_end:, starting_from:)
|
||||
total_width = IDEAL_DISTANCE * gaps
|
||||
size = if at_end && starting_from + total_width >= MAX_POSITION
|
||||
(MAX_POSITION - starting_from) / gaps
|
||||
|
@ -108,23 +51,17 @@ module RelativePositioning
|
|||
IDEAL_DISTANCE
|
||||
end
|
||||
|
||||
# Shift max elements leftwards if there isn't enough space
|
||||
return [size, starting_from] if size >= MIN_GAP
|
||||
|
||||
order = at_end ? :desc : :asc
|
||||
terminus = object
|
||||
.send(:relative_siblings) # rubocop:disable GitlabSecurity/PublicSend
|
||||
.where('relative_position IS NOT NULL')
|
||||
.order(relative_position: order)
|
||||
.first
|
||||
|
||||
if at_end
|
||||
terminus.move_sequence_before(true)
|
||||
max_relative_position = terminus.reset.relative_position
|
||||
terminus = context.max_sibling
|
||||
terminus.shift_left
|
||||
max_relative_position = terminus.relative_position
|
||||
[[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
|
||||
else
|
||||
terminus.move_sequence_after(true)
|
||||
min_relative_position = terminus.reset.relative_position
|
||||
terminus = context.min_sibling
|
||||
terminus.shift_right
|
||||
min_relative_position = terminus.relative_position
|
||||
[[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
|
||||
end
|
||||
end
|
||||
|
@ -142,8 +79,9 @@ module RelativePositioning
|
|||
objects = objects.reject(&:relative_position)
|
||||
return 0 if objects.empty?
|
||||
|
||||
representative = objects.first
|
||||
number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each
|
||||
representative = RelativePositioning.mover.context(objects.first)
|
||||
|
||||
position = if at_end
|
||||
representative.max_relative_position
|
||||
else
|
||||
|
@ -198,306 +136,68 @@ module RelativePositioning
|
|||
end
|
||||
end
|
||||
|
||||
def min_relative_position(&block)
|
||||
calculate_relative_position('MIN', &block)
|
||||
end
|
||||
|
||||
def max_relative_position(&block)
|
||||
calculate_relative_position('MAX', &block)
|
||||
end
|
||||
|
||||
def prev_relative_position(ignoring: nil)
|
||||
prev_pos = nil
|
||||
|
||||
if self.relative_position
|
||||
prev_pos = max_relative_position do |relation|
|
||||
relation = relation.id_not_in(ignoring.id) if ignoring.present?
|
||||
relation.where('relative_position < ?', self.relative_position)
|
||||
end
|
||||
end
|
||||
|
||||
prev_pos
|
||||
end
|
||||
|
||||
def next_relative_position(ignoring: nil)
|
||||
next_pos = nil
|
||||
|
||||
if self.relative_position
|
||||
next_pos = min_relative_position do |relation|
|
||||
relation = relation.id_not_in(ignoring.id) if ignoring.present?
|
||||
relation.where('relative_position > ?', self.relative_position)
|
||||
end
|
||||
end
|
||||
|
||||
next_pos
|
||||
def self.mover
|
||||
::Gitlab::RelativePositioning::Mover.new(START_POSITION, (MIN_POSITION..MAX_POSITION))
|
||||
end
|
||||
|
||||
def move_between(before, after)
|
||||
return move_after(before) unless after
|
||||
return move_before(after) unless before
|
||||
before, after = [before, after].sort_by(&:relative_position) if before && after
|
||||
|
||||
before, after = after, before if after.relative_position < before.relative_position
|
||||
|
||||
pos_left = before.relative_position
|
||||
pos_right = after.relative_position
|
||||
|
||||
if pos_right - pos_left < MIN_GAP
|
||||
# Not enough room! Make space by shifting all previous elements to the left
|
||||
# if there is enough space, else to the right
|
||||
gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
if gap.present?
|
||||
after.move_sequence_before(next_gap: gap)
|
||||
pos_left -= optimum_delta_for_gap(gap)
|
||||
else
|
||||
before.move_sequence_after
|
||||
pos_right = after.reset.relative_position
|
||||
end
|
||||
end
|
||||
|
||||
new_position = self.class.position_between(pos_left, pos_right)
|
||||
|
||||
self.relative_position = new_position
|
||||
RelativePositioning.mover.move(self, before, after)
|
||||
rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
|
||||
could_not_move(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
def move_after(before = self)
|
||||
pos_before = before.relative_position
|
||||
pos_after = before.next_relative_position(ignoring: self)
|
||||
|
||||
if pos_before == MAX_POSITION || gap_too_small?(pos_after, pos_before)
|
||||
gap = before.send(:find_next_gap_after) # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
if gap.nil?
|
||||
before.move_sequence_before(true)
|
||||
pos_before = before.reset.relative_position
|
||||
else
|
||||
before.move_sequence_after(next_gap: gap)
|
||||
pos_after += optimum_delta_for_gap(gap)
|
||||
end
|
||||
end
|
||||
|
||||
self.relative_position = self.class.position_between(pos_before, pos_after)
|
||||
RelativePositioning.mover.move(self, before, nil)
|
||||
rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
|
||||
could_not_move(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
def move_before(after = self)
|
||||
pos_after = after.relative_position
|
||||
pos_before = after.prev_relative_position(ignoring: self)
|
||||
|
||||
if pos_after == MIN_POSITION || gap_too_small?(pos_before, pos_after)
|
||||
gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
if gap.nil?
|
||||
after.move_sequence_after(true)
|
||||
pos_after = after.reset.relative_position
|
||||
else
|
||||
after.move_sequence_before(next_gap: gap)
|
||||
pos_before -= optimum_delta_for_gap(gap)
|
||||
end
|
||||
end
|
||||
|
||||
self.relative_position = self.class.position_between(pos_before, pos_after)
|
||||
RelativePositioning.mover.move(self, nil, after)
|
||||
rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
|
||||
could_not_move(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
def move_to_end
|
||||
max_pos = max_relative_position
|
||||
|
||||
if max_pos.nil?
|
||||
self.relative_position = START_POSITION
|
||||
elsif gap_too_small?(max_pos, MAX_POSITION + 1)
|
||||
max = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'DESC')).first
|
||||
max.move_sequence_before(true)
|
||||
max.reset
|
||||
self.relative_position = self.class.position_between(max.relative_position, MAX_POSITION + 1)
|
||||
else
|
||||
self.relative_position = self.class.position_between(max_pos, MAX_POSITION + 1)
|
||||
end
|
||||
RelativePositioning.mover.move_to_end(self)
|
||||
rescue NoSpaceLeft => e
|
||||
could_not_move(e)
|
||||
self.relative_position = MAX_POSITION
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
could_not_move(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
def move_to_start
|
||||
min_pos = min_relative_position
|
||||
|
||||
if min_pos.nil?
|
||||
self.relative_position = START_POSITION
|
||||
elsif gap_too_small?(min_pos, MIN_POSITION - 1)
|
||||
min = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'ASC')).first
|
||||
min.move_sequence_after(true)
|
||||
min.reset
|
||||
self.relative_position = self.class.position_between(MIN_POSITION - 1, min.relative_position)
|
||||
else
|
||||
self.relative_position = self.class.position_between(MIN_POSITION - 1, min_pos)
|
||||
end
|
||||
RelativePositioning.mover.move_to_start(self)
|
||||
rescue NoSpaceLeft => e
|
||||
could_not_move(e)
|
||||
self.relative_position = MIN_POSITION
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
could_not_move(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
# Moves the sequence before the current item to the middle of the next gap
|
||||
# For example, we have
|
||||
#
|
||||
# 5 . . . . . 11 12 13 14 [15] 16 . 17
|
||||
# -----------
|
||||
#
|
||||
# This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
|
||||
#
|
||||
# 5 . . 8 9 10 11 . . . [15] 16 . 17
|
||||
# ---------
|
||||
#
|
||||
# Creating a gap to the left of the current item. We can understand this as
|
||||
# dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
|
||||
#
|
||||
# If `include_self` is true, the current item will also be moved, creating a
|
||||
# gap to the right of the current item:
|
||||
#
|
||||
# 5 . . 8 9 10 11 [14] . . . 16 . 17
|
||||
# --------------
|
||||
#
|
||||
# As an optimization, the gap can be precalculated and passed to this method.
|
||||
#
|
||||
# @api private
|
||||
# @raises NoSpaceLeft if the sequence cannot be moved
|
||||
def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
|
||||
raise NoSpaceLeft unless next_gap.present?
|
||||
|
||||
delta = optimum_delta_for_gap(next_gap)
|
||||
|
||||
move_sequence(next_gap[:start], relative_position, -delta, include_self)
|
||||
end
|
||||
|
||||
# Moves the sequence after the current item to the middle of the next gap
|
||||
# For example, we have:
|
||||
#
|
||||
# 8 . 10 [11] 12 13 14 15 . . . . . 21
|
||||
# -----------
|
||||
#
|
||||
# This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
|
||||
#
|
||||
# 8 . 10 [11] . . . 15 16 17 18 . . 21
|
||||
# -----------
|
||||
#
|
||||
# Creating a gap to the right of the current item. We can understand this as
|
||||
# dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
|
||||
#
|
||||
# If `include_self` is true, the current item will also be moved, creating a
|
||||
# gap to the left of the current item:
|
||||
#
|
||||
# 8 . 10 . . . [14] 15 16 17 18 . . 21
|
||||
# ----------------
|
||||
#
|
||||
# As an optimization, the gap can be precalculated and passed to this method.
|
||||
#
|
||||
# @api private
|
||||
# @raises NoSpaceLeft if the sequence cannot be moved
|
||||
def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
|
||||
raise NoSpaceLeft unless next_gap.present?
|
||||
|
||||
delta = optimum_delta_for_gap(next_gap)
|
||||
|
||||
move_sequence(relative_position, next_gap[:start], delta, include_self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gap_too_small?(pos_a, pos_b)
|
||||
return false unless pos_a && pos_b
|
||||
|
||||
(pos_a - pos_b).abs < MIN_GAP
|
||||
end
|
||||
|
||||
# Find the first suitable gap to the left of the current position.
|
||||
#
|
||||
# Satisfies the relations:
|
||||
# - gap[:start] <= relative_position
|
||||
# - abs(gap[:start] - gap[:end]) >= MIN_GAP
|
||||
# - MIN_POSITION <= gap[:start] <= MAX_POSITION
|
||||
# - MIN_POSITION <= gap[:end] <= MAX_POSITION
|
||||
#
|
||||
# Supposing that the current item is 13, and we have a sequence of items:
|
||||
#
|
||||
# 1 . . . 5 . . . . 11 12 [13] 14 . . 17
|
||||
# ^---------^
|
||||
#
|
||||
# Then we return: `{ start: 11, end: 5 }`
|
||||
#
|
||||
# Here start refers to the end of the gap closest to the current item.
|
||||
def find_next_gap_before
|
||||
items_with_next_pos = scoped_items
|
||||
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
|
||||
.where('relative_position <= ?', relative_position)
|
||||
.order(relative_position: :desc)
|
||||
|
||||
find_next_gap(items_with_next_pos, MIN_POSITION)
|
||||
end
|
||||
|
||||
# Find the first suitable gap to the right of the current position.
|
||||
#
|
||||
# Satisfies the relations:
|
||||
# - gap[:start] >= relative_position
|
||||
# - abs(gap[:start] - gap[:end]) >= MIN_GAP
|
||||
# - MIN_POSITION <= gap[:start] <= MAX_POSITION
|
||||
# - MIN_POSITION <= gap[:end] <= MAX_POSITION
|
||||
#
|
||||
# Supposing the current item is 13, and that we have a sequence of items:
|
||||
#
|
||||
# 9 . . . [13] 14 15 . . . . 20 . . . 24
|
||||
# ^---------^
|
||||
#
|
||||
# Then we return: `{ start: 15, end: 20 }`
|
||||
#
|
||||
# Here start refers to the end of the gap closest to the current item.
|
||||
def find_next_gap_after
|
||||
items_with_next_pos = scoped_items
|
||||
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
|
||||
.where('relative_position >= ?', relative_position)
|
||||
.order(:relative_position)
|
||||
|
||||
find_next_gap(items_with_next_pos, MAX_POSITION)
|
||||
end
|
||||
|
||||
def find_next_gap(items_with_next_pos, end_is_nil)
|
||||
gap = self.class
|
||||
.from(items_with_next_pos, :items)
|
||||
.where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
|
||||
.limit(1)
|
||||
.pluck(:pos, :next_pos)
|
||||
.first
|
||||
|
||||
return if gap.nil? || gap.first == end_is_nil
|
||||
|
||||
{ start: gap.first, end: gap.second || end_is_nil }
|
||||
end
|
||||
|
||||
def optimum_delta_for_gap(gap)
|
||||
delta = ((gap[:start] - gap[:end]) / 2.0).abs.ceil
|
||||
|
||||
[delta, IDEAL_DISTANCE].min
|
||||
end
|
||||
|
||||
def move_sequence(start_pos, end_pos, delta, include_self = false)
|
||||
relation = include_self ? scoped_items : relative_siblings
|
||||
|
||||
# This method is used during rebalancing - override it to customise the update
|
||||
# logic:
|
||||
def update_relative_siblings(relation, range, delta)
|
||||
relation
|
||||
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
|
||||
.where(relative_position: range)
|
||||
.update_all("relative_position = relative_position + #{delta}")
|
||||
end
|
||||
|
||||
def calculate_relative_position(calculation)
|
||||
# When calculating across projects, this is much more efficient than
|
||||
# MAX(relative_position) without the GROUP BY, due to index usage:
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977
|
||||
relation = scoped_items
|
||||
.order(Gitlab::Database.nulls_last_order('position', 'DESC'))
|
||||
.group(self.class.relative_positioning_parent_column)
|
||||
.limit(1)
|
||||
|
||||
relation = yield relation if block_given?
|
||||
|
||||
relation
|
||||
.pluck(self.class.relative_positioning_parent_column, Arel.sql("#{calculation}(relative_position) AS position"))
|
||||
.first&.last
|
||||
# This method is used to exclude the current self (or another object)
|
||||
# from a relation. Customize this if `id <> :id` is not sufficient
|
||||
def exclude_self(relation, excluded: self)
|
||||
relation.id_not_in(excluded.id)
|
||||
end
|
||||
|
||||
def relative_siblings(relation = scoped_items)
|
||||
relation.id_not_in(id)
|
||||
end
|
||||
|
||||
def scoped_items
|
||||
self.class.relative_positioning_query_base(self)
|
||||
# Override if you want to be notified of failures to move
|
||||
def could_not_move(exception)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -444,20 +444,9 @@ class Issue < ApplicationRecord
|
|||
Gitlab::EtagCaching::Store.new.touch(key)
|
||||
end
|
||||
|
||||
def find_next_gap_before
|
||||
super
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
def could_not_move(exception)
|
||||
# Symptom of running out of space - schedule rebalancing
|
||||
IssueRebalancingWorker.perform_async(nil, project_id)
|
||||
raise e
|
||||
end
|
||||
|
||||
def find_next_gap_after
|
||||
super
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
# Symptom of running out of space - schedule rebalancing
|
||||
IssueRebalancingWorker.perform_async(nil, project_id)
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -41,10 +41,6 @@ class Project < ApplicationRecord
|
|||
|
||||
STATISTICS_ATTRIBUTE = 'repositories_count'
|
||||
UNKNOWN_IMPORT_URL = 'http://unknown.git'
|
||||
# Hashed Storage versions handle rolling out new storage to project and dependents models:
|
||||
# nil: legacy
|
||||
# 1: repository
|
||||
# 2: attachments
|
||||
LATEST_STORAGE_VERSION = 2
|
||||
HASHED_STORAGE_FEATURES = {
|
||||
repository: 1,
|
||||
|
|
|
@ -279,6 +279,7 @@ class User < ApplicationRecord
|
|||
:view_diffs_file_by_file, :view_diffs_file_by_file=,
|
||||
:tab_width, :tab_width=,
|
||||
:sourcegraph_enabled, :sourcegraph_enabled=,
|
||||
:gitpod_enabled, :gitpod_enabled=,
|
||||
:setup_for_company, :setup_for_company=,
|
||||
:render_whitespace_in_code, :render_whitespace_in_code=,
|
||||
:experience_level, :experience_level=,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Placeholder class for model that is implemented in EE
|
||||
# It reserves '+' as a reference prefix, but the table does not exist in FOSS
|
||||
class Vulnerability < ApplicationRecord
|
||||
include IgnorableColumns
|
||||
|
||||
def self.reference_prefix
|
||||
'+'
|
||||
end
|
||||
|
||||
def self.reference_prefix_escaped
|
||||
'+'
|
||||
end
|
||||
end
|
||||
|
||||
Vulnerability.prepend_if_ee('EE::Vulnerability')
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Ci
|
||||
class CreateJobArtifactsService < ::BaseService
|
||||
include Gitlab::Utils::UsageData
|
||||
|
||||
ArtifactsExistError = Class.new(StandardError)
|
||||
|
||||
LSIF_ARTIFACT_TYPE = 'lsif'
|
||||
|
@ -22,7 +24,11 @@ module Ci
|
|||
return result unless result[:status] == :success
|
||||
|
||||
headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size(artifact_type))
|
||||
headers[:ProcessLsif] = lsif?(artifact_type)
|
||||
|
||||
if lsif?(artifact_type)
|
||||
headers[:ProcessLsif] = true
|
||||
track_usage_event('i_source_code_code_intelligence', project)
|
||||
end
|
||||
|
||||
success(headers: headers)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
- return unless Gitlab::Gitpod.feature_available?
|
||||
- expanded = integration_expanded?('gitpod_')
|
||||
- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
|
||||
|
||||
%section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
= _('Gitpod')
|
||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
|
||||
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
||||
|
||||
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :gitpod_enabled, class: 'form-check-input'
|
||||
= f.label :gitpod_enabled, s_('Gitpod|Enable Gitpod integration'), class: 'form-check-label'
|
||||
.form-group
|
||||
= f.label :gitpod_url, s_('Gitpod|Gitpod URL'), class: 'label-bold'
|
||||
= f.text_field :gitpod_url, class: 'form-control', placeholder: s_('Gitpod|e.g. https://gitpod.example.com')
|
||||
.form-text.text-muted
|
||||
= s_('Gitpod|Add the URL to your Gitpod instance configured to read your GitLab projects.')
|
||||
= f.submit s_('Save changes'), class: 'btn btn-success'
|
|
@ -117,6 +117,7 @@
|
|||
#js-maintenance-mode-settings
|
||||
|
||||
= render_if_exists 'admin/application_settings/elasticsearch_form'
|
||||
= render 'admin/application_settings/gitpod'
|
||||
= render 'admin/application_settings/plantuml'
|
||||
= render 'admin/application_settings/sourcegraph'
|
||||
= render_if_exists 'admin/application_settings/slack'
|
||||
|
|
|
@ -23,10 +23,3 @@
|
|||
= build.stage
|
||||
%td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
|
||||
= render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
|
||||
%tr.build-log
|
||||
- if build.has_trace?
|
||||
%td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
|
||||
%pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
|
||||
= build.trace.html(last_lines: 30).html_safe
|
||||
- else
|
||||
%td{ colspan: "2" }
|
||||
|
|
|
@ -14,7 +14,4 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
|
|||
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
|
||||
Stage: <%= build.stage %>
|
||||
Name: <%= build.name %>
|
||||
<% if build.has_trace? -%>
|
||||
Trace: <%= build.trace.raw(last_lines: 30) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
|
|
|
@ -34,8 +34,4 @@ had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
|
|||
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
|
||||
Stage: <%= build.stage %>
|
||||
Name: <%= build.name %>
|
||||
<% if build.has_trace? -%>
|
||||
Trace: <%= build.trace.raw(last_lines: 30) %>
|
||||
<% end -%>
|
||||
|
||||
<% end -%>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
|
||||
|
||||
%label.label-bold#gitpod
|
||||
= s_('Gitpod')
|
||||
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
||||
.form-group.form-check
|
||||
= f.check_box :gitpod_enabled, class: 'form-check-input'
|
||||
= f.label :gitpod_enabled, class: 'form-check-label' do
|
||||
= s_('Gitpod|Enable Gitpod integration').html_safe
|
||||
.form-text.text-muted
|
||||
= s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
|
|
@ -0,0 +1,18 @@
|
|||
- views = integration_views
|
||||
- return unless views.any?
|
||||
|
||||
.col-sm-12
|
||||
%hr
|
||||
|
||||
.col-lg-4.profile-settings-sidebar#integrations
|
||||
%h4.gl-mt-0
|
||||
= s_('Preferences|Integrations')
|
||||
%p
|
||||
= s_('Preferences|Customize integrations with third party services.')
|
||||
= succeed '.' do
|
||||
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank'
|
||||
|
||||
.col-lg-8
|
||||
- views.each do |view|
|
||||
= render view, f: f
|
||||
|
|
@ -1,26 +1,10 @@
|
|||
- return unless Gitlab::Sourcegraph::feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
|
||||
- sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
|
||||
|
||||
.col-sm-12
|
||||
%hr
|
||||
|
||||
.col-lg-4.profile-settings-sidebar#integrations
|
||||
%h4.gl-mt-0
|
||||
= s_('Preferences|Integrations')
|
||||
%p
|
||||
= s_('Preferences|Customize integrations with third party services.')
|
||||
= succeed '.' do
|
||||
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank'
|
||||
.col-lg-8
|
||||
%label.label-bold
|
||||
= s_('Preferences|Sourcegraph')
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
||||
.form-group.form-check
|
||||
= f.check_box :sourcegraph_enabled, class: 'form-check-input'
|
||||
= f.label :sourcegraph_enabled, class: 'form-check-label' do
|
||||
- link_start = '<a href="%{url}">'.html_safe % { url: sourcegraph_url }
|
||||
- link_end = '</a>'.html_safe
|
||||
= s_('Preferences|Enable integrated code intelligence on code views').html_safe % { link_start: link_start, link_end: link_end }
|
||||
.form-text.text-muted
|
||||
= sourcegraph_url_message
|
||||
= sourcegraph_experimental_message
|
||||
%label.label-bold
|
||||
= s_('Preferences|Sourcegraph')
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information')
|
||||
.form-group.form-check
|
||||
= f.check_box :sourcegraph_enabled, class: 'form-check-input'
|
||||
= f.label :sourcegraph_enabled, class: 'form-check-label' do
|
||||
= s_('Preferences|Enable integrated code intelligence on code views').html_safe
|
||||
.form-text.text-muted
|
||||
= sourcegraph_url_message
|
||||
= sourcegraph_experimental_message
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
.form-text.text-muted
|
||||
= s_('Preferences|For example: 30 mins ago.')
|
||||
|
||||
= render 'sourcegraph', f: f
|
||||
= render 'integrations', f: f
|
||||
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
.col-lg-8
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- can_collaborate = can_collaborate_with_project?(@project)
|
||||
- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
|
||||
- can_visit_ide = can_collaborate || current_user&.already_forked?(@project)
|
||||
|
||||
.tree-ref-container
|
||||
.tree-ref-holder
|
||||
|
@ -14,12 +15,12 @@
|
|||
|
||||
= render 'projects/find_file_link'
|
||||
|
||||
- if can_collaborate || current_user&.already_forked?(@project)
|
||||
#js-tree-web-ide-link.d-inline-block
|
||||
- elsif can_create_mr_from_fork
|
||||
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
|
||||
= _('Web IDE')
|
||||
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
|
||||
- if can_visit_ide || can_create_mr_from_fork
|
||||
#js-tree-web-ide-link.d-inline-block{ data: { options: vue_ide_link_data(@project, @ref).to_json } }
|
||||
- if !can_visit_ide
|
||||
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
|
||||
- unless current_user&.gitpod_enabled
|
||||
= render 'shared/gitpod/enable_gitpod_modal'
|
||||
|
||||
- if show_xcode_link?(@project)
|
||||
.project-action-button.project-xcode.inline<
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
#modal-enable-gitpod.modal.qa-enable-gitpod-modal
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
%h3.page-title= _('Enable Gitpod?')
|
||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
||||
%span{ "aria-hidden": true } ×
|
||||
.modal-body.p-3
|
||||
%p= (_("To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}.") % { user_prefs: link_to(_('user preferences'), profile_preferences_path(anchor: 'gitpod')) }).html_safe
|
||||
.modal-footer
|
||||
= link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
|
||||
= link_to _('Enable Gitpod'), profile_path(user: { gitpod_enabled: true}), class: 'btn btn-success', method: :put
|
|
@ -60,11 +60,11 @@ class FeatureFlagOptionParser
|
|||
options.force = value
|
||||
end
|
||||
|
||||
opts.on('-m', '--introduced-by-url [string]', String, 'URL to Merge Request introducing Feature Flag') do |value|
|
||||
opts.on('-m', '--introduced-by-url [string]', String, 'URL of Merge Request introducing the Feature Flag') do |value|
|
||||
options.introduced_by_url = value
|
||||
end
|
||||
|
||||
opts.on('-i', '--rollout-issue-url [string]', String, 'URL to Issue rolling out Feature Flag') do |value|
|
||||
opts.on('-i', '--rollout-issue-url [string]', String, 'URL of Issue rolling out the Feature Flag') do |value|
|
||||
options.rollout_issue_url = value
|
||||
end
|
||||
|
||||
|
@ -106,7 +106,7 @@ class FeatureFlagOptionParser
|
|||
|
||||
def read_group
|
||||
$stdout.puts
|
||||
$stdout.puts ">> Please specify the group introducing feature flag, like `group::apm`:"
|
||||
$stdout.puts ">> Specify the group introducing the feature flag, like `group::apm`:"
|
||||
|
||||
loop do
|
||||
$stdout.print "?> "
|
||||
|
@ -114,7 +114,7 @@ class FeatureFlagOptionParser
|
|||
group = nil if group.empty?
|
||||
return group if group.nil? || group.start_with?('group::')
|
||||
|
||||
$stderr.puts "Group needs to include `group::`"
|
||||
$stderr.puts "The group needs to include `group::`"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -123,7 +123,7 @@ class FeatureFlagOptionParser
|
|||
return TYPES.first.first if TYPES.one?
|
||||
|
||||
$stdout.puts
|
||||
$stdout.puts ">> Please specify the type of your feature flag:"
|
||||
$stdout.puts ">> Specify the feature flag type:"
|
||||
$stdout.puts
|
||||
TYPES.each do |type, data|
|
||||
$stdout.puts "#{type.to_s.rjust(15)}#{' '*6}#{data[:description]}"
|
||||
|
@ -141,7 +141,7 @@ class FeatureFlagOptionParser
|
|||
|
||||
def read_introduced_by_url
|
||||
$stdout.puts
|
||||
$stdout.puts ">> If you have MR open, can you paste the URL here? (or enter to skip)"
|
||||
$stdout.puts ">> URL of the MR introducing the feature flag (enter to skip):"
|
||||
|
||||
loop do
|
||||
$stdout.print "?> "
|
||||
|
@ -166,11 +166,11 @@ class FeatureFlagOptionParser
|
|||
issue_new_url = url + "?" + URI.encode_www_form(params)
|
||||
|
||||
$stdout.puts
|
||||
$stdout.puts ">> Open this URL and fill the rest of details:"
|
||||
$stdout.puts ">> Open this URL and fill in the rest of the details:"
|
||||
$stdout.puts issue_new_url
|
||||
$stdout.puts
|
||||
|
||||
$stdout.puts ">> Paste URL of `rollout issue` here, or enter to skip:"
|
||||
$stdout.puts ">> URL of the rollout issue (enter to skip):"
|
||||
|
||||
loop do
|
||||
$stdout.print "?> "
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Create placeholder model for Vulnerability to reserve + as a reference prefix
|
||||
merge_request: 42147
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track projects using code intelligence
|
||||
merge_request: 41881
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix daemon memory killer jobs hash thread safety issue
|
||||
merge_request: 42468
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Design comments do not render the blockquotes correctly
|
||||
merge_request: 42498
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Gitpod integration
|
||||
merge_request: 37985
|
||||
author: Cornelius Ludmann @corneliusludmann
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactor relative positioning to enable better testing
|
||||
merge_request: 41967
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove job logs from notification e-mails
|
||||
merge_request: 42395
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: gitpod
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37985
|
||||
rollout_issue_url:
|
||||
group: group::editor
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: group_level_integrations
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238575
|
||||
group: group::ecosystem
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddGitpodApplicationSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# limit is added in 20200727154631_add_gitpod_application_settings_text_limit
|
||||
def change
|
||||
add_column :application_settings, :gitpod_enabled, :boolean, default: false, null: false
|
||||
add_column :application_settings, :gitpod_url, :text, default: 'https://gitpod.io/', null: true
|
||||
end
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddGitpodApplicationSettingsTextLimit < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_text_limit :application_settings, :gitpod_url, 255
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :application_settings, :gitpod_url
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddGitpodUserPreferences < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :user_preferences, :gitpod_enabled, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
c04fe7e1a56bdcd41b5e1af346f9bfcae170d601954c4a0bcfcc9aea19d55528
|
|
@ -0,0 +1 @@
|
|||
0ce17a8ad6c5ca5bba49ff522fede400fe6666490157af123ad98a7643f3ce01
|
|
@ -0,0 +1 @@
|
|||
523f200c635e37ee1ac52257ffd45443a3e17bfe993d22775a5377865e044a46
|
|
@ -9272,6 +9272,9 @@ CREATE TABLE public.application_settings (
|
|||
enforce_namespace_storage_limit boolean DEFAULT false NOT NULL,
|
||||
container_registry_delete_tags_service_timeout integer DEFAULT 250 NOT NULL,
|
||||
elasticsearch_client_request_timeout integer DEFAULT 0 NOT NULL,
|
||||
gitpod_enabled boolean DEFAULT false NOT NULL,
|
||||
gitpod_url text DEFAULT 'https://gitpod.io/'::text,
|
||||
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
|
||||
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
|
||||
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
|
||||
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
|
||||
|
@ -16277,7 +16280,8 @@ CREATE TABLE public.user_preferences (
|
|||
tab_width smallint,
|
||||
feature_filter_type bigint,
|
||||
experience_level smallint,
|
||||
view_diffs_file_by_file boolean DEFAULT false NOT NULL
|
||||
view_diffs_file_by_file boolean DEFAULT false NOT NULL,
|
||||
gitpod_enabled boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.user_preferences_id_seq
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Geo
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
type: howto
|
||||
---
|
||||
|
||||
CAUTION: **Caution:**
|
||||
This runbook is in **alpha**. For complete, production-ready documentation, see the
|
||||
[disaster recovery documentation](index.md).
|
||||
|
||||
# Disaster Recovery (Geo) promotion runbooks **(PREMIUM ONLY)**
|
||||
|
||||
## Geo planned failover runbook 1
|
||||
|
||||
| Component | Configuration |
|
||||
| ----------- | --------------- |
|
||||
| PostgreSQL | Omnibus-managed |
|
||||
| Geo site | Single-node |
|
||||
| Secondaries | One |
|
||||
|
||||
This runbook will guide you through a planned failover of a single-node Geo site
|
||||
with one secondary. The following general architecture is assumed:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph main[Geo deployment]
|
||||
subgraph Primary[Primary site]
|
||||
Node_1[(GitLab node)]
|
||||
end
|
||||
subgraph Secondary1[Secondary site]
|
||||
Node_2[(GitLab node)]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This guide will result in the following:
|
||||
|
||||
1. An offline primary.
|
||||
1. A promoted secondary that is now the new primary.
|
||||
|
||||
What is not covered:
|
||||
|
||||
1. Re-adding the old **primary** as a secondary.
|
||||
1. Adding a new secondary.
|
||||
|
||||
### Preparation
|
||||
|
||||
NOTE: **Note:**
|
||||
Before following any of those steps, make sure you have `root` access to the
|
||||
**secondary** to promote it, since there isn't provided an automated way to
|
||||
promote a Geo replica and perform a failover.
|
||||
|
||||
On the **secondary** node, navigate to the **Admin Area > Geo** dashboard to
|
||||
review its status. Replicated objects (shown in green) should be close to 100%,
|
||||
and there should be no failures (shown in red). If a large proportion of
|
||||
objects aren't yet replicated (shown in gray), consider giving the node more
|
||||
time to complete.
|
||||
|
||||
![Replication status](img/replication-status.png)
|
||||
|
||||
If any objects are failing to replicate, this should be investigated before
|
||||
scheduling the maintenance window. After a planned failover, anything that
|
||||
failed to replicate will be **lost**.
|
||||
|
||||
You can use the
|
||||
[Geo status API](../../../api/geo_nodes.md#retrieve-project-sync-or-verification-failures-that-occurred-on-the-current-node)
|
||||
to review failed objects and the reasons for failure.
|
||||
A common cause of replication failures is the data being missing on the
|
||||
**primary** node - you can resolve these failures by restoring the data from backup,
|
||||
or removing references to the missing data.
|
||||
|
||||
The maintenance window won't end until Geo replication and verification is
|
||||
completely finished. To keep the window as short as possible, you should
|
||||
ensure these processes are close to 100% as possible during active use.
|
||||
|
||||
If the **secondary** node is still replicating data from the **primary** node,
|
||||
follow these steps to avoid unnecessary data loss:
|
||||
|
||||
1. Until a [read-only mode](https://gitlab.com/gitlab-org/gitlab/-/issues/14609)
|
||||
is implemented, updates must be prevented from happening manually to the
|
||||
**primary**. Note that your **secondary** node still needs read-only
|
||||
access to the **primary** node during the maintenance window:
|
||||
|
||||
1. At the scheduled time, using your cloud provider or your node's firewall, block
|
||||
all HTTP, HTTPS and SSH traffic to/from the **primary** node, **except** for your IP and
|
||||
the **secondary** node's IP.
|
||||
|
||||
For instance, you can run the following commands on the **primary** node:
|
||||
|
||||
```shell
|
||||
sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 22 -j ACCEPT
|
||||
sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 22 -j ACCEPT
|
||||
sudo iptables -A INPUT --destination-port 22 -j REJECT
|
||||
|
||||
sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 80 -j ACCEPT
|
||||
sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 80 -j ACCEPT
|
||||
sudo iptables -A INPUT --tcp-dport 80 -j REJECT
|
||||
|
||||
sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 443 -j ACCEPT
|
||||
sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 443 -j ACCEPT
|
||||
sudo iptables -A INPUT --tcp-dport 443 -j REJECT
|
||||
```
|
||||
|
||||
From this point, users will be unable to view their data or make changes on the
|
||||
**primary** node. They will also be unable to log in to the **secondary** node.
|
||||
However, existing sessions will work for the remainder of the maintenance period, and
|
||||
public data will be accessible throughout.
|
||||
|
||||
1. Verify the **primary** node is blocked to HTTP traffic by visiting it in browser via
|
||||
another IP. The server should refuse connection.
|
||||
|
||||
1. Verify the **primary** node is blocked to Git over SSH traffic by attempting to pull an
|
||||
existing Git repository with an SSH remote URL. The server should refuse
|
||||
connection.
|
||||
|
||||
1. On the **primary** node, disable non-Geo periodic background jobs by navigating
|
||||
to **Admin Area > Monitoring > Background Jobs > Cron**, clicking `Disable All`,
|
||||
and then clicking `Enable` for the `geo_sidekiq_cron_config_worker` cron job.
|
||||
This job will re-enable several other cron jobs that are essential for planned
|
||||
failover to complete successfully.
|
||||
|
||||
1. Finish replicating and verifying all data:
|
||||
|
||||
CAUTION: **Caution:**
|
||||
Not all data is automatically replicated. Read more about
|
||||
[what is excluded](planned_failover.md#not-all-data-is-automatically-replicated).
|
||||
|
||||
1. If you are manually replicating any
|
||||
[data not managed by Geo](../replication/datatypes.md#limitations-on-replicationverification),
|
||||
trigger the final replication process now.
|
||||
1. On the **primary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
|
||||
and wait for all queues except those with `geo` in the name to drop to 0.
|
||||
These queues contain work that has been submitted by your users; failing over
|
||||
before it is completed will cause the work to be lost.
|
||||
1. On the **primary** node, navigate to **Admin Area > Geo** and wait for the
|
||||
following conditions to be true of the **secondary** node you are failing over to:
|
||||
- All replication meters to each 100% replicated, 0% failures.
|
||||
- All verification meters reach 100% verified, 0% failures.
|
||||
- Database replication lag is 0ms.
|
||||
- The Geo log cursor is up to date (0 events behind).
|
||||
|
||||
1. On the **secondary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
|
||||
and wait for all the `geo` queues to drop to 0 queued and 0 running jobs.
|
||||
1. On the **secondary** node, use [these instructions](../../raketasks/check.md)
|
||||
to verify the integrity of CI artifacts, LFS objects, and uploads in file
|
||||
storage.
|
||||
|
||||
At this point, your **secondary** node will contain an up-to-date copy of everything the
|
||||
**primary** node has, meaning nothing will be lost when you fail over.
|
||||
|
||||
1. In this final step, you need to permanently disable the **primary** node.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
When the **primary** node goes offline, there may be data saved on the **primary** node
|
||||
that has not been replicated to the **secondary** node. This data should be treated
|
||||
as lost if you proceed.
|
||||
|
||||
TIP: **Tip:**
|
||||
If you plan to [update the **primary** domain DNS record](index.md#step-4-optional-updating-the-primary-domain-dns-record),
|
||||
you may wish to lower the TTL now to speed up propagation.
|
||||
|
||||
When performing a failover, we want to avoid a split-brain situation where
|
||||
writes can occur in two different GitLab instances. So to prepare for the
|
||||
failover, you must disable the **primary** node:
|
||||
|
||||
- If you have SSH access to the **primary** node, stop and disable GitLab:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl stop
|
||||
```
|
||||
|
||||
Prevent GitLab from starting up again if the server unexpectedly reboots:
|
||||
|
||||
```shell
|
||||
sudo systemctl disable gitlab-runsvdir
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
(**CentOS only**) In CentOS 6 or older, there is no easy way to prevent GitLab from being
|
||||
started if the machine reboots isn't available (see [Omnibus GitLab issue #3058](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3058)).
|
||||
It may be safest to uninstall the GitLab package completely with `sudo yum remove gitlab-ee`.
|
||||
|
||||
NOTE: **Note:**
|
||||
(**Ubuntu 14.04 LTS**) If you are using an older version of Ubuntu
|
||||
or any other distribution based on the Upstart init system, you can prevent GitLab
|
||||
from starting if the machine reboots as `root` with
|
||||
`initctl stop gitlab-runsvvdir && echo 'manual' > /etc/init/gitlab-runsvdir.override && initctl reload-configuration`.
|
||||
|
||||
- If you do not have SSH access to the **primary** node, take the machine offline and
|
||||
prevent it from rebooting. Since there are many ways you may prefer to accomplish
|
||||
this, we will avoid a single recommendation. You may need to:
|
||||
|
||||
- Reconfigure the load balancers.
|
||||
- Change DNS records (for example, point the **primary** DNS record to the **secondary**
|
||||
node in order to stop usage of the **primary** node).
|
||||
- Stop the virtual servers.
|
||||
- Block traffic through a firewall.
|
||||
- Revoke object storage permissions from the **primary** node.
|
||||
- Physically disconnect a machine.
|
||||
|
||||
### Promoting the **secondary** node
|
||||
|
||||
Note the following when promoting a secondary:
|
||||
|
||||
- A new **secondary** should not be added at this time. If you want to add a new
|
||||
**secondary**, do this after you have completed the entire process of promoting
|
||||
the **secondary** to the **primary**.
|
||||
- If you encounter an `ActiveRecord::RecordInvalid: Validation failed: Name has already been taken`
|
||||
error during this process, read
|
||||
[the troubleshooting advice](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-node).
|
||||
|
||||
To promote the secondary node:
|
||||
|
||||
1. SSH in to your **secondary** node and login as root:
|
||||
|
||||
```shell
|
||||
sudo -i
|
||||
```
|
||||
|
||||
1. Edit `/etc/gitlab/gitlab.rb` to reflect its new status as **primary** by
|
||||
removing any lines that enabled the `geo_secondary_role`:
|
||||
|
||||
```ruby
|
||||
## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
|
||||
geo_secondary_role['enable'] = true
|
||||
|
||||
## In 11.5+ documentation, the role was enabled as follows. Remove this line.
|
||||
roles ['geo_secondary_role']
|
||||
```
|
||||
|
||||
1. Run the following command to list out all preflight checks and automatically
|
||||
check if replication and verification are complete before scheduling a planned
|
||||
failover to ensure the process will go smoothly:
|
||||
|
||||
```shell
|
||||
gitlab-ctl promotion-preflight-checks
|
||||
```
|
||||
|
||||
1. Promote the **secondary**:
|
||||
|
||||
```shell
|
||||
gitlab-ctl promote-to-primary-node
|
||||
```
|
||||
|
||||
If you have already run the [preflight checks](planned_failover.md#preflight-checks)
|
||||
or don't want to run them, you can skip them:
|
||||
|
||||
```shell
|
||||
gitlab-ctl promote-to-primary-node --skip-preflight-check
|
||||
```
|
||||
|
||||
You can also promote the secondary node to primary **without any further confirmation**, even when preflight checks fail:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl promote-to-primary-node --force
|
||||
```
|
||||
|
||||
1. Verify you can connect to the newly promoted **primary** node using the URL used
|
||||
previously for the **secondary** node.
|
||||
|
||||
If successful, the **secondary** node has now been promoted to the **primary** node.
|
||||
|
||||
### Next steps
|
||||
|
||||
To regain geographic redundancy as quickly as possible, you should
|
||||
[add a new **secondary** node](../setup/index.md). To
|
||||
do that, you can re-add the old **primary** as a new secondary and bring it back
|
||||
online.
|
|
@ -1196,9 +1196,16 @@ CAUTION: **Caution:**
|
|||
|
||||
## Data recovery
|
||||
|
||||
If a Gitaly node fails replication jobs for any reason, it ends up hosting outdated versions of
|
||||
the affected repositories. Praefect provides tools for automatically or manually reconciling
|
||||
the outdated repositories in order to bring them fully up to date again.
|
||||
If a Gitaly node fails replication jobs for any reason, it ends up hosting outdated versions of the
|
||||
affected repositories. Praefect provides tools for:
|
||||
|
||||
- [Automatic](#automatic-reconciliation) reconciliation, for GitLab 13.4 and later.
|
||||
- [Manual](#manual-reconciliation) reconciliation, for:
|
||||
- GitLab 13.3 and earlier.
|
||||
- Repositories upgraded to GitLab 13.4 and later without entries in the `repositories` table.
|
||||
A migration tool [is planned](https://gitlab.com/gitlab-org/gitaly/-/issues/3033).
|
||||
|
||||
These tools reconcile the outdated repositories to bring them fully up to date again.
|
||||
|
||||
### Automatic reconciliation
|
||||
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
## List users
|
||||
|
||||
Active users = Total accounts - Blocked users
|
||||
|
||||
Get a list of users.
|
||||
|
||||
This function takes pagination parameters `page` and `per_page` to restrict the list of users.
|
||||
|
@ -49,9 +47,9 @@ For example:
|
|||
GET /users?username=jack_smith
|
||||
```
|
||||
|
||||
In addition, you can filter users based on states eg. `blocked`, `active`
|
||||
This works only to filter users who are `blocked` or `active`.
|
||||
It does not support `active=false` or `blocked=false`.
|
||||
In addition, you can filter users based on the states `blocked` and `active`.
|
||||
It does not support `active=false` or `blocked=false`. The list of active users
|
||||
is the total number of users minus the blocked users.
|
||||
|
||||
```plaintext
|
||||
GET /users?active=true
|
||||
|
|
|
@ -57,6 +57,7 @@ Complementary reads:
|
|||
- [Generate a changelog entry with `bin/changelog`](changelog.md)
|
||||
- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLab team members)
|
||||
- [Patch release process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/patch/process.md#process-for-developers)
|
||||
- [Adding a new service component to GitLab](adding_service_component.md)
|
||||
|
||||
## UX and Frontend guides
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
# Adding a new Service Component to GitLab
|
||||
|
||||
The GitLab product is made up of several service components that run as independent system processes in communication with each other. These services can be run on the same instance, or spread across different instances. A list of the existing components can be found in the [GitLab architecture overview](architecture.md).
|
||||
|
||||
## Integration phases
|
||||
|
||||
The following outline re-uses the [maturity metric](https://about.gitlab.com/direction/maturity) naming as an example of the various phases of integrating a component. These are only loosely coupled to a components actual maturity, and are intended as a guide for implementation order (for example, a component does not need to be enabled by default to be Lovable, and being enabled by default does not on its own cause a component to be Lovable).
|
||||
|
||||
- Proposed
|
||||
- [Proposing a new component](#proposing-a-new-component)
|
||||
- Minimal
|
||||
- [Integrating a new service with GitLab](#integrating-a-new-service-with-gitlab)
|
||||
- [Handling service dependencies](#handling-service-dependencies)
|
||||
- Viable
|
||||
- [Bundled with GitLab installations](#bundling-a-service-with-gitlab)
|
||||
- [End-to-end testing in GitLab QA](testing_guide/end_to_end/beginners_guide.md)
|
||||
- [Release management](#release-management)
|
||||
- [Enabled on GitLab.com](feature_flags/controls.md#enabling-a-feature-for-gitlabcom)
|
||||
- Complete
|
||||
- [Configurable by the GitLab orchestrator](https://gitlab.com/gitlab-org/gitlab-orchestrator)
|
||||
- Lovable
|
||||
- Enabled by default for the majority of users
|
||||
|
||||
## Proposing a new component
|
||||
|
||||
The initial step for integrating a new component with GitLab starts with creating a [Feature proposal in the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal).
|
||||
|
||||
Identify the [product category](https://about.gitlab.com/handbook/product/categories/) the component falls under and assign the Engineering Manager and Product Manager responsible for that category.
|
||||
|
||||
The general steps for getting any GitLab feature from proposal to release can be found in the [Product development flow](https://about.gitlab.com/handbook/product-development-flow/).
|
||||
|
||||
## Integrating a new service with GitLab
|
||||
|
||||
Adding a new service follows the same [merge request workflow](contributing/merge_request_workflow.md) as other contributions, and must meet the same [completion criteria](contributing/merge_request_workflow.md#definition-of-done) and in addition needs to cover the following:
|
||||
|
||||
- The [architecture component list](architecture.md#component-list) has been updated to include the service.
|
||||
- Features provided by the component have been accepted into the [GitLab Product Direction](https://about.gitlab.com/direction/).
|
||||
- Documentation is available and the support team has been made aware of the new component.
|
||||
|
||||
**For services that can operate completely separate from GitLab:**
|
||||
|
||||
The first iteration should be to add the ability to connect and use the service as an externally installed component. Often this involves providing settings in GitLab to connect to the service, or allow connections from it. And then shipping documentation on how to install and configure the service with GitLab.
|
||||
|
||||
TIP: **Tip:**
|
||||
[Elasticsearch](../integration/elasticsearch.md#installing-elasticsearch) is an example of a service that has been integrated this way. And many of the other services, including internal projects like Gitaly, started off as separately installed alternatives.
|
||||
|
||||
**For services that depend on the existing GitLab codebase:**
|
||||
|
||||
The first iteration should be opt-in, either through the `gitlab.yml` configuration or through [feature flags](feature_flags.md). For these types of services it is often necessary to [bundle the service and its dependencies with GitLab](#bundling-a-service-with-gitlab) as part of the initial integration.
|
||||
|
||||
TIP: **Tip:**
|
||||
[ActionCable](https://docs.gitlab.com/omnibus/settings/actioncable.html) is an example of a service that has been added this way.
|
||||
|
||||
## Bundling a service with GitLab
|
||||
|
||||
NOTE: **Note:**
|
||||
Code shipped with GitLab needs to use a license approved by the Legal team. See the list of [existing approved licenses](https://about.gitlab.com/handbook/engineering/open-source/#using-open-source-libraries).
|
||||
|
||||
NOTE: **Note:**
|
||||
Notify the [Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/) when adding a new dependency that must be compiled. We must be able to compile the dependency on all supported platforms.
|
||||
|
||||
New services to be bundled with GitLab need to be available in the following environments.
|
||||
|
||||
**Dev environment**
|
||||
|
||||
The first step of bundling a new service is to provide it in the development environment to engage in collaboration and feedback.
|
||||
|
||||
- [Include in the GDK](https://gitlab.com/gitlab-org/gitlab-development-kit)
|
||||
- [Include in the source install instructions](../install/installation.md)
|
||||
|
||||
**Standard install methods**
|
||||
|
||||
In order for a service to be bundled for end-users or GitLab.com, it needs to be included in the standard install methods:
|
||||
|
||||
- [Included in the Omnibus package](https://gitlab.com/gitlab-org/omnibus-gitlab)
|
||||
- [Included in the GitLab Helm charts](https://gitlab.com/gitlab-org/charts/gitlab)
|
||||
|
||||
## Handling service dependencies
|
||||
|
||||
Dependencies should be kept up to date and be tracked for security updates. For the Rails codebase, the JavaScript and Ruby dependencies are
|
||||
scanned for vulnerabilities using GitLab [dependency scanning](../user/application_security/dependency_scanning/index.md).
|
||||
|
||||
In addition, any system dependencies used in Omnibus packages or the Cloud Native images should be added to the [dependency update automation](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/maintenance/dependencies.io.html#adding-new-dependencies).
|
||||
|
||||
## Release management
|
||||
|
||||
If the service component needs to be updated or released with the monthly GitLab release, then the component should be added to the [release tools automation](https://gitlab.com/gitlab-org/release-tools). This project is maintained by the [Delivery team](https://about.gitlab.com/handbook/engineering/infrastructure/team/delivery/). A list of the projects managed this way can be found in the [release tools project directory](https://about.gitlab.com/handbook/engineering/infrastructure/team/delivery/).
|
||||
|
||||
For example, during the monthly GitLab release, the desired version of Gitaly, GitLab Workhorse, GitLab Shell, etc., need to synchronized through the various release pipelines.
|
|
@ -242,6 +242,7 @@ request:
|
|||
1. The [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
|
||||
1. The [CI environment preparation](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/prepare_build.sh).
|
||||
1. The [Omnibus package creator](https://gitlab.com/gitlab-org/omnibus-gitlab).
|
||||
1. The [Cloud Native GitLab Dockerfiles](https://gitlab.com/gitlab-org/build/CNG)
|
||||
|
||||
## Incremental improvements
|
||||
|
||||
|
|
|
@ -1120,16 +1120,14 @@ they need to interact with the application.
|
|||
When you take screenshots:
|
||||
|
||||
- *Capture the most relevant area of the page.* Don't include unnecessary white
|
||||
space or areas of the page that don't help illustrate your point. Also, don't
|
||||
include the entire page if you don't have to, but also ensure the image
|
||||
contains enough information to allow the user to determine where things are.
|
||||
- *Be consistent.* Find a browser window size that works for you that also
|
||||
displays all areas of the product, including the left navigation (usually >
|
||||
1200px wide). For consistency, use this browser window size for your
|
||||
screenshots by installing a browser extension for setting a window to a
|
||||
specific size (for example,
|
||||
[Window Resizer](https://chrome.google.com/webstore/detail/window-resizer/kkelicaakdanhinjdeammmilcgefonfh/related?hl=en)
|
||||
for Google Chrome).
|
||||
space or areas of the page that don't help illustrate the point. The left
|
||||
sidebar of the GitLab user interface can change, so don't include the sidebar
|
||||
if it's not necessary.
|
||||
- *Keep it small.* If you don't need to show the full width of the screen, don't.
|
||||
A value of 1000 pixels is a good maximum width for your screenshot image.
|
||||
- *Be consistent.* Coordinate screenshots with the other screenshots already on
|
||||
a documentation page. For example, if other screenshots include the left
|
||||
sidebar, include the sidebar in all screenshots.
|
||||
|
||||
### Save the image
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ Each feature flag is defined in a separate YAML file consisting of a number of f
|
|||
|---------------------|----------|----------------------------------------------------------------|
|
||||
| `name` | yes | Name of the feature flag. |
|
||||
| `type` | yes | Type of feature flag. |
|
||||
| `default_enabled` | yes | The default state of the feature flag that is strongly validated, with `default_enabled:` passed as an argument. |
|
||||
| `default_enabled` | yes | The default state of the feature flag that is strictly validated, with `default_enabled:` passed as an argument. |
|
||||
| `introduced_by_url` | no | The URL to the Merge Request that introduced the feature flag. |
|
||||
| `rollout_issue_url` | no | The URL to the Issue covering the feature flag rollout. |
|
||||
| `group` | no | The [group](https://about.gitlab.com/handbook/product/product-categories/#devops-stages) that owns the feature flag. |
|
||||
|
@ -129,16 +129,16 @@ Only feature flags that have a YAML definition file can be used when running the
|
|||
|
||||
```shell
|
||||
$ bin/feature-flag my-feature-flag
|
||||
>> Please specify the group introducing feature flag, like `group::apm`:
|
||||
>> Specify the group introducing the feature flag, like `group::apm`:
|
||||
?> group::memory
|
||||
|
||||
>> If you have MR open, can you paste the URL here? (or enter to skip)
|
||||
>> URL of the MR introducing the feature flag (enter to skip):
|
||||
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
|
||||
|
||||
>> Open this URL and fill the rest of details:
|
||||
>> Open this URL and fill in the rest of the details:
|
||||
https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue%5Btitle%5D=%5BFeature+flag%5D+Rollout+of+%60test-flag%60&issuable_template=Feature+Flag+Roll+Out
|
||||
|
||||
>> Paste URL of `rollout issue` here, or enter to skip:
|
||||
>> URL of the rollout issue (enter to skip):
|
||||
?> https://gitlab.com/gitlab-org/gitlab/-/issues/232533
|
||||
create config/feature_flags/development/test-flag.yml
|
||||
---
|
||||
|
@ -305,7 +305,7 @@ used as an actor for `Feature.enabled?`.
|
|||
### Feature flags for licensed features
|
||||
|
||||
If a feature is license-gated, there's no need to add an additional
|
||||
explicit feature flag check since the flag will be checked as part of the
|
||||
explicit feature flag check since the flag is checked as part of the
|
||||
`License.feature_available?` call. Similarly, there's no need to "clean up" a
|
||||
feature flag once the feature has reached general availability.
|
||||
|
||||
|
@ -316,7 +316,7 @@ a by default enabled feature flag with the same name as the provided argument.
|
|||
|
||||
**An important side-effect of the implicit feature flags mentioned above is that
|
||||
unless the feature is explicitly disabled or limited to a percentage of users,
|
||||
the feature flag check will default to `true`.**
|
||||
the feature flag check defaults to `true`.**
|
||||
|
||||
NOTE: **Note:**
|
||||
Due to limitations with `feature_available?`, the YAML definition for `licensed` feature
|
||||
|
@ -361,9 +361,9 @@ default_enabled: [false, true]
|
|||
|
||||
Feature groups must be defined statically in `lib/feature.rb` (in the
|
||||
`.register_feature_groups` method), but their implementation can obviously be
|
||||
dynamic (querying the DB etc.).
|
||||
dynamic (querying the DB, for example).
|
||||
|
||||
Once defined in `lib/feature.rb`, you will be able to activate a
|
||||
Once defined in `lib/feature.rb`, you can to activate a
|
||||
feature for a given feature group via the [`feature_group` parameter of the features API](../../api/features.md#set-or-create-a-feature)
|
||||
|
||||
### Enabling a feature flag locally (in development)
|
||||
|
@ -374,7 +374,7 @@ In the rails console (`rails c`), enter the following command to enable a featur
|
|||
Feature.enable(:feature_flag_name)
|
||||
```
|
||||
|
||||
Similarly, the following command will disable a feature flag:
|
||||
Similarly, the following command disables a feature flag:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:feature_flag_name)
|
||||
|
@ -388,7 +388,7 @@ Feature.enable(:feature_flag_name, Project.find_by_full_path("root/my-project"))
|
|||
|
||||
## Feature flags in tests
|
||||
|
||||
Introducing a feature flag into the codebase creates an additional codepath that should be tested.
|
||||
Introducing a feature flag into the codebase creates an additional code path that should be tested.
|
||||
It is strongly advised to test all code affected by a feature flag, both when **enabled** and **disabled**
|
||||
to ensure the feature works properly.
|
||||
|
||||
|
@ -423,10 +423,10 @@ Feature.enabled?(:ci_live_trace, project2) # => false
|
|||
|
||||
The behavior of FlipperGate is as follows:
|
||||
|
||||
1. You can enable an override for a specified actor to be enabled
|
||||
1. You can enable an override for a specified actor to be enabled.
|
||||
1. You can disable (remove) an override for a specified actor,
|
||||
falling back to default state
|
||||
1. There's no way to model that you explicitly disable a specified actor
|
||||
falling back to the default state.
|
||||
1. There's no way to model that you explicitly disabled a specified actor.
|
||||
|
||||
```ruby
|
||||
Feature.enable(:my_feature)
|
||||
|
@ -467,7 +467,7 @@ Feature.enable_percentage_of_time(:my_feature_3, 50)
|
|||
Feature.enable_percentage_of_actors(:my_feature_4, 50)
|
||||
```
|
||||
|
||||
Each feature flag that has a defined state will be persisted
|
||||
Each feature flag that has a defined state is persisted
|
||||
during test execution time:
|
||||
|
||||
```ruby
|
||||
|
|
|
@ -235,11 +235,10 @@ For example, to add support for files referenced by a `Widget` model with a
|
|||
`ee/lib/gitlab/geo.rb`:
|
||||
|
||||
```ruby
|
||||
def self.replicator_classes
|
||||
classes = [::Geo::PackageFileReplicator,
|
||||
::Geo::WidgetReplicator]
|
||||
|
||||
classes.select(&:enabled?)
|
||||
REPLICATOR_CLASSES = [
|
||||
::Geo::PackageFileReplicator,
|
||||
::Geo::WidgetReplicator
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
|
@ -315,10 +314,6 @@ For example, to add support for files referenced by a `Widget` model with a
|
|||
end
|
||||
```
|
||||
|
||||
The method `has_create_events?` should return `true` in most of the cases.
|
||||
However, if the entity you add doesn't have the create event, don't add the
|
||||
method at all.
|
||||
|
||||
1. Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
|
||||
|
||||
1. Add `widget_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
|
||||
|
@ -435,7 +430,7 @@ for verification state to the widgets table:
|
|||
```
|
||||
|
||||
1. Add a partial index on `verification_failure` and `verification_checksum` to ensure
|
||||
re-verification can be performed efficiently. Add a migration in `ee/db/geo/migrate/`:
|
||||
re-verification can be performed efficiently:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
@ -461,9 +456,9 @@ for verification state to the widgets table:
|
|||
|
||||
##### Option 2: Create a separate `widget_states` table with verification state fields
|
||||
|
||||
1. Add a migration in `ee/db/geo/migrate/` to create a `widget_states` table and add a
|
||||
partial index on `verification_failure` and `verification_checksum` to ensure
|
||||
re-verification can be performed efficiently. Order the columns according to [our guidelines](../ordering_table_columns.md):
|
||||
1. Create a `widget_states` table and add a partial index on `verification_failure` and
|
||||
`verification_checksum` to ensure re-verification can be performed efficiently. Order
|
||||
the columns according to [our guidelines](../ordering_table_columns.md):
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
# Redis guidelines
|
||||
|
||||
GitLab uses [Redis](https://redis.io) for three distinct purposes:
|
||||
GitLab uses [Redis](https://redis.io) for the following distinct purposes:
|
||||
|
||||
- Caching via `Rails.cache`.
|
||||
- Caching (mostly via `Rails.cache`).
|
||||
- As a job processing queue with [Sidekiq](sidekiq_style_guide.md).
|
||||
- To manage the shared application state.
|
||||
- As a Pub/Sub queue backend for ActionCable.
|
||||
|
||||
In most environments (including the GDK), all of these point to the same
|
||||
Redis instance.
|
||||
|
||||
On GitLab.com, we use [separate Redis
|
||||
instances](../administration/redis/replication_and_failover.md#running-multiple-redis-clusters).
|
||||
(We do not currently use [ActionCable on
|
||||
GitLab.com](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/228)).
|
||||
|
||||
Every application process is configured to use the same Redis servers, so they
|
||||
can be used for inter-process communication in cases where [PostgreSQL](sql.md)
|
||||
|
@ -21,11 +30,11 @@ to key names to avoid collisions. Typically we use colon-separated elements to
|
|||
provide a semblance of structure at application level. An example might be
|
||||
`projects:1:somekey`.
|
||||
|
||||
Although we split our Redis usage into three separate purposes, and those may
|
||||
map to separate Redis servers in a [Highly Available](../administration/high_availability/redis.md)
|
||||
configuration, the default Omnibus and GDK setups share a single Redis server.
|
||||
This means that keys should **always** be globally unique across the three
|
||||
purposes.
|
||||
Although we split our Redis usage by purpose into distinct categories, and
|
||||
those may map to separate Redis servers in a Highly Available
|
||||
configuration like GitLab.com, the default Omnibus and GDK setups share
|
||||
a single Redis server. This means that keys should **always** be
|
||||
globally unique across all categories.
|
||||
|
||||
It is usually better to use immutable identifiers - project ID rather than
|
||||
full path, for instance - in Redis key names. If full path is used, the key will
|
||||
|
@ -56,3 +65,127 @@ Currently, we validate this in the development and test environments
|
|||
with the [`RedisClusterValidator`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/instrumentation/redis_cluster_validator.rb),
|
||||
which is enabled for the `cache` and `shared_state`
|
||||
[Redis instances](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances)..
|
||||
|
||||
## Redis in structured logging
|
||||
|
||||
Our [structured logging](logging.md#use-structured-json-logging) for web
|
||||
requests and Sidekiq jobs contains fields for the duration, call count,
|
||||
bytes written, and bytes read per Redis instance, along with a total for
|
||||
all Redis instances. For a particular request, this might look like:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| `json.queue_duration_s` | 0.01 |
|
||||
| `json.redis_cache_calls` | 1 |
|
||||
| `json.redis_cache_duration_s` | 0 |
|
||||
| `json.redis_cache_read_bytes` | 109 |
|
||||
| `json.redis_cache_write_bytes` | 49 |
|
||||
| `json.redis_calls` | 2 |
|
||||
| `json.redis_duration_s` | 0.001 |
|
||||
| `json.redis_read_bytes` | 111 |
|
||||
| `json.redis_shared_state_calls` | 1 |
|
||||
| `json.redis_shared_state_duration_s` | 0 |
|
||||
| `json.redis_shared_state_read_bytes` | 2 |
|
||||
| `json.redis_shared_state_write_bytes` | 206 |
|
||||
| `json.redis_write_bytes` | 255 |
|
||||
|
||||
As all of these fields are indexed, it is then straightforward to
|
||||
investigate Redis usage in production. For instance, to find the
|
||||
requests that read the most data from the cache, we can just sort by
|
||||
`redis_cache_read_bytes` in descending order.
|
||||
|
||||
### The slow log
|
||||
|
||||
On GitLab.com, entries from the [Redis
|
||||
slow log](https://redis.io/commands/slowlog) are available in the
|
||||
`pubsub-redis-inf-gprd*` index with the [`redis.slowlog`
|
||||
tag](https://log.gprd.gitlab.net/app/kibana#/discover?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-1d,to:now))&_a=(columns:!(json.type,json.command,json.exec_time),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:AWSQX_Vf93rHTYrsexmk,key:json.tag,negate:!f,params:(query:redis.slowlog),type:phrase),query:(match:(json.tag:(query:redis.slowlog,type:phrase))))),index:AWSQX_Vf93rHTYrsexmk)).
|
||||
This shows commands that have taken a long time and may be a performance
|
||||
concern.
|
||||
|
||||
The
|
||||
[fluent-plugin-redis-slowlog](https://gitlab.com/gitlab-org/fluent-plugin-redis-slowlog)
|
||||
project is responsible for taking the slowlog entries from Redis and
|
||||
passing to fluentd (and ultimately Elasticsearch).
|
||||
|
||||
## Analyzing the entire keyspace
|
||||
|
||||
The [Redis Keyspace
|
||||
Analyzer](https://gitlab.com/gitlab-com/gl-infra/redis-keyspace-analyzer)
|
||||
project contains tools for dumping the full key list and memory usage of a Redis
|
||||
instance, and then analyzing those lists while elimating potentially sensitive
|
||||
data from the results. It can be used to find the most frequent key patterns, or
|
||||
those that use the most memory.
|
||||
|
||||
Currently this is not run automatically for the GitLab.com Redis instances, but
|
||||
is run manually on an as-needed basis.
|
||||
|
||||
## Utility classes
|
||||
|
||||
We have some extra classes to help with specific use cases. These are
|
||||
mostly for fine-grained control of Redis usage, so they wouldn't be used
|
||||
in combination with the `Rails.cache` wrapper: we'd either use
|
||||
`Rails.cache` or these classes and literal Redis commands.
|
||||
|
||||
`Rails.cache` or these classes and literal Redis commands. We prefer
|
||||
using `Rails.cache` so we can reap the benefits of future optimizations
|
||||
done to Rails. It is worth noting that Ruby objects are
|
||||
[marshalled](https://github.com/rails/rails/blob/v6.0.3.1/activesupport/lib/active_support/cache/redis_cache_store.rb#L447)
|
||||
when written to Redis, so we need to pay attention to not to store huge
|
||||
objects, or untrusted user input.
|
||||
|
||||
Typically we would only use these classes when at least one of the
|
||||
following is true:
|
||||
|
||||
1. We want to manipulate data on a non-cache Redis instance.
|
||||
1. `Rails.cache` does not support the operations we want to perform.
|
||||
|
||||
### `Gitlab::Redis::{Cache,SharedState,Queues}`
|
||||
|
||||
These classes wrap the Redis instances (using
|
||||
[`Gitlab::Redis::Wrapper`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/wrapper.rb))
|
||||
to make it convenient to work with them directly. The typical use is to
|
||||
call `.with` on the class, which takes a block that yields the Redis
|
||||
connection. For example:
|
||||
|
||||
```ruby
|
||||
# Get the value of `key` from the shared state (persistent) Redis
|
||||
Gitlab::Redis::SharedState.with { |redis| redis.get(key) }
|
||||
|
||||
# Check if `value` is a member of the set `key`
|
||||
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }
|
||||
```
|
||||
|
||||
### `Gitlab::Redis::Boolean`
|
||||
|
||||
In Redis, every value is a string.
|
||||
[`Gitlab::Redis::Boolean`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/boolean.rb)
|
||||
makes sure that booleans are encoded and decoded consistently.
|
||||
|
||||
### `Gitlab::Redis::HLL`
|
||||
|
||||
The Redis [`PFCOUNT`](https://redis.io/commands/pfcount),
|
||||
[`PFADD`](https://redis.io/commands/pfadd), and
|
||||
[`PFMERGE`](https://redis.io/commands/pfmergge) commands operate on
|
||||
HyperLogLogs, a data structure that allows estimating the number of unique
|
||||
elements with low memory usage. (In addition to the `PFCOUNT` documentation,
|
||||
Thoughtbot's article on [HyperLogLogs in
|
||||
Redis](https://thoughtbot.com/blog/hyperloglogs-in-redis) provides a good
|
||||
background here.)
|
||||
|
||||
[`Gitlab::Redis::HLL`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/redis/hll.rb)
|
||||
provides a convenient interface for adding and counting values in HyperLogLogs.
|
||||
|
||||
### `Gitlab::SetCache`
|
||||
|
||||
For cases where we need to efficiently check the whether an item is in a group
|
||||
of items, we can use a Redis set.
|
||||
[`Gitlab::SetCache`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/set_cache.rb)
|
||||
provides an `#include?` method that will use the
|
||||
[`SISMEMBER`](https://redis.io/commands/sismember) command, as well as `#read`
|
||||
to fetch all entries in the set.
|
||||
|
||||
This is used by the
|
||||
[`RepositorySetCache`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/repository_set_cache.rb)
|
||||
to provide a convenient way to use sets to cache repository data like branch
|
||||
names.
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
type: reference, how-to
|
||||
stage: Create
|
||||
group: Editor
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
|
||||
---
|
||||
|
||||
# Gitpod Integration
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228893) in GitLab 13.4.
|
||||
> - It's [deployed behind a feature flag](#enable-or-disable-the-gitpod-integration), disabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#configure-your-gitlab-instance-with-gitpod). **(CORE ONLY)**
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
|
||||
With [Gitpod](https://gitpod.io/) you can describe your dev environment as code to get fully set
|
||||
up, compiled, and tested dev environments for any GitLab project. The dev environments are not only
|
||||
automated but also prebuilt which means that Gitpod continuously builds your Git branches like a CI
|
||||
server. By that you don’t have to wait for dependencies to be downloaded and builds to finish, but
|
||||
you can start coding immediately.
|
||||
|
||||
In short: With Gitpod you can start coding instantly on any project, branch, and merge request from
|
||||
any device, at any time.
|
||||
|
||||
![Gitpod interface](img/gitpod_web_interface_v13_4.png)
|
||||
|
||||
You can launch Gitpod directly from GitLab by clicking the **Gitpod** button from the **Web IDE**
|
||||
dropdown on the project page:
|
||||
|
||||
![Gitpod Button on Project Page](img/gitpod_button_project_page_v13_4.png)
|
||||
|
||||
To learn more about Gitpod, see their [features](https://www.gitpod.io/features/) and
|
||||
[documentation](https://www.gitpod.io/docs/).
|
||||
|
||||
To use the GitLab-Gitpod integration, you need to enable it from your user preferences:
|
||||
|
||||
1. From the GitLab UI, click your avatar in the top-right corner, then click **Settings**.
|
||||
1. On the left-hand nav, click **Preferences**.
|
||||
1. Under **Integrations**, find the **Gitpod** section.
|
||||
1. Check **Enable Gitpod**.
|
||||
|
||||
Users of GitLab.com can enable it and start using straightaway. Users of GitLab self-managed instances
|
||||
can follow the same steps once the integration has been enabled and configured by a GitLab administrator.
|
||||
|
||||
## Configure your GitLab instance with Gitpod **(CORE ONLY)**
|
||||
|
||||
If you are new to Gitpod, head over to the [Gitpod documentation](https://www.gitpod.io/docs/self-hosted/latest/self-hosted/)
|
||||
and get your instance up and running.
|
||||
|
||||
1. In GitLab, go to **Admin Area > Settings > Integrations**.
|
||||
1. Expand the **Gitpod** configuration section.
|
||||
1. Check **Enable Gitpod**.
|
||||
1. Add your Gitpod instance URL (for example, `https://gitpod.example.com`).
|
||||
|
||||
## Enable or disable the Gitpod integration **(CORE ONLY)**
|
||||
|
||||
The Gitpod integration is under development and not ready for production use. It is deployed behind a
|
||||
feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:gitpod)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:gitpod)
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
|
@ -64,10 +64,10 @@ Once a vulnerability is found, you can interact with it. Read more on how to
|
|||
Please note that in some cases the reported vulnerabilities provide metadata that can contain
|
||||
external links exposed in the UI. These links might not be accessible within an offline environment.
|
||||
|
||||
### Suggested Solutions for vulnerabilities
|
||||
### Automatic remediation for vulnerabilities
|
||||
|
||||
The [suggested solutions](../index.md#solutions-for-vulnerabilities-auto-remediation) feature
|
||||
(auto-remediation) is available for Dependency Scanning and Container Scanning, but may not work
|
||||
The [automatic remediation for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation) feature
|
||||
(auto-remediation) is available for offline Dependency Scanning and Container Scanning, but may not work
|
||||
depending on your instance's configuration. We can only suggest solutions, which are generally more
|
||||
current versions that have been patched, when we are able to access up-to-date registry services
|
||||
hosting the latest versions of that dependency or image.
|
||||
|
|
|
@ -182,6 +182,12 @@ Manage the availability of integrated code intelligence features powered by
|
|||
Sourcegraph. View [the Sourcegraph feature documentation](../../integration/sourcegraph.md#enable-sourcegraph-in-user-preferences)
|
||||
for more information.
|
||||
|
||||
### Gitpod
|
||||
|
||||
Enable and disable the [GitLab-Gitpod integration](../../integration/gitpod.md). This is only
|
||||
visible after the integration is configured by a GitLab administrator. View
|
||||
[the Gitpod feature documentation](../../integration/gitpod.md) for more information.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -37,7 +37,12 @@ Import your projects from Bitbucket Server to GitLab with minimal effort.
|
|||
empty changes.
|
||||
1. Attachments in Markdown are currently not imported.
|
||||
1. Task lists are not imported.
|
||||
1. Emoji reactions are not imported
|
||||
1. Emoji reactions are not imported.
|
||||
1. [LFS objects](../../../topics/git/lfs/index.md) are not imported.
|
||||
|
||||
NOTE: **Note:**
|
||||
To import a repository including LFS objects from a Bitbucket server repository, use the [Repo by URL](../import/repo_by_url.md) importer.
|
||||
|
||||
1. Project filtering does not support fuzzy search (only `starts with` or `full
|
||||
match strings` are currently supported)
|
||||
|
||||
|
|
|
@ -289,6 +289,17 @@ the command line.
|
|||
NOTE: **Note:**
|
||||
This section might move in its own document in the future.
|
||||
|
||||
### Copy the branch name for local checkout
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23767) in GitLab 13.4.
|
||||
|
||||
The merge request sidebar contains the branch reference for the source branch
|
||||
used to contribute changes for this merge request.
|
||||
|
||||
To copy the branch reference into your clipboard, click the **Copy branch name** button
|
||||
(**{copy-to-clipboard}**) in the right sidebar. You can then use it to checkout the branch locally
|
||||
via command line by running `git checkout <branch-name>`.
|
||||
|
||||
### Checkout merge requests locally through the `head` ref
|
||||
|
||||
A merge request contains all the history from a repository, plus the additional
|
||||
|
|
|
@ -15,10 +15,7 @@ type: reference, howto
|
|||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can [disable it](#enable-or-disable-project-access-tokens).
|
||||
|
||||
Project access tokens are scoped to a project and can be used to authenticate with the [GitLab API](../../../api/README.md#personalproject-access-tokens).
|
||||
|
||||
<!-- Commented out until https://gitlab.com/gitlab-org/gitlab/-/issues/219551 is fixed -->
|
||||
<!-- You can also use project access tokens with Git to authenticate over HTTP or SSH. -->
|
||||
Project access tokens are scoped to a project and can be used to authenticate with the [GitLab API](../../../api/README.md#personalproject-access-tokens). You can also use project access tokens with Git to authenticate over HTTP or SSH.
|
||||
|
||||
Project access tokens expire on the date you define, at midnight UTC.
|
||||
|
||||
|
|
|
@ -53,21 +53,42 @@ If you are missing Syntax Highlighting support for any language, we prepared a s
|
|||
NOTE: **Note:**
|
||||
Single file editing is based on the [Ace Editor](https://ace.c9.io).
|
||||
|
||||
### Schema based validation
|
||||
### Themes
|
||||
|
||||
> - Support for `.gitlab-ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0.
|
||||
> - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1.
|
||||
|
||||
All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor.
|
||||
You can pick a theme from your [profile preferences](../../profile/preferences.md).
|
||||
|
||||
The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and
|
||||
the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
|
||||
which apply to the entire Web IDE screen.
|
||||
|
||||
| Solarized Light Theme | Solarized Dark Theme | Dark Theme |
|
||||
|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------|
|
||||
| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) |
|
||||
|
||||
## Schema based validation
|
||||
|
||||
> - Support for validation based on predefined schemas [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
|
||||
> - It was deployed behind a feature flag, disabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It cannot be enabled or disabled per-project.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-schema-based-validation).
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-validation-based-on-predefined-schemas).
|
||||
> - Support for validation based on custom schemas [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/226982) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
|
||||
|
||||
The Web IDE provides validation support for certain JSON and YAML files using schemas
|
||||
based on the [JSON Schema Store](https://www.schemastore.org/json/). This feature is
|
||||
only supported for the `.gitlab-ci.yml` file.
|
||||
based on the [JSON Schema Store](https://www.schemastore.org/json/).
|
||||
|
||||
#### Enable or disable Schema based validation **(CORE ONLY)**
|
||||
### Predefined schemas
|
||||
|
||||
Schema based validation is under development and not ready for production use. It is
|
||||
The Web IDE has validation for certain files built in. This feature is only supported for
|
||||
the `*.gitlab-ci.yml` files.
|
||||
|
||||
#### Enable or disable validation based on predefined schemas **(CORE ONLY)**
|
||||
|
||||
Validation based on predefined schemas is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default** for self-managed instances,
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it for your instance.
|
||||
|
@ -84,21 +105,35 @@ To disable it:
|
|||
Feature.disable(:schema_linting)
|
||||
```
|
||||
|
||||
### Themes
|
||||
### Custom schemas **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0.
|
||||
> - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/226982) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
|
||||
|
||||
All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor.
|
||||
You can pick a theme from your [profile preferences](../../profile/preferences.md).
|
||||
The Web IDE also allows you to define custom schemas for certain JSON/YAML files in your project.
|
||||
You can do so by defining a `schemas` entry in the `.gitlab/.gitlab-webide.yml` file inside the
|
||||
repository's root. Here is an example configuration:
|
||||
|
||||
The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and
|
||||
the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
|
||||
which apply to the entire Web IDE screen.
|
||||
```yaml
|
||||
schemas:
|
||||
- uri: https://json.schemastore.org/package
|
||||
match:
|
||||
- package.json
|
||||
- uri: https://somewebsite.com/first/raw/url
|
||||
match:
|
||||
- data/release_posts/unreleased/*.{yml,yaml}
|
||||
- uri: https://somewebsite.com/second/raw/url
|
||||
match:
|
||||
- "*.meta.json"
|
||||
```
|
||||
|
||||
| Solarized Light Theme | Solarized Dark Theme | Dark Theme |
|
||||
|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------|
|
||||
| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) |
|
||||
Each schema entry supports two properties:
|
||||
|
||||
- `uri`: please provide an absolute URL for the schema definition file here. The schema from this URL
|
||||
is loaded when a matching file is open.
|
||||
- `match`: a list of matching paths or glob expressions. If a schema matches a particular path pattern,
|
||||
it will be applied to that file. Please enclose the pattern in quotes if it begins with an asterisk (`*`),
|
||||
it's be applied to that file. If a pattern begins with an asterisk (`*`), enclose it in quotation
|
||||
marks. Otherwise, the configuration file is not valid YAML.
|
||||
|
||||
## Configure the Web IDE
|
||||
|
||||
|
|
|
@ -61,6 +61,10 @@ module API
|
|||
end
|
||||
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
|
||||
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
|
||||
optional :gitpod_enabled, type: Boolean, desc: 'Enable Gitpod'
|
||||
given gitpod_enabled: ->(val) { val } do
|
||||
requires :gitpod_url, type: String, desc: 'The configured Gitpod instance URL'
|
||||
end
|
||||
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
|
||||
optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
|
||||
optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
|
||||
|
@ -150,6 +154,7 @@ module API
|
|||
end
|
||||
optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute."
|
||||
optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute."
|
||||
optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes"
|
||||
|
||||
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
|
||||
optional :"#{type}_key_restriction",
|
||||
|
|
|
@ -11,9 +11,11 @@ module Gitlab
|
|||
class SetNullPackageFilesFileStoreToLocalValue
|
||||
LOCAL_STORE = 1 # equal to ObjectStorage::Store::LOCAL
|
||||
|
||||
# Temporary AR class for package files
|
||||
class PackageFile < ActiveRecord::Base
|
||||
self.table_name = 'packages_package_files'
|
||||
module Packages
|
||||
# Temporary AR class for package files
|
||||
class PackageFile < ActiveRecord::Base
|
||||
self.table_name = 'packages_package_files'
|
||||
end
|
||||
end
|
||||
|
||||
def perform(start_id, stop_id)
|
||||
|
|
|
@ -6,17 +6,21 @@ module Gitlab
|
|||
class Metrics
|
||||
extend Gitlab::Utils::StrongMemoize
|
||||
|
||||
OPERATIONS = [:appended, :mutated, :overwrite, :accepted,
|
||||
:finalized, :discarded, :flaky].freeze
|
||||
OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite,
|
||||
:accepted, :finalized, :discarded, :conflict].freeze
|
||||
|
||||
def increment_trace_operation(operation: :unknown)
|
||||
unless OPERATIONS.include?(operation)
|
||||
raise ArgumentError, 'unknown trace operation'
|
||||
raise ArgumentError, "unknown trace operation: #{operation}"
|
||||
end
|
||||
|
||||
self.class.trace_operations.increment(operation: operation)
|
||||
end
|
||||
|
||||
def increment_trace_bytes(size)
|
||||
self.class.trace_bytes.increment(by: size.to_i)
|
||||
end
|
||||
|
||||
def self.trace_operations
|
||||
strong_memoize(:trace_operations) do
|
||||
name = :gitlab_ci_trace_operations_total
|
||||
|
@ -25,6 +29,15 @@ module Gitlab
|
|||
Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
end
|
||||
|
||||
def self.trace_bytes
|
||||
strong_memoize(:trace_bytes) do
|
||||
name = :gitlab_ci_trace_bytes_total
|
||||
comment = 'Total amount of build trace bytes transferred'
|
||||
|
||||
Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ module Gitlab
|
|||
BUFFER_SIZE = 4096
|
||||
LIMIT_SIZE = 500.kilobytes
|
||||
|
||||
attr_reader :stream
|
||||
attr_reader :stream, :metrics
|
||||
|
||||
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
|
||||
|
||||
|
@ -16,9 +16,10 @@ module Gitlab
|
|||
|
||||
alias_method :present?, :valid?
|
||||
|
||||
def initialize
|
||||
def initialize(metrics = Trace::Metrics.new)
|
||||
@stream = yield
|
||||
@stream&.binmode
|
||||
@metrics = metrics
|
||||
end
|
||||
|
||||
def valid?
|
||||
|
@ -43,6 +44,9 @@ module Gitlab
|
|||
def append(data, offset)
|
||||
data = data.force_encoding(Encoding::BINARY)
|
||||
|
||||
metrics.increment_trace_operation(operation: :streamed)
|
||||
metrics.increment_trace_bytes(data.bytesize)
|
||||
|
||||
stream.seek(offset, IO::SEEK_SET)
|
||||
stream.write(data)
|
||||
stream.truncate(offset + data.bytesize)
|
||||
|
|
|
@ -35,19 +35,12 @@ module Gitlab
|
|||
[service_address, service_port]
|
||||
end
|
||||
|
||||
def discover_prometheus_uri
|
||||
def discover_prometheus_server_address
|
||||
service_address, service_port = discover_service(service_name: 'prometheus')
|
||||
|
||||
return unless service_address && service_port
|
||||
|
||||
# There really is not a way to discover whether a Prometheus connection is using TLS or not
|
||||
# Try TLS first because HTTPS will return fast if failed.
|
||||
%w[https http].find do |scheme|
|
||||
connection_url = "#{scheme}://#{service_address}:#{service_port}"
|
||||
break connection_url if Gitlab::PrometheusClient.new(connection_url, allow_local_requests: true).healthy?
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
"#{service_address}:#{service_port}"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class Gitpod
|
||||
class << self
|
||||
def feature_conditional?
|
||||
feature.conditional?
|
||||
end
|
||||
|
||||
def feature_available?
|
||||
# The gitpod_bundle feature could be conditionally applied, so check if `!off?`
|
||||
!feature.off?
|
||||
end
|
||||
|
||||
def feature_enabled?(actor = nil)
|
||||
feature.enabled?(actor)
|
||||
end
|
||||
|
||||
def feature_and_settings_enabled?(actor = nil)
|
||||
feature_enabled?(actor) && Gitlab::CurrentSettings.gitpod_enabled
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def feature
|
||||
Feature.get(:gitpod) # rubocop:disable Gitlab/AvoidFeatureGet
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -25,6 +25,10 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def self.server_address
|
||||
uri&.strip&.sub(/^http[s]?:\/\//, '')
|
||||
end
|
||||
|
||||
def self.listen_address
|
||||
Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
|
||||
rescue Settingslogic::MissingSetting
|
||||
|
|
|
@ -42,6 +42,15 @@ module Gitlab
|
|||
response_body == HEALTHY_RESPONSE
|
||||
end
|
||||
|
||||
def ready?
|
||||
response = get(ready_url, {})
|
||||
|
||||
# From Prometheus docs: This endpoint returns 200 when Prometheus is ready to serve traffic (i.e. respond to queries).
|
||||
response.code == 200
|
||||
rescue => e
|
||||
raise PrometheusClient::UnexpectedResponseError, "#{e.message}"
|
||||
end
|
||||
|
||||
def proxy(type, args)
|
||||
path = api_path(type)
|
||||
get(path, args)
|
||||
|
@ -103,7 +112,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def health_url
|
||||
[api_url, '-/healthy'].join('/')
|
||||
"#{api_url}/-/healthy"
|
||||
end
|
||||
|
||||
def ready_url
|
||||
"#{api_url}/-/ready"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module RelativePositioning
|
||||
STEPS = 10
|
||||
IDEAL_DISTANCE = 2**(STEPS - 1) + 1
|
||||
|
||||
MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
|
||||
START_POSITION = 0
|
||||
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
|
||||
|
||||
MAX_GAP = IDEAL_DISTANCE * 2
|
||||
MIN_GAP = 2
|
||||
|
||||
NoSpaceLeft = Class.new(StandardError)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
module Gitlab
|
||||
module RelativePositioning
|
||||
class Gap
|
||||
attr_reader :start_pos, :end_pos
|
||||
|
||||
def initialize(start_pos, end_pos)
|
||||
@start_pos, @end_pos = start_pos, end_pos
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(self.class) && other.start_pos == start_pos && other.end_pos == end_pos
|
||||
end
|
||||
|
||||
def delta
|
||||
((start_pos - end_pos) / 2.0).abs.ceil.clamp(0, RelativePositioning::IDEAL_DISTANCE)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,259 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module RelativePositioning
|
||||
# This class is API private - it should not be explicitly instantiated
|
||||
# outside of tests
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
class ItemContext
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :object, :model_class, :range
|
||||
attr_accessor :ignoring
|
||||
|
||||
def initialize(object, range, ignoring: nil)
|
||||
@object = object
|
||||
@range = range
|
||||
@model_class = object.class
|
||||
@ignoring = ignoring
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(self.class) && other.object == object && other.range == range && other.ignoring == ignoring
|
||||
end
|
||||
|
||||
def positioned?
|
||||
relative_position.present?
|
||||
end
|
||||
|
||||
def min_relative_position
|
||||
strong_memoize(:min_relative_position) { calculate_relative_position('MIN') }
|
||||
end
|
||||
|
||||
def max_relative_position
|
||||
strong_memoize(:max_relative_position) { calculate_relative_position('MAX') }
|
||||
end
|
||||
|
||||
def prev_relative_position
|
||||
calculate_relative_position('MAX') { |r| nextify(r, false) } if object.relative_position
|
||||
end
|
||||
|
||||
def next_relative_position
|
||||
calculate_relative_position('MIN') { |r| nextify(r) } if object.relative_position
|
||||
end
|
||||
|
||||
def nextify(relation, gt = true)
|
||||
if gt
|
||||
relation.where("relative_position > ?", relative_position)
|
||||
else
|
||||
relation.where("relative_position < ?", relative_position)
|
||||
end
|
||||
end
|
||||
|
||||
def relative_siblings(relation = scoped_items)
|
||||
object.exclude_self(relation)
|
||||
end
|
||||
|
||||
# Handles the possibility that the position is already occupied by a sibling
|
||||
def place_at_position(position, lhs)
|
||||
current_occupant = relative_siblings.find_by(relative_position: position)
|
||||
|
||||
if current_occupant.present?
|
||||
Mover.new(position, range).move(object, lhs.object, current_occupant)
|
||||
else
|
||||
object.relative_position = position
|
||||
end
|
||||
end
|
||||
|
||||
def lhs_neighbour
|
||||
scoped_items
|
||||
.where('relative_position < ?', relative_position)
|
||||
.reorder(relative_position: :desc)
|
||||
.first
|
||||
.then { |x| neighbour(x) }
|
||||
end
|
||||
|
||||
def rhs_neighbour
|
||||
scoped_items
|
||||
.where('relative_position > ?', relative_position)
|
||||
.reorder(relative_position: :asc)
|
||||
.first
|
||||
.then { |x| neighbour(x) }
|
||||
end
|
||||
|
||||
def neighbour(item)
|
||||
return unless item.present?
|
||||
|
||||
self.class.new(item, range, ignoring: ignoring)
|
||||
end
|
||||
|
||||
def scoped_items
|
||||
r = model_class.relative_positioning_query_base(object)
|
||||
r = object.exclude_self(r, excluded: ignoring) if ignoring.present?
|
||||
r
|
||||
end
|
||||
|
||||
def calculate_relative_position(calculation)
|
||||
# When calculating across projects, this is much more efficient than
|
||||
# MAX(relative_position) without the GROUP BY, due to index usage:
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977
|
||||
relation = scoped_items
|
||||
.order(Gitlab::Database.nulls_last_order('position', 'DESC'))
|
||||
.group(grouping_column)
|
||||
.limit(1)
|
||||
|
||||
relation = yield relation if block_given?
|
||||
|
||||
relation
|
||||
.pluck(grouping_column, Arel.sql("#{calculation}(relative_position) AS position"))
|
||||
.first&.last
|
||||
end
|
||||
|
||||
def grouping_column
|
||||
model_class.relative_positioning_parent_column
|
||||
end
|
||||
|
||||
def max_sibling
|
||||
sib = relative_siblings
|
||||
.order(Gitlab::Database.nulls_last_order('relative_position', 'DESC'))
|
||||
.first
|
||||
|
||||
neighbour(sib)
|
||||
end
|
||||
|
||||
def min_sibling
|
||||
sib = relative_siblings
|
||||
.order(Gitlab::Database.nulls_last_order('relative_position', 'ASC'))
|
||||
.first
|
||||
|
||||
neighbour(sib)
|
||||
end
|
||||
|
||||
def shift_left
|
||||
move_sequence_before(true)
|
||||
object.reset
|
||||
end
|
||||
|
||||
def shift_right
|
||||
move_sequence_after(true)
|
||||
object.reset
|
||||
end
|
||||
|
||||
def create_space_left
|
||||
find_next_gap_before.tap { |gap| move_sequence_before(false, next_gap: gap) }
|
||||
end
|
||||
|
||||
def create_space_right
|
||||
find_next_gap_after.tap { |gap| move_sequence_after(false, next_gap: gap) }
|
||||
end
|
||||
|
||||
def find_next_gap_before
|
||||
items_with_next_pos = scoped_items
|
||||
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
|
||||
.where('relative_position <= ?', relative_position)
|
||||
.order(relative_position: :desc)
|
||||
|
||||
find_next_gap(items_with_next_pos, range.first)
|
||||
end
|
||||
|
||||
def find_next_gap_after
|
||||
items_with_next_pos = scoped_items
|
||||
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
|
||||
.where('relative_position >= ?', relative_position)
|
||||
.order(:relative_position)
|
||||
|
||||
find_next_gap(items_with_next_pos, range.last)
|
||||
end
|
||||
|
||||
def find_next_gap(items_with_next_pos, default_end)
|
||||
gap = model_class
|
||||
.from(items_with_next_pos, :items)
|
||||
.where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
|
||||
.limit(1)
|
||||
.pluck(:pos, :next_pos)
|
||||
.first
|
||||
|
||||
return if gap.nil? || gap.first == default_end
|
||||
|
||||
Gap.new(gap.first, gap.second || default_end)
|
||||
end
|
||||
|
||||
def relative_position
|
||||
object.relative_position
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Moves the sequence before the current item to the middle of the next gap
|
||||
# For example, we have
|
||||
#
|
||||
# 5 . . . . . 11 12 13 14 [15] 16 . 17
|
||||
# -----------
|
||||
#
|
||||
# This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
|
||||
#
|
||||
# 5 . . 8 9 10 11 . . . [15] 16 . 17
|
||||
# ---------
|
||||
#
|
||||
# Creating a gap to the left of the current item. We can understand this as
|
||||
# dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
|
||||
#
|
||||
# If `include_self` is true, the current item will also be moved, creating a
|
||||
# gap to the right of the current item:
|
||||
#
|
||||
# 5 . . 8 9 10 11 [14] . . . 16 . 17
|
||||
# --------------
|
||||
#
|
||||
# As an optimization, the gap can be precalculated and passed to this method.
|
||||
#
|
||||
# @api private
|
||||
# @raises NoSpaceLeft if the sequence cannot be moved
|
||||
def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
|
||||
raise NoSpaceLeft unless next_gap.present?
|
||||
|
||||
delta = next_gap.delta
|
||||
|
||||
move_sequence(next_gap.start_pos, relative_position, -delta, include_self)
|
||||
end
|
||||
|
||||
# Moves the sequence after the current item to the middle of the next gap
|
||||
# For example, we have:
|
||||
#
|
||||
# 8 . 10 [11] 12 13 14 15 . . . . . 21
|
||||
# -----------
|
||||
#
|
||||
# This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
|
||||
#
|
||||
# 8 . 10 [11] . . . 15 16 17 18 . . 21
|
||||
# -----------
|
||||
#
|
||||
# Creating a gap to the right of the current item. We can understand this as
|
||||
# dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
|
||||
#
|
||||
# If `include_self` is true, the current item will also be moved, creating a
|
||||
# gap to the left of the current item:
|
||||
#
|
||||
# 8 . 10 . . . [14] 15 16 17 18 . . 21
|
||||
# ----------------
|
||||
#
|
||||
# As an optimization, the gap can be precalculated and passed to this method.
|
||||
#
|
||||
# @api private
|
||||
# @raises NoSpaceLeft if the sequence cannot be moved
|
||||
def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
|
||||
raise NoSpaceLeft unless next_gap.present?
|
||||
|
||||
delta = next_gap.delta
|
||||
|
||||
move_sequence(relative_position, next_gap.start_pos, delta, include_self)
|
||||
end
|
||||
|
||||
def move_sequence(start_pos, end_pos, delta, include_self = false)
|
||||
relation = include_self ? scoped_items : relative_siblings
|
||||
|
||||
object.update_relative_siblings(relation, (start_pos..end_pos), delta)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
|
@ -0,0 +1,155 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module RelativePositioning
|
||||
class Mover
|
||||
attr_reader :range, :start_position
|
||||
|
||||
def initialize(start, range)
|
||||
@range = range
|
||||
@start_position = start
|
||||
end
|
||||
|
||||
def move_to_end(object)
|
||||
focus = context(object, ignoring: object)
|
||||
max_pos = focus.max_relative_position
|
||||
|
||||
move_to_range_end(focus, max_pos)
|
||||
end
|
||||
|
||||
def move_to_start(object)
|
||||
focus = context(object, ignoring: object)
|
||||
min_pos = focus.min_relative_position
|
||||
|
||||
move_to_range_start(focus, min_pos)
|
||||
end
|
||||
|
||||
def move(object, first, last)
|
||||
raise ArgumentError, 'object is required' unless object
|
||||
|
||||
lhs = context(first, ignoring: object)
|
||||
rhs = context(last, ignoring: object)
|
||||
focus = context(object)
|
||||
range = RelativePositioning.range(lhs, rhs)
|
||||
|
||||
if range.cover?(focus)
|
||||
# Moving a object already within a range is a no-op
|
||||
elsif range.open_on_left?
|
||||
move_to_range_start(focus, range.rhs.relative_position)
|
||||
elsif range.open_on_right?
|
||||
move_to_range_end(focus, range.lhs.relative_position)
|
||||
else
|
||||
pos_left, pos_right = create_space_between(range)
|
||||
desired_position = position_between(pos_left, pos_right)
|
||||
focus.place_at_position(desired_position, range.lhs)
|
||||
end
|
||||
end
|
||||
|
||||
def context(object, ignoring: nil)
|
||||
return unless object
|
||||
|
||||
ItemContext.new(object, range, ignoring: ignoring)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gap_too_small?(pos_a, pos_b)
|
||||
return false unless pos_a && pos_b
|
||||
|
||||
(pos_a - pos_b).abs < MIN_GAP
|
||||
end
|
||||
|
||||
def move_to_range_end(context, max_pos)
|
||||
range_end = range.last + 1
|
||||
|
||||
new_pos = if max_pos.nil?
|
||||
start_position
|
||||
elsif gap_too_small?(max_pos, range_end)
|
||||
max = context.max_sibling
|
||||
max.ignoring = context.object
|
||||
max.shift_left
|
||||
position_between(max.relative_position, range_end)
|
||||
else
|
||||
position_between(max_pos, range_end)
|
||||
end
|
||||
|
||||
context.object.relative_position = new_pos
|
||||
end
|
||||
|
||||
def move_to_range_start(context, min_pos)
|
||||
range_end = range.first - 1
|
||||
|
||||
new_pos = if min_pos.nil?
|
||||
start_position
|
||||
elsif gap_too_small?(min_pos, range_end)
|
||||
sib = context.min_sibling
|
||||
sib.ignoring = context.object
|
||||
sib.shift_right
|
||||
position_between(sib.relative_position, range_end)
|
||||
else
|
||||
position_between(min_pos, range_end)
|
||||
end
|
||||
|
||||
context.object.relative_position = new_pos
|
||||
end
|
||||
|
||||
def create_space_between(range)
|
||||
pos_left = range.lhs&.relative_position
|
||||
pos_right = range.rhs&.relative_position
|
||||
|
||||
return [pos_left, pos_right] unless gap_too_small?(pos_left, pos_right)
|
||||
|
||||
gap = range.rhs.create_space_left
|
||||
[pos_left - gap.delta, pos_right]
|
||||
rescue NoSpaceLeft
|
||||
gap = range.lhs.create_space_right
|
||||
[pos_left, pos_right + gap.delta]
|
||||
end
|
||||
|
||||
# This method takes two integer values (positions) and
|
||||
# calculates the position between them. The range is huge as
|
||||
# the maximum integer value is 2147483647.
|
||||
#
|
||||
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
|
||||
#
|
||||
# Then we handle one of three cases:
|
||||
# - If the gap is too small, we raise NoSpaceLeft
|
||||
# - If the gap is larger than MAX_GAP, we place the new position at most
|
||||
# IDEAL_DISTANCE from the edge of the gap.
|
||||
# - otherwise we place the new position at the midpoint.
|
||||
#
|
||||
# The new position will always satisfy: pos_before <= midpoint <= pos_after
|
||||
#
|
||||
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
|
||||
# If the gap is too small, NoSpaceLeft is raised.
|
||||
#
|
||||
# @raises NoSpaceLeft
|
||||
def position_between(pos_before, pos_after)
|
||||
pos_before ||= range.first
|
||||
pos_after ||= range.last
|
||||
|
||||
pos_before, pos_after = [pos_before, pos_after].sort
|
||||
|
||||
gap_width = pos_after - pos_before
|
||||
|
||||
if gap_too_small?(pos_before, pos_after)
|
||||
raise NoSpaceLeft
|
||||
elsif gap_width > MAX_GAP
|
||||
if pos_before <= range.first
|
||||
pos_after - IDEAL_DISTANCE
|
||||
elsif pos_after >= range.last
|
||||
pos_before + IDEAL_DISTANCE
|
||||
else
|
||||
midpoint(pos_before, pos_after)
|
||||
end
|
||||
else
|
||||
midpoint(pos_before, pos_after)
|
||||
end
|
||||
end
|
||||
|
||||
def midpoint(lower_bound, upper_bound)
|
||||
((lower_bound + upper_bound) / 2.0).ceil.clamp(lower_bound, upper_bound - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module RelativePositioning
|
||||
IllegalRange = Class.new(ArgumentError)
|
||||
|
||||
class Range
|
||||
attr_reader :lhs, :rhs
|
||||
|
||||
def open_on_left?
|
||||
lhs.nil?
|
||||
end
|
||||
|
||||
def open_on_right?
|
||||
rhs.nil?
|
||||
end
|
||||
|
||||
def cover?(item_context)
|
||||
return false unless item_context
|
||||
return false unless item_context.positioned?
|
||||
return true if item_context.object == lhs&.object
|
||||
return true if item_context.object == rhs&.object
|
||||
|
||||
pos = item_context.relative_position
|
||||
|
||||
return lhs.relative_position < pos if open_on_right?
|
||||
return pos < rhs.relative_position if open_on_left?
|
||||
|
||||
lhs.relative_position < pos && pos < rhs.relative_position
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(RelativePositioning::Range) && lhs == other.lhs && rhs == other.rhs
|
||||
end
|
||||
end
|
||||
|
||||
def self.range(lhs, rhs)
|
||||
if lhs && rhs
|
||||
ClosedRange.new(lhs, rhs)
|
||||
elsif lhs
|
||||
StartingFrom.new(lhs)
|
||||
elsif rhs
|
||||
EndingAt.new(rhs)
|
||||
else
|
||||
raise IllegalRange, 'One of rhs or lhs must be provided' unless lhs && rhs
|
||||
end
|
||||
end
|
||||
|
||||
class ClosedRange < RelativePositioning::Range
|
||||
def initialize(lhs, rhs)
|
||||
@lhs, @rhs = lhs, rhs
|
||||
raise IllegalRange, 'Either lhs or rhs is missing' unless lhs && rhs
|
||||
raise IllegalRange, 'lhs and rhs cannot be the same object' if lhs == rhs
|
||||
end
|
||||
end
|
||||
|
||||
class StartingFrom < RelativePositioning::Range
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(lhs)
|
||||
@lhs = lhs
|
||||
raise IllegalRange, 'lhs is required' unless lhs
|
||||
end
|
||||
|
||||
def rhs
|
||||
strong_memoize(:rhs) { lhs.rhs_neighbour }
|
||||
end
|
||||
end
|
||||
|
||||
class EndingAt < RelativePositioning::Range
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(rhs)
|
||||
@rhs = rhs
|
||||
raise IllegalRange, 'rhs is required' unless rhs
|
||||
end
|
||||
|
||||
def lhs
|
||||
strong_memoize(:lhs) { rhs.lhs_neighbour }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -230,8 +230,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def rss_increase_by_jobs
|
||||
Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
|
||||
rss_increase_by_job(job)
|
||||
Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do
|
||||
Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord
|
||||
rss_increase_by_job(job)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ module Gitlab
|
|||
!feature.off?
|
||||
end
|
||||
|
||||
def feature_enabled?(thing = nil)
|
||||
feature.enabled?(thing)
|
||||
def feature_enabled?(actor = nil)
|
||||
feature.enabled?(actor)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -815,11 +815,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
# rubocop: disable UsageData/DistinctCountByLargeForeignKey
|
||||
def cluster_applications_user_distinct_count(applications, time_period)
|
||||
distinct_count(applications.where(time_period).available.joins(:cluster), 'clusters.user_id')
|
||||
end
|
||||
# rubocop: enable UsageData/DistinctCountByLargeForeignKey
|
||||
|
||||
def clusters_user_distinct_count(clusters, time_period)
|
||||
distinct_count(clusters.where(time_period), :user_id)
|
||||
|
|
|
@ -40,7 +40,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def topology_fetch_all_data
|
||||
with_prometheus_client(fallback: {}) do |client|
|
||||
with_prometheus_client(fallback: {}, verify: false) do |client|
|
||||
{
|
||||
application_requests_per_hour: topology_app_requests_per_hour(client),
|
||||
query_apdex_weekly_average: topology_query_apdex_weekly_average(client),
|
||||
|
|
|
@ -120,6 +120,10 @@
|
|||
- name: merge_request_action
|
||||
category: source_code
|
||||
aggregation: daily
|
||||
- name: i_source_code_code_intelligence
|
||||
redis_slot: source_code
|
||||
category: source_code
|
||||
aggregation: daily
|
||||
# Incident management
|
||||
- name: incident_management_alert_status_changed
|
||||
redis_slot: incident_management
|
||||
|
|
|
@ -83,11 +83,11 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def with_prometheus_client(fallback: nil)
|
||||
api_url = prometheus_api_url
|
||||
return fallback unless api_url
|
||||
def with_prometheus_client(fallback: nil, verify: true)
|
||||
client = prometheus_client(verify: verify)
|
||||
return fallback unless client
|
||||
|
||||
yield Gitlab::PrometheusClient.new(api_url, allow_local_requests: true)
|
||||
yield client
|
||||
end
|
||||
|
||||
def measure_duration
|
||||
|
@ -111,11 +111,27 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def prometheus_api_url
|
||||
def prometheus_client(verify:)
|
||||
server_address = prometheus_server_address
|
||||
|
||||
return unless server_address
|
||||
|
||||
# There really is not a way to discover whether a Prometheus connection is using TLS or not
|
||||
# Try TLS first because HTTPS will return fast if failed.
|
||||
%w[https http].find do |scheme|
|
||||
api_url = "#{scheme}://#{server_address}"
|
||||
client = Gitlab::PrometheusClient.new(api_url, allow_local_requests: true, verify: verify)
|
||||
break client if client.ready?
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def prometheus_server_address
|
||||
if Gitlab::Prometheus::Internal.prometheus_enabled?
|
||||
Gitlab::Prometheus::Internal.uri
|
||||
Gitlab::Prometheus::Internal.server_address
|
||||
elsif Gitlab::Consul::Internal.api_url
|
||||
Gitlab::Consul::Internal.discover_prometheus_uri
|
||||
Gitlab::Consul::Internal.discover_prometheus_server_address
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9240,9 +9240,6 @@ msgstr ""
|
|||
msgid "Edit files in the editor and commit changes here"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit fork in Web IDE"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit group: %{group_name}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9420,9 +9417,18 @@ msgstr ""
|
|||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Auto DevOps"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Gitpod"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Gitpod?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable HTML emails"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11996,6 +12002,21 @@ msgstr ""
|
|||
msgid "Gitlab Pages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gitpod"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gitpod|Add the URL to your Gitpod instance configured to read your GitLab projects."
|
||||
msgstr ""
|
||||
|
||||
msgid "Gitpod|Enable Gitpod integration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gitpod|Gitpod URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gitpod|e.g. https://gitpod.example.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "Given access %{time_ago}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14594,6 +14615,9 @@ msgstr ""
|
|||
msgid "Latest pipeline for the most recent commit on this branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launch a ready-to-code development environment for your project."
|
||||
msgstr ""
|
||||
|
||||
msgid "Lead"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20679,6 +20703,9 @@ msgstr ""
|
|||
msgid "Quick range"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quickly and easily edit multiple files in your project."
|
||||
msgstr ""
|
||||
|
||||
msgid "README"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26561,6 +26588,9 @@ msgstr ""
|
|||
msgid "To update Snippets with multiple files, you must use the `files` parameter"
|
||||
msgstr ""
|
||||
|
||||
msgid "To use Gitpod you must first enable the feature in the integrations section of your %{user_prefs}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30892,6 +30922,9 @@ msgstr ""
|
|||
msgid "user avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "user preferences"
|
||||
msgstr ""
|
||||
|
||||
msgid "username"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
|
|||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
ENV DOCKER_VERSION="17.09.0-ce"
|
||||
ENV CHROME_VERSION="85.0.4183.83-1"
|
||||
ENV CHROME_DRIVER_VERSION="85.0.4183.87"
|
||||
ENV CHROME_VERSION="84.0.4147.89-1"
|
||||
ENV CHROME_DRIVER_VERSION="84.0.4147.30"
|
||||
ENV CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||
ENV CHROME_URL="https://s3.amazonaws.com/gitlab-google-chrome-stable/${CHROME_DEB}"
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ module QA
|
|||
element :new_file_option
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/repository/components/web_ide_link.vue' do
|
||||
view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do
|
||||
element :web_ide_button
|
||||
end
|
||||
|
||||
|
|
|
@ -31,11 +31,11 @@ module RuboCop
|
|||
private
|
||||
|
||||
def allowed_foreign_key?(key)
|
||||
[:sym, :str].include?(key.type) && allowed_foreign_keys.include?(key.value.to_sym)
|
||||
[:sym, :str].include?(key.type) && allowed_foreign_keys.include?(key.value.to_s)
|
||||
end
|
||||
|
||||
def allowed_foreign_keys
|
||||
cop_config['AllowedForeignKeys'] || []
|
||||
(cop_config['AllowedForeignKeys'] || []).map(&:to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,12 +38,13 @@ UsageData/DistinctCountByLargeForeignKey:
|
|||
- 'lib/gitlab/usage_data.rb'
|
||||
- 'ee/lib/ee/gitlab/usage_data.rb'
|
||||
AllowedForeignKeys:
|
||||
- :user_id
|
||||
- :author_id
|
||||
- :creator_id
|
||||
- :owner_id
|
||||
- :project_id
|
||||
- :issue_id
|
||||
- :merge_request_id
|
||||
- :merge_requests.target_project_id
|
||||
- :agent_id
|
||||
- 'agent_id'
|
||||
- 'author_id'
|
||||
- 'clusters.user_id'
|
||||
- 'creator_id'
|
||||
- 'issue_id'
|
||||
- 'merge_request_id'
|
||||
- 'merge_requests.target_project_id'
|
||||
- 'owner_id'
|
||||
- 'project_id'
|
||||
- 'user_id'
|
||||
|
|
|
@ -135,7 +135,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
expect($stdin).to receive(:gets).and_return(type)
|
||||
expect do
|
||||
expect(described_class.read_type).to eq(:development)
|
||||
end.to output(/specify the type/).to_stdout
|
||||
end.to output(/Specify the feature flag type/).to_stdout
|
||||
end
|
||||
|
||||
context 'when invalid type is given' do
|
||||
|
@ -147,7 +147,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
|
||||
expect do
|
||||
expect { described_class.read_type }.to raise_error(/EOF/)
|
||||
end.to output(/specify the type/).to_stdout
|
||||
end.to output(/Specify the feature flag type/).to_stdout
|
||||
.and output(/Invalid type specified/).to_stderr
|
||||
end
|
||||
end
|
||||
|
@ -161,7 +161,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
expect($stdin).to receive(:gets).and_return(group)
|
||||
expect do
|
||||
expect(described_class.read_group).to eq('group::memory')
|
||||
end.to output(/specify the group/).to_stdout
|
||||
end.to output(/Specify the group introducing the feature flag/).to_stdout
|
||||
end
|
||||
|
||||
context 'invalid group given' do
|
||||
|
@ -173,8 +173,8 @@ RSpec.describe 'bin/feature-flag' do
|
|||
|
||||
expect do
|
||||
expect { described_class.read_group }.to raise_error(/EOF/)
|
||||
end.to output(/specify the group/).to_stdout
|
||||
.and output(/Group needs to include/).to_stderr
|
||||
end.to output(/Specify the group introducing the feature flag/).to_stdout
|
||||
.and output(/The group needs to include/).to_stderr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -186,7 +186,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
expect($stdin).to receive(:gets).and_return(url)
|
||||
expect do
|
||||
expect(described_class.read_introduced_by_url).to eq('https://merge-request')
|
||||
end.to output(/can you paste the URL here/).to_stdout
|
||||
end.to output(/URL of the MR introducing the feature flag/).to_stdout
|
||||
end
|
||||
|
||||
context 'empty URL given' do
|
||||
|
@ -196,7 +196,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
expect($stdin).to receive(:gets).and_return(url)
|
||||
expect do
|
||||
expect(described_class.read_introduced_by_url).to be_nil
|
||||
end.to output(/can you paste the URL here/).to_stdout
|
||||
end.to output(/URL of the MR introducing the feature flag/).to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -209,7 +209,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
|
||||
expect do
|
||||
expect { described_class.read_introduced_by_url }.to raise_error(/EOF/)
|
||||
end.to output(/can you paste the URL here/).to_stdout
|
||||
end.to output(/URL of the MR introducing the feature flag/).to_stdout
|
||||
.and output(/URL needs to start with/).to_stderr
|
||||
end
|
||||
end
|
||||
|
@ -223,7 +223,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
expect($stdin).to receive(:gets).and_return(url)
|
||||
expect do
|
||||
expect(described_class.read_rollout_issue_url(options)).to eq('https://issue')
|
||||
end.to output(/Paste URL of `rollout issue` here/).to_stdout
|
||||
end.to output(/URL of the rollout issue/).to_stdout
|
||||
end
|
||||
|
||||
context 'invalid URL given' do
|
||||
|
@ -235,7 +235,7 @@ RSpec.describe 'bin/feature-flag' do
|
|||
|
||||
expect do
|
||||
expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/)
|
||||
end.to output(/Paste URL of `rollout issue` here/).to_stdout
|
||||
end.to output(/URL of the rollout issue/).to_stdout
|
||||
.and output(/URL needs to start/).to_stderr
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,10 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n
|
|||
end
|
||||
|
||||
context 'General page' do
|
||||
let(:gitpod_feature_enabled) { true }
|
||||
|
||||
before do
|
||||
stub_feature_flags(gitpod: gitpod_feature_enabled)
|
||||
visit general_admin_application_settings_path
|
||||
end
|
||||
|
||||
|
@ -205,6 +208,32 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n
|
|||
expect(page).to have_content "Application settings saved successfully"
|
||||
expect(current_settings.terminal_max_session_time).to eq(15)
|
||||
end
|
||||
|
||||
context 'Configure Gitpod' do
|
||||
context 'with feature disabled' do
|
||||
let(:gitpod_feature_enabled) { false }
|
||||
|
||||
it 'do not show settings' do
|
||||
expect(page).not_to have_selector('#js-gitpod-settings')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with feature enabled' do
|
||||
let(:gitpod_feature_enabled) { true }
|
||||
|
||||
it 'changes gitpod settings' do
|
||||
page.within('#js-gitpod-settings') do
|
||||
check 'Enable Gitpod integration'
|
||||
fill_in 'Gitpod URL', with: 'https://gitpod.test/'
|
||||
click_button 'Save changes'
|
||||
end
|
||||
|
||||
expect(page).to have_content 'Application settings saved successfully'
|
||||
expect(current_settings.gitpod_url).to eq('https://gitpod.test/')
|
||||
expect(current_settings.gitpod_enabled).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Integrations page' do
|
||||
|
|
|
@ -68,7 +68,7 @@ exports[`Design note component should match the snapshot 1`] = `
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="note-text js-note-text"
|
||||
class="note-text js-note-text md"
|
||||
data-qa-selector="note_content"
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import WebIdeLink from '~/repository/components/web_ide_link.vue';
|
||||
|
||||
describe('Web IDE link component', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(props) {
|
||||
wrapper = mount(WebIdeLink, {
|
||||
propsData: { ...props },
|
||||
mocks: {
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders link to the Web IDE for a project if only projectPath is given', () => {
|
||||
createComponent({ projectPath: 'gitlab-org/gitlab', refSha: 'master' });
|
||||
|
||||
expect(wrapper.attributes('href')).toBe('/-/ide/project/gitlab-org/gitlab/edit/master/-/');
|
||||
expect(wrapper.text()).toBe('Web IDE');
|
||||
});
|
||||
|
||||
it('renders link to the Web IDE for a project even if both projectPath and forkPath are given', () => {
|
||||
createComponent({
|
||||
projectPath: 'gitlab-org/gitlab',
|
||||
refSha: 'master',
|
||||
forkPath: 'my-namespace/gitlab',
|
||||
});
|
||||
|
||||
expect(wrapper.attributes('href')).toBe('/-/ide/project/gitlab-org/gitlab/edit/master/-/');
|
||||
expect(wrapper.text()).toBe('Web IDE');
|
||||
});
|
||||
|
||||
it('renders link to the forked project if it exists and cannot write to the repo', () => {
|
||||
createComponent({
|
||||
projectPath: 'gitlab-org/gitlab',
|
||||
refSha: 'master',
|
||||
forkPath: 'my-namespace/gitlab',
|
||||
canPushCode: false,
|
||||
});
|
||||
|
||||
expect(wrapper.attributes('href')).toBe('/-/ide/project/my-namespace/gitlab/edit/master/-/');
|
||||
expect(wrapper.text()).toBe('Edit fork in Web IDE');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,203 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlDropdown, GlLink } from '@gitlab/ui';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import ActionsButton from '~/vue_shared/components/actions_button.vue';
|
||||
|
||||
const TEST_ACTION = {
|
||||
key: 'action1',
|
||||
text: 'Sample',
|
||||
secondaryText: 'Lorem ipsum.',
|
||||
tooltip: '',
|
||||
href: '/sample',
|
||||
attrs: { 'data-test': '123' },
|
||||
};
|
||||
const TEST_ACTION_2 = {
|
||||
key: 'action2',
|
||||
text: 'Sample 2',
|
||||
secondaryText: 'Dolar sit amit.',
|
||||
tooltip: 'Dolar sit amit.',
|
||||
href: '#',
|
||||
attrs: { 'data-test': '456' },
|
||||
};
|
||||
const TEST_TOOLTIP = 'Lorem ipsum dolar sit';
|
||||
|
||||
describe('Actions button component', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(props) {
|
||||
wrapper = shallowMount(ActionsButton, {
|
||||
propsData: { ...props },
|
||||
directives: { GlTooltip: createMockDirective() },
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const getTooltip = child => {
|
||||
const directiveBinding = getBinding(child.element, 'gl-tooltip');
|
||||
|
||||
return directiveBinding.value;
|
||||
};
|
||||
const findLink = () => wrapper.find(GlLink);
|
||||
const findLinkTooltip = () => getTooltip(findLink());
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findDropdownTooltip = () => getTooltip(findDropdown());
|
||||
const parseDropdownItems = () =>
|
||||
findDropdown()
|
||||
.findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub')
|
||||
.wrappers.map(x => {
|
||||
if (x.is('gl-dropdown-divider-stub')) {
|
||||
return { type: 'divider' };
|
||||
}
|
||||
|
||||
const { isCheckItem, isChecked, secondaryText } = x.props();
|
||||
|
||||
return {
|
||||
type: 'item',
|
||||
isCheckItem,
|
||||
isChecked,
|
||||
secondaryText,
|
||||
text: x.text(),
|
||||
};
|
||||
});
|
||||
const clickOn = (child, evt = new Event('click')) => child.vm.$emit('click', evt);
|
||||
const clickLink = (...args) => clickOn(findLink(), ...args);
|
||||
const clickDropdown = (...args) => clickOn(findDropdown(), ...args);
|
||||
|
||||
describe('with 1 action', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ actions: [TEST_ACTION] });
|
||||
});
|
||||
|
||||
it('should not render dropdown', () => {
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render single button', () => {
|
||||
const link = findLink();
|
||||
|
||||
expect(link.attributes()).toEqual({
|
||||
class: expect.any(String),
|
||||
href: TEST_ACTION.href,
|
||||
...TEST_ACTION.attrs,
|
||||
});
|
||||
expect(link.text()).toBe(TEST_ACTION.text);
|
||||
});
|
||||
|
||||
it('should have tooltip', () => {
|
||||
expect(findLinkTooltip()).toBe(TEST_ACTION.tooltip);
|
||||
});
|
||||
|
||||
it('should have attrs', () => {
|
||||
expect(findLink().attributes()).toMatchObject(TEST_ACTION.attrs);
|
||||
});
|
||||
|
||||
it('can click', () => {
|
||||
expect(clickLink).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with 1 action with tooltip', () => {
|
||||
it('should have tooltip', () => {
|
||||
createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
|
||||
|
||||
expect(findLinkTooltip()).toBe(TEST_TOOLTIP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with 1 action with handle', () => {
|
||||
it('can click and trigger handle', () => {
|
||||
const handleClick = jest.fn();
|
||||
createComponent({ actions: [{ ...TEST_ACTION, handle: handleClick }] });
|
||||
|
||||
const event = new Event('click');
|
||||
clickLink(event);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledWith(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with multiple actions', () => {
|
||||
let handleAction;
|
||||
|
||||
beforeEach(() => {
|
||||
handleAction = jest.fn();
|
||||
|
||||
createComponent({ actions: [{ ...TEST_ACTION, handle: handleAction }, TEST_ACTION_2] });
|
||||
});
|
||||
|
||||
it('should default to selecting first action', () => {
|
||||
expect(findDropdown().attributes()).toMatchObject({
|
||||
text: TEST_ACTION.text,
|
||||
'split-href': TEST_ACTION.href,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle first action click', () => {
|
||||
const event = new Event('click');
|
||||
|
||||
clickDropdown(event);
|
||||
|
||||
expect(handleAction).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should render dropdown items', () => {
|
||||
expect(parseDropdownItems()).toEqual([
|
||||
{
|
||||
type: 'item',
|
||||
isCheckItem: true,
|
||||
isChecked: true,
|
||||
secondaryText: TEST_ACTION.secondaryText,
|
||||
text: TEST_ACTION.text,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
isCheckItem: true,
|
||||
isChecked: false,
|
||||
secondaryText: TEST_ACTION_2.secondaryText,
|
||||
text: TEST_ACTION_2.text,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should select action 2 when clicked', () => {
|
||||
expect(wrapper.emitted('select')).toBeUndefined();
|
||||
|
||||
const action2 = wrapper.find(`[data-testid="action_${TEST_ACTION_2.key}"]`);
|
||||
action2.vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]);
|
||||
});
|
||||
|
||||
it('should have tooltip value', () => {
|
||||
expect(findDropdownTooltip()).toBe(TEST_ACTION.tooltip);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with multiple actions and selectedKey', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ actions: [TEST_ACTION, TEST_ACTION_2], selectedKey: TEST_ACTION_2.key });
|
||||
});
|
||||
|
||||
it('should show action 2 as selected', () => {
|
||||
expect(parseDropdownItems()).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'item',
|
||||
isChecked: false,
|
||||
}),
|
||||
{ type: 'divider' },
|
||||
expect.objectContaining({
|
||||
type: 'item',
|
||||
isChecked: true,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have tooltip value', () => {
|
||||
expect(findDropdownTooltip()).toBe(TEST_ACTION_2.tooltip);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||
import ActionsButton from '~/vue_shared/components/actions_button.vue';
|
||||
|
||||
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
|
||||
const TEST_GITPOD_URL = 'https://gitpod.test/';
|
||||
|
||||
const ACTION_WEB_IDE = {
|
||||
href: TEST_WEB_IDE_URL,
|
||||
key: 'webide',
|
||||
secondaryText: 'Quickly and easily edit multiple files in your project.',
|
||||
tooltip: '',
|
||||
text: 'Web IDE',
|
||||
attrs: {
|
||||
'data-qa-selector': 'web_ide_button',
|
||||
},
|
||||
};
|
||||
const ACTION_WEB_IDE_FORK = {
|
||||
...ACTION_WEB_IDE,
|
||||
href: '#modal-confirm-fork',
|
||||
handle: expect.any(Function),
|
||||
};
|
||||
const ACTION_GITPOD = {
|
||||
href: TEST_GITPOD_URL,
|
||||
key: 'gitpod',
|
||||
secondaryText: 'Launch a ready-to-code development environment for your project.',
|
||||
tooltip: 'Launch a ready-to-code development environment for your project.',
|
||||
text: 'Gitpod',
|
||||
attrs: {
|
||||
'data-qa-selector': 'gitpod_button',
|
||||
},
|
||||
};
|
||||
const ACTION_GITPOD_ENABLE = {
|
||||
...ACTION_GITPOD,
|
||||
href: '#modal-enable-gitpod',
|
||||
handle: expect.any(Function),
|
||||
};
|
||||
|
||||
describe('Web IDE link component', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(props) {
|
||||
wrapper = shallowMount(WebIdeLink, {
|
||||
propsData: {
|
||||
webIdeUrl: TEST_WEB_IDE_URL,
|
||||
gitpodUrl: TEST_GITPOD_URL,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findActionsButton = () => wrapper.find(ActionsButton);
|
||||
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
|
||||
|
||||
it.each`
|
||||
props | expectedActions
|
||||
${{}} | ${[ACTION_WEB_IDE]}
|
||||
${{ needsToFork: true }} | ${[ACTION_WEB_IDE_FORK]}
|
||||
${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }} | ${[ACTION_GITPOD]}
|
||||
${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_GITPOD_ENABLE]}
|
||||
${{ showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_WEB_IDE, ACTION_GITPOD_ENABLE]}
|
||||
`('renders actions with props=$props', ({ props, expectedActions }) => {
|
||||
createComponent(props);
|
||||
|
||||
expect(findActionsButton().props('actions')).toEqual(expectedActions);
|
||||
});
|
||||
|
||||
describe('with multiple actions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ showWebIdeButton: true, showGitpodButton: true, gitpodEnabled: true });
|
||||
});
|
||||
|
||||
it('selected Web IDE by default', () => {
|
||||
expect(findActionsButton().props()).toMatchObject({
|
||||
actions: [ACTION_WEB_IDE, ACTION_GITPOD],
|
||||
selectedKey: ACTION_WEB_IDE.key,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set selection with local storage value', async () => {
|
||||
expect(findActionsButton().props('selectedKey')).toBe(ACTION_WEB_IDE.key);
|
||||
|
||||
findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
|
||||
});
|
||||
|
||||
it('should update local storage when selection changes', async () => {
|
||||
expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key);
|
||||
|
||||
findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
|
||||
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue