Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2f2c8f84bf
commit
ea937d0916
46 changed files with 885 additions and 389 deletions
|
@ -72,11 +72,18 @@ export default {
|
|||
GlPagination,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['projectPath', 'newIssuePath', 'incidentTemplateName', 'issuePath'],
|
||||
inject: [
|
||||
'projectPath',
|
||||
'newIssuePath',
|
||||
'incidentTemplateName',
|
||||
'issuePath',
|
||||
'publishedAvailable',
|
||||
],
|
||||
apollo: {
|
||||
incidents: {
|
||||
query: getIncidents,
|
||||
|
@ -144,6 +151,20 @@ export default {
|
|||
newIncidentPath() {
|
||||
return mergeUrlParams({ issuable_template: this.incidentTemplateName }, this.newIssuePath);
|
||||
},
|
||||
availableFields() {
|
||||
return this.publishedAvailable
|
||||
? [
|
||||
...this.$options.fields,
|
||||
...[
|
||||
{
|
||||
key: 'published',
|
||||
label: s__('IncidentManagement|Published'),
|
||||
thClass: 'gl-pointer-events-none',
|
||||
},
|
||||
],
|
||||
]
|
||||
: this.$options.fields;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInputChange: debounce(function debounceSearch(input) {
|
||||
|
@ -230,7 +251,7 @@ export default {
|
|||
</h4>
|
||||
<gl-table
|
||||
:items="incidents.list || []"
|
||||
:fields="$options.fields"
|
||||
:fields="availableFields"
|
||||
:show-empty="true"
|
||||
:busy="loading"
|
||||
stacked="md"
|
||||
|
@ -245,7 +266,7 @@ export default {
|
|||
<gl-icon
|
||||
v-if="item.state === 'closed'"
|
||||
name="issue-close"
|
||||
class="gl-fill-blue-500"
|
||||
class="gl-ml-1 gl-fill-blue-500"
|
||||
data-testid="incident-closed"
|
||||
/>
|
||||
</div>
|
||||
|
@ -285,6 +306,13 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="publishedAvailable" #cell(published)="{ item }">
|
||||
<published-cell
|
||||
:status-page-published-incident="item.statusPagePublishedIncident"
|
||||
:un-published="$options.i18n.unPublished"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #table-busy>
|
||||
<gl-loading-icon size="lg" color="dark" class="mt-3" />
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,7 @@ export const I18N = {
|
|||
unassigned: s__('IncidentManagement|Unassigned'),
|
||||
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
|
||||
searchPlaceholder: __('Search or filter results...'),
|
||||
unPublished: s__('IncidentManagement|Unpublished'),
|
||||
};
|
||||
|
||||
export const INCIDENT_STATE_TABS = [
|
||||
|
|
|
@ -37,6 +37,7 @@ query getIncidents(
|
|||
webUrl
|
||||
}
|
||||
}
|
||||
statusPagePublishedIncident
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
|
|
|
@ -8,7 +8,13 @@ export default () => {
|
|||
const selector = '#js-incidents';
|
||||
|
||||
const domEl = document.querySelector(selector);
|
||||
const { projectPath, newIssuePath, incidentTemplateName, issuePath } = domEl.dataset;
|
||||
const {
|
||||
projectPath,
|
||||
newIssuePath,
|
||||
incidentTemplateName,
|
||||
issuePath,
|
||||
publishedAvailable,
|
||||
} = domEl.dataset;
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
|
@ -21,6 +27,7 @@ export default () => {
|
|||
incidentTemplateName,
|
||||
newIssuePath,
|
||||
issuePath,
|
||||
publishedAvailable,
|
||||
},
|
||||
apolloProvider,
|
||||
components: {
|
||||
|
|
|
@ -107,7 +107,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-new-dropdown class="ref-selector" @shown="focusSearchBox">
|
||||
<gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
|
||||
<template slot="button-content">
|
||||
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-600" data-testid="button-content">
|
||||
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import { escapeFileUrl } from '../lib/utils/url_utility';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
import Breadcrumbs from './components/breadcrumbs.vue';
|
||||
|
@ -109,7 +110,7 @@ export default function setupVueRepositoryList() {
|
|||
return h(TreeActionLink, {
|
||||
props: {
|
||||
path: `${historyLink}/${
|
||||
this.$route.params.path ? encodeURIComponent(this.$route.params.path) : ''
|
||||
this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
|
||||
}`,
|
||||
text: __('History'),
|
||||
},
|
||||
|
|
|
@ -5,39 +5,88 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import { spriteIcon } from '~/lib/utils/common_utils';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
|
||||
/**
|
||||
* Creates the HTML template for each row of the mentions dropdown.
|
||||
*
|
||||
* @param original - An object from the array returned from the `autocomplete_sources/members` API
|
||||
* @returns {string} - An HTML template
|
||||
*/
|
||||
function menuItemTemplate({ original }) {
|
||||
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
|
||||
const AutoComplete = {
|
||||
Labels: 'labels',
|
||||
Members: 'members',
|
||||
};
|
||||
|
||||
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
|
||||
gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
|
||||
|
||||
const avatarTag = original.avatar_url
|
||||
? `<img
|
||||
src="${original.avatar_url}"
|
||||
alt="${original.username}'s avatar"
|
||||
class="${avatarClasses}"/>`
|
||||
: `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
|
||||
|
||||
const name = escape(original.name);
|
||||
|
||||
const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
|
||||
|
||||
const icon = original.mentionsDisabled
|
||||
? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
|
||||
: '';
|
||||
|
||||
return `${avatarTag}
|
||||
${original.username}
|
||||
<small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
|
||||
${icon}`;
|
||||
function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
|
||||
const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
|
||||
const currentLine = fullText.split('\n')[currentLineNumber - 1];
|
||||
return currentLine.startsWith(searchString);
|
||||
}
|
||||
|
||||
const autoCompleteMap = {
|
||||
[AutoComplete.Labels]: {
|
||||
filterValues() {
|
||||
const fullText = this.$slots.default?.[0]?.elm?.value;
|
||||
const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
|
||||
|
||||
if (doesCurrentLineStartWith('/label', fullText, selectionStart)) {
|
||||
return this.labels.filter(label => !label.set);
|
||||
}
|
||||
|
||||
if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) {
|
||||
return this.labels.filter(label => label.set);
|
||||
}
|
||||
|
||||
return this.labels;
|
||||
},
|
||||
menuItemTemplate({ original }) {
|
||||
return `
|
||||
<span class="dropdown-label-box" style="background: ${escape(original.color)};"></span>
|
||||
${escape(original.title)}`;
|
||||
},
|
||||
},
|
||||
[AutoComplete.Members]: {
|
||||
filterValues() {
|
||||
const fullText = this.$slots.default?.[0]?.elm?.value;
|
||||
const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
|
||||
|
||||
if (!this.assignees) {
|
||||
this.assignees =
|
||||
SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
|
||||
}
|
||||
|
||||
if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
|
||||
return this.members.filter(member => !this.assignees.includes(member.username));
|
||||
}
|
||||
|
||||
if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
|
||||
return this.members.filter(member => this.assignees.includes(member.username));
|
||||
}
|
||||
|
||||
return this.members;
|
||||
},
|
||||
menuItemTemplate({ original }) {
|
||||
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
|
||||
|
||||
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
|
||||
gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
|
||||
|
||||
const avatarTag = original.avatar_url
|
||||
? `<img
|
||||
src="${original.avatar_url}"
|
||||
alt="${original.username}'s avatar"
|
||||
class="${avatarClasses}"/>`
|
||||
: `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
|
||||
|
||||
const name = escape(original.name);
|
||||
|
||||
const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
|
||||
|
||||
const icon = original.mentionsDisabled
|
||||
? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
|
||||
: '';
|
||||
|
||||
return `${avatarTag}
|
||||
${original.username}
|
||||
<small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
|
||||
${icon}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'GlMentions',
|
||||
props: {
|
||||
|
@ -47,67 +96,57 @@ export default {
|
|||
default: () => gl.GfmAutoComplete?.dataSources || {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
assignees: undefined,
|
||||
members: undefined,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const NON_WORD_OR_INTEGER = /\W|^\d+$/;
|
||||
|
||||
this.tribute = new Tribute({
|
||||
trigger: '@',
|
||||
fillAttr: 'username',
|
||||
lookup: value => value.name + value.username,
|
||||
menuItemTemplate,
|
||||
values: this.getMembers,
|
||||
collection: [
|
||||
{
|
||||
trigger: '@',
|
||||
fillAttr: 'username',
|
||||
lookup: value => value.name + value.username,
|
||||
menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
|
||||
values: this.getValues(AutoComplete.Members),
|
||||
},
|
||||
{
|
||||
trigger: '~',
|
||||
lookup: 'title',
|
||||
menuItemTemplate: autoCompleteMap[AutoComplete.Labels].menuItemTemplate,
|
||||
selectTemplate: ({ original }) =>
|
||||
NON_WORD_OR_INTEGER.test(original.title)
|
||||
? `~"${original.title}"`
|
||||
: `~${original.title}`,
|
||||
values: this.getValues(AutoComplete.Labels),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const input = this.$slots.default[0].elm;
|
||||
const input = this.$slots.default?.[0]?.elm;
|
||||
this.tribute.attach(input);
|
||||
},
|
||||
beforeDestroy() {
|
||||
const input = this.$slots.default[0].elm;
|
||||
const input = this.$slots.default?.[0]?.elm;
|
||||
this.tribute.detach(input);
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Creates the list of users to show in the mentions dropdown.
|
||||
*
|
||||
* @param inputText - The text entered by the user in the mentions input field
|
||||
* @param processValues - Callback function to set the list of users to show in the mentions dropdown
|
||||
*/
|
||||
getMembers(inputText, processValues) {
|
||||
if (this.members) {
|
||||
processValues(this.getFilteredMembers());
|
||||
} else if (this.dataSources.members) {
|
||||
axios
|
||||
.get(this.dataSources.members)
|
||||
.then(response => {
|
||||
this.members = response.data;
|
||||
processValues(this.getFilteredMembers());
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
processValues([]);
|
||||
}
|
||||
},
|
||||
getFilteredMembers() {
|
||||
const fullText = this.$slots.default[0].elm.value;
|
||||
|
||||
if (!this.assignees) {
|
||||
this.assignees =
|
||||
SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
|
||||
}
|
||||
|
||||
if (fullText.startsWith('/assign @')) {
|
||||
return this.members.filter(member => !this.assignees.includes(member.username));
|
||||
}
|
||||
|
||||
if (fullText.startsWith('/unassign @')) {
|
||||
return this.members.filter(member => this.assignees.includes(member.username));
|
||||
}
|
||||
|
||||
return this.members;
|
||||
getValues(autoCompleteType) {
|
||||
return (inputText, processValues) => {
|
||||
if (this[autoCompleteType]) {
|
||||
const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
|
||||
processValues(filteredValues);
|
||||
} else if (this.dataSources[autoCompleteType]) {
|
||||
axios
|
||||
.get(this.dataSources[autoCompleteType])
|
||||
.then(response => {
|
||||
this[autoCompleteType] = response.data;
|
||||
const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
|
||||
processValues(filteredValues);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
processValues([]);
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
|
|
|
@ -171,7 +171,7 @@ export default {
|
|||
mergeRequests: this.enableAutocomplete,
|
||||
epics: this.enableAutocomplete,
|
||||
milestones: this.enableAutocomplete,
|
||||
labels: this.enableAutocomplete,
|
||||
labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
|
||||
snippets: this.enableAutocomplete,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -98,4 +98,9 @@
|
|||
@include gl-w-full;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Abstract to `@gitlab/ui` utility set: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/921
|
||||
.gl-fill-green-500 {
|
||||
fill: $gray-500;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,6 +97,9 @@ module Types
|
|||
|
||||
field :design_collection, Types::DesignManagement::DesignCollectionType, null: true,
|
||||
description: 'Collection of design images associated with this issue'
|
||||
|
||||
field :status_page_published_incident, GraphQL::BOOLEAN_TYPE, null: true,
|
||||
description: 'Indicates whether an issue is published to the status page'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,3 +10,5 @@ module Projects::IncidentsHelper
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
Projects::IncidentsHelper.prepend_if_ee('EE::Projects::IncidentsHelper')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= link_to safe_params.merge(rss_url_options), class: 'btn btn-svg has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
|
||||
= link_to safe_params.merge(rss_url_options), class: 'btn btn-svg has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') do
|
||||
= sprite_icon('rss', css_class: 'qa-rss-icon', size: 16)
|
||||
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
|
||||
= sprite_icon('calendar')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove updated_at column on audit_events table
|
||||
merge_request: 35690
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow optional keyset pagination for branch list API
|
||||
merge_request: 37524
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixes the history button link URL being encoded incorrectly
|
||||
merge_request: 38392
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,119 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveUpdatedAtFromAuditEvents < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::SchemaHelpers
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
SOURCE_TABLE_NAME = 'audit_events'
|
||||
PARTITIONED_TABLE_NAME = 'audit_events_part_5fc467ac26'
|
||||
TRIGGER_FUNCTION_NAME = 'table_sync_function_2be879775d'
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_column SOURCE_TABLE_NAME, :updated_at
|
||||
|
||||
create_trigger_function(TRIGGER_FUNCTION_NAME, replace: true) do
|
||||
<<~SQL
|
||||
IF (TG_OP = 'DELETE') THEN
|
||||
DELETE FROM #{PARTITIONED_TABLE_NAME} where id = OLD.id;
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
UPDATE #{PARTITIONED_TABLE_NAME}
|
||||
SET author_id = NEW.author_id,
|
||||
type = NEW.type,
|
||||
entity_id = NEW.entity_id,
|
||||
entity_type = NEW.entity_type,
|
||||
details = NEW.details,
|
||||
ip_address = NEW.ip_address,
|
||||
author_name = NEW.author_name,
|
||||
entity_path = NEW.entity_path,
|
||||
target_details = NEW.target_details,
|
||||
created_at = NEW.created_at
|
||||
WHERE #{PARTITIONED_TABLE_NAME}.id = NEW.id;
|
||||
ELSIF (TG_OP = 'INSERT') THEN
|
||||
INSERT INTO #{PARTITIONED_TABLE_NAME} (id,
|
||||
author_id,
|
||||
type,
|
||||
entity_id,
|
||||
entity_type,
|
||||
details,
|
||||
ip_address,
|
||||
author_name,
|
||||
entity_path,
|
||||
target_details,
|
||||
created_at)
|
||||
VALUES (NEW.id,
|
||||
NEW.author_id,
|
||||
NEW.type,
|
||||
NEW.entity_id,
|
||||
NEW.entity_type,
|
||||
NEW.details,
|
||||
NEW.ip_address,
|
||||
NEW.author_name,
|
||||
NEW.entity_path,
|
||||
NEW.target_details,
|
||||
NEW.created_at);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
SQL
|
||||
end
|
||||
|
||||
remove_column PARTITIONED_TABLE_NAME, :updated_at
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
add_column SOURCE_TABLE_NAME, :updated_at, :datetime # rubocop:disable Migration/Datetime
|
||||
add_column PARTITIONED_TABLE_NAME, :updated_at, :datetime # rubocop:disable Migration/Datetime
|
||||
|
||||
create_trigger_function(TRIGGER_FUNCTION_NAME, replace: true) do
|
||||
<<~SQL
|
||||
IF (TG_OP = 'DELETE') THEN
|
||||
DELETE FROM #{PARTITIONED_TABLE_NAME} where id = OLD.id;
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
UPDATE #{PARTITIONED_TABLE_NAME}
|
||||
SET author_id = NEW.author_id,
|
||||
type = NEW.type,
|
||||
entity_id = NEW.entity_id,
|
||||
entity_type = NEW.entity_type,
|
||||
details = NEW.details,
|
||||
updated_at = NEW.updated_at,
|
||||
ip_address = NEW.ip_address,
|
||||
author_name = NEW.author_name,
|
||||
entity_path = NEW.entity_path,
|
||||
target_details = NEW.target_details,
|
||||
created_at = NEW.created_at
|
||||
WHERE #{PARTITIONED_TABLE_NAME}.id = NEW.id;
|
||||
ELSIF (TG_OP = 'INSERT') THEN
|
||||
INSERT INTO #{PARTITIONED_TABLE_NAME} (id,
|
||||
author_id,
|
||||
type,
|
||||
entity_id,
|
||||
entity_type,
|
||||
details,
|
||||
updated_at,
|
||||
ip_address,
|
||||
author_name,
|
||||
entity_path,
|
||||
target_details,
|
||||
created_at)
|
||||
VALUES (NEW.id,
|
||||
NEW.author_id,
|
||||
NEW.type,
|
||||
NEW.entity_id,
|
||||
NEW.entity_type,
|
||||
NEW.details,
|
||||
NEW.updated_at,
|
||||
NEW.ip_address,
|
||||
NEW.author_name,
|
||||
NEW.entity_path,
|
||||
NEW.target_details,
|
||||
NEW.created_at);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
1
db/schema_migrations/20200724100421
Normal file
1
db/schema_migrations/20200724100421
Normal file
|
@ -0,0 +1 @@
|
|||
77601e653f7b4f2740db87a7b19b64bb73fffbe4ce59c0f68b0bb65599da0eb3
|
|
@ -25,7 +25,6 @@ ELSIF (TG_OP = 'UPDATE') THEN
|
|||
entity_id = NEW.entity_id,
|
||||
entity_type = NEW.entity_type,
|
||||
details = NEW.details,
|
||||
updated_at = NEW.updated_at,
|
||||
ip_address = NEW.ip_address,
|
||||
author_name = NEW.author_name,
|
||||
entity_path = NEW.entity_path,
|
||||
|
@ -39,7 +38,6 @@ ELSIF (TG_OP = 'INSERT') THEN
|
|||
entity_id,
|
||||
entity_type,
|
||||
details,
|
||||
updated_at,
|
||||
ip_address,
|
||||
author_name,
|
||||
entity_path,
|
||||
|
@ -51,7 +49,6 @@ ELSIF (TG_OP = 'INSERT') THEN
|
|||
NEW.entity_id,
|
||||
NEW.entity_type,
|
||||
NEW.details,
|
||||
NEW.updated_at,
|
||||
NEW.ip_address,
|
||||
NEW.author_name,
|
||||
NEW.entity_path,
|
||||
|
@ -72,7 +69,6 @@ CREATE TABLE public.audit_events_part_5fc467ac26 (
|
|||
entity_id integer NOT NULL,
|
||||
entity_type character varying NOT NULL,
|
||||
details text,
|
||||
updated_at timestamp without time zone,
|
||||
ip_address inet,
|
||||
author_name text,
|
||||
entity_path text,
|
||||
|
@ -9470,7 +9466,6 @@ CREATE TABLE public.audit_events (
|
|||
entity_type character varying NOT NULL,
|
||||
details text,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
ip_address inet,
|
||||
author_name text,
|
||||
entity_path text,
|
||||
|
|
|
@ -52,7 +52,6 @@ The following metrics are available:
|
|||
| `gitlab_sql_duration_seconds` | Histogram | 10.2 | SQL execution time, excluding `SCHEMA` operations and `BEGIN` / `COMMIT` | |
|
||||
| `gitlab_ruby_threads_max_expected_threads` | Gauge | 13.3 | Maximum number of threads expected to be running and performing application work |
|
||||
| `gitlab_ruby_threads_running_threads` | Gauge | 13.3 | Number of running Ruby threads by name |
|
||||
| `gitlab_transaction_allocated_memory_bytes` | Histogram | 10.2 | Allocated memory for all transactions (`gitlab_transaction_*` metrics) | |
|
||||
| `gitlab_transaction_cache_<key>_count_total` | Counter | 10.2 | Counter for total Rails cache calls (per key) | |
|
||||
| `gitlab_transaction_cache_<key>_duration_total` | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (per key) | |
|
||||
| `gitlab_transaction_cache_count_total` | Counter | 10.2 | Counter for total Rails cache calls (aggregate) | |
|
||||
|
|
|
@ -13,7 +13,7 @@ There are essentially three setups to choose from.
|
|||
This setup is for when you have installed GitLab using the
|
||||
[Omnibus GitLab **Enterprise Edition** (EE) package](https://about.gitlab.com/install/?version=ee).
|
||||
|
||||
All the tools that are needed like PostgreSQL, PgBouncer, Repmgr are bundled in
|
||||
All the tools that are needed like PostgreSQL, PgBouncer, Patroni, and repmgr are bundled in
|
||||
the package, so you can it to set up the whole PostgreSQL infrastructure (primary, replica).
|
||||
|
||||
[> Read how to set up PostgreSQL replication and failover using Omnibus GitLab](replication_and_failover.md)
|
||||
|
|
|
@ -29,6 +29,11 @@ You also need to take into consideration the underlying network topology, making
|
|||
sure you have redundant connectivity between all Database and GitLab instances
|
||||
to avoid the network becoming a single point of failure.
|
||||
|
||||
NOTE: **Note:**
|
||||
As of GitLab 13.3, PostgreSQL 12 is shipped with Omnibus GitLab. Clustering for PostgreSQL 12 is only supported with
|
||||
Patroni. See the [Patroni](#patroni) section for further details. The support for repmgr will not be extended beyond
|
||||
PostgreSQL 11.
|
||||
|
||||
### Database node
|
||||
|
||||
Each database node runs three services:
|
||||
|
|
|
@ -1,36 +1,46 @@
|
|||
---
|
||||
type: reference, concepts
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
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
|
||||
---
|
||||
# Reference architectures
|
||||
|
||||
<!-- TBD to be reviewed by Eric -->
|
||||
|
||||
You can set up GitLab on a single server or scale it up to serve many users.
|
||||
This page details the recommended Reference Architectures that were built and verified by GitLab's Quality and Support teams.
|
||||
This page details the recommended Reference Architectures that were built and
|
||||
verified by GitLab's Quality and Support teams.
|
||||
|
||||
Below is a chart representing each architecture tier and the number of users they can handle. As your number of users grow with time, it’s recommended that you scale GitLab accordingly.
|
||||
Below is a chart representing each architecture tier and the number of users
|
||||
they can handle. As your number of users grow with time, it’s recommended that
|
||||
you scale GitLab accordingly.
|
||||
|
||||
![Reference Architectures](img/reference-architectures.png)
|
||||
<!-- Internal link: https://docs.google.com/spreadsheets/d/1obYP4fLKkVVDOljaI3-ozhmCiPtEeMblbBKkf2OADKs/edit#gid=1403207183 -->
|
||||
|
||||
Testing on these reference architectures were performed with [GitLab's Performance Tool](https://gitlab.com/gitlab-org/quality/performance)
|
||||
at specific coded workloads, and the throughputs used for testing were calculated based on sample customer data.
|
||||
After selecting the reference architecture that matches your scale, refer to
|
||||
Testing on these reference architectures were performed with
|
||||
[GitLab's Performance Tool](https://gitlab.com/gitlab-org/quality/performance)
|
||||
at specific coded workloads, and the throughputs used for testing were
|
||||
calculated based on sample customer data. After selecting the reference
|
||||
architecture that matches your scale, refer to
|
||||
[Configure GitLab to Scale](#configure-gitlab-to-scale) to see the components
|
||||
involved, and how to configure them.
|
||||
|
||||
Each endpoint type is tested with the following number of requests per second (RPS) per 1000 users:
|
||||
Each endpoint type is tested with the following number of requests per second (RPS)
|
||||
per 1,000 users:
|
||||
|
||||
- API: 20 RPS
|
||||
- Web: 2 RPS
|
||||
- Git: 2 RPS
|
||||
|
||||
For GitLab instances with less than 2,000 users, it's recommended that you use the [default setup](#automated-backups-core-only)
|
||||
by [installing GitLab](../../install/README.md) on a single machine to minimize maintenance and resource costs.
|
||||
For GitLab instances with less than 2,000 users, it's recommended that you use
|
||||
the [default setup](#automated-backups-core-only) by
|
||||
[installing GitLab](../../install/README.md) on a single machine to minimize
|
||||
maintenance and resource costs.
|
||||
|
||||
If your organization has more than 2,000 users, the recommendation is to scale GitLab's components to multiple
|
||||
machine nodes. The machine nodes are grouped by component(s). The addition of these
|
||||
nodes increases the performance and scalability of to your GitLab instance.
|
||||
If your organization has more than 2,000 users, the recommendation is to scale
|
||||
GitLab's components to multiple machine nodes. The machine nodes are grouped by
|
||||
components. The addition of these nodes increases the performance and
|
||||
scalability of to your GitLab instance.
|
||||
|
||||
When scaling GitLab, there are several factors to consider:
|
||||
|
||||
|
@ -39,12 +49,13 @@ When scaling GitLab, there are several factors to consider:
|
|||
- The application nodes connects to a shared file server and PostgreSQL and Redis services on the backend.
|
||||
|
||||
NOTE: **Note:**
|
||||
Depending on your workflow, the following recommended
|
||||
reference architectures may need to be adapted accordingly. Your workload
|
||||
is influenced by factors including how active your users are,
|
||||
how much automation you use, mirroring, and repository/change size. Additionally the
|
||||
displayed memory values are provided by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
|
||||
For different cloud vendors, attempt to select options that best match the provided architecture.
|
||||
Depending on your workflow, the following recommended reference architectures
|
||||
may need to be adapted accordingly. Your workload is influenced by factors
|
||||
including how active your users are, how much automation you use, mirroring,
|
||||
and repository/change size. Additionally the displayed memory values are
|
||||
provided by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
|
||||
For different cloud vendors, attempt to select options that best match the
|
||||
provided architecture.
|
||||
|
||||
## Available reference architectures
|
||||
|
||||
|
@ -60,14 +71,14 @@ The following reference architectures are available:
|
|||
|
||||
## Availability Components
|
||||
|
||||
GitLab comes with the following components for your use, listed from
|
||||
least to most complex:
|
||||
GitLab comes with the following components for your use, listed from least to
|
||||
most complex:
|
||||
|
||||
1. [Automated backups](#automated-backups-core-only)
|
||||
1. [Traffic load balancer](#traffic-load-balancer-starter-only)
|
||||
1. [Zero downtime updates](#zero-downtime-updates-starter-only)
|
||||
1. [Automated database failover](#automated-database-failover-premium-only)
|
||||
1. [Instance level replication with GitLab Geo](#instance-level-replication-with-gitlab-geo-premium-only)
|
||||
- [Automated backups](#automated-backups-core-only)
|
||||
- [Traffic load balancer](#traffic-load-balancer-starter-only)
|
||||
- [Zero downtime updates](#zero-downtime-updates-starter-only)
|
||||
- [Automated database failover](#automated-database-failover-premium-only)
|
||||
- [Instance level replication with GitLab Geo](#instance-level-replication-with-gitlab-geo-premium-only)
|
||||
|
||||
As you implement these components, begin with a single server and then do
|
||||
backups. Only after completing the first server should you proceed to the next.
|
||||
|
@ -144,7 +155,9 @@ that can also be promoted in case of disaster.
|
|||
## Configure GitLab to scale
|
||||
|
||||
NOTE: **Note:**
|
||||
From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0, support for NFS for Git repositories is scheduled to be removed. Upgrade to [Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
|
||||
From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0,
|
||||
support for NFS for Git repositories is scheduled to be removed. Upgrade to
|
||||
[Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
|
||||
|
||||
The following components are the ones you need to configure in order to scale
|
||||
GitLab. They are listed in the order you'll typically configure them if they are
|
||||
|
@ -233,9 +246,3 @@ cluster with the Rails nodes broken down into a number of smaller Pods across th
|
|||
as with time they may be adjusted higher or lower depending on the scale of your
|
||||
environment's workload. If you're running the environment on a Cloud provider
|
||||
you may need to refer to their documentation on how configure IOPS correctly.
|
||||
|
||||
1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||
CPU platform on GCP. On different hardware you may find that adjustments, either lower
|
||||
or higher, are required for your CPU or Node counts accordingly. For more information, a
|
||||
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
|
||||
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
|
|
@ -4470,6 +4470,11 @@ type EpicIssue implements Noteable {
|
|||
"""
|
||||
state: IssueState!
|
||||
|
||||
"""
|
||||
Indicates whether an issue is published to the status page
|
||||
"""
|
||||
statusPagePublishedIncident: Boolean
|
||||
|
||||
"""
|
||||
Indicates the currently logged in user is subscribed to the issue
|
||||
"""
|
||||
|
@ -6103,6 +6108,11 @@ type Issue implements Noteable {
|
|||
"""
|
||||
state: IssueState!
|
||||
|
||||
"""
|
||||
Indicates whether an issue is published to the status page
|
||||
"""
|
||||
statusPagePublishedIncident: Boolean
|
||||
|
||||
"""
|
||||
Indicates the currently logged in user is subscribed to the issue
|
||||
"""
|
||||
|
|
|
@ -12461,6 +12461,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "statusPagePublishedIncident",
|
||||
"description": "Indicates whether an issue is published to the status page",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "subscribed",
|
||||
"description": "Indicates the currently logged in user is subscribed to the issue",
|
||||
|
@ -16817,6 +16831,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "statusPagePublishedIncident",
|
||||
"description": "Indicates whether an issue is published to the status page",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "subscribed",
|
||||
"description": "Indicates the currently logged in user is subscribed to the issue",
|
||||
|
|
|
@ -750,6 +750,7 @@ Relationship between an epic and an issue
|
|||
| `relationPath` | String | URI path of the epic-issue relation |
|
||||
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
||||
| `state` | IssueState! | State of the issue |
|
||||
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
|
||||
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
|
||||
| `taskCompletionStatus` | TaskCompletionStatus! | Task completion status of the issue |
|
||||
| `timeEstimate` | Int! | Time estimate of the issue |
|
||||
|
@ -916,6 +917,7 @@ Represents a Group Member
|
|||
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
|
||||
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
||||
| `state` | IssueState! | State of the issue |
|
||||
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
|
||||
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
|
||||
| `taskCompletionStatus` | TaskCompletionStatus! | Task completion status of the issue |
|
||||
| `timeEstimate` | Int! | Time estimate of the issue |
|
||||
|
|
|
@ -11,7 +11,7 @@ Available `RAILS_ENV`:
|
|||
- `development` (this is your main GDK db).
|
||||
- `test` (used for tests like RSpec).
|
||||
|
||||
## Nuke everything and start over
|
||||
## Delete everything and start over
|
||||
|
||||
If you just want to delete everything and start over with an empty DB (approximately 1 minute):
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ bundle exec guard
|
|||
|
||||
When using spring and guard together, use `SPRING=1 bundle exec guard` instead to make use of spring.
|
||||
|
||||
Use [Factory Doctor](https://test-prof.evilmartians.io/#/factory_doctor.md) to find cases on un-necessary database manipulation, which can cause slow tests.
|
||||
Use [Factory Doctor](https://test-prof.evilmartians.io/#/profilers/factory_doctor) to find cases on un-necessary database manipulation, which can cause slow tests.
|
||||
|
||||
```shell
|
||||
# run test for path
|
||||
|
@ -261,8 +261,8 @@ As much as possible, do not implement this using `before(:all)` or `before(:cont
|
|||
you would need to manually clean up the data as those hooks run outside a database transaction.
|
||||
|
||||
Instead, this can be achieved by using
|
||||
[`let_it_be`](https://test-prof.evilmartians.io/#/let_it_be) variables and the
|
||||
[`before_all`](https://test-prof.evilmartians.io/#/before_all) hook
|
||||
[`let_it_be`](https://test-prof.evilmartians.io/#/recipes/let_it_be) variables and the
|
||||
[`before_all`](https://test-prof.evilmartians.io/#/recipes/before_all) hook
|
||||
from the [`test-prof` gem](https://rubygems.org/gems/test-prof).
|
||||
|
||||
```ruby
|
||||
|
|
|
@ -67,12 +67,10 @@ For instance, consider the following workflow:
|
|||
|
||||
## Example configuration
|
||||
|
||||
CAUTION: **Caution:**
|
||||
The job definition shown below is supported on GitLab 11.11 and later versions. It
|
||||
also requires the GitLab Runner 11.5 or later. For earlier versions, use the
|
||||
[previous job definitions](#previous-job-definitions).
|
||||
|
||||
This example shows how to run Code Quality on your code by using GitLab CI/CD and Docker.
|
||||
It requires GitLab 11.11 or later, and GitLab Runner 11.5 or later. If you are using
|
||||
GitLab 11.4 or ealier, you can view the deprecated job definitions in the
|
||||
[documentation archive](https://docs.gitlab.com/12.10/ee/user/project/merge_requests/code_quality.html#previous-job-definitions).
|
||||
|
||||
First, you need GitLab Runner configured:
|
||||
|
||||
|
@ -132,105 +130,6 @@ definition they will be able to execute privileged Docker commands on the Runner
|
|||
host. Having proper access control policies mitigates this attack vector by
|
||||
allowing access only to trusted actors.
|
||||
|
||||
### Previous job definitions
|
||||
|
||||
CAUTION: **Caution:**
|
||||
Before GitLab 11.5, Code Quality job and artifact had to be named specifically to
|
||||
automatically extract report data and show it in the merge request widget. While these
|
||||
old job definitions are still maintained they have been deprecated and are no longer supported on GitLab 12.0 or higher.
|
||||
You're advised to update your `.gitlab-ci.yml` configuration to reflect that change.
|
||||
|
||||
For GitLab 11.5 and later, the job should look like:
|
||||
|
||||
```yaml
|
||||
code_quality:
|
||||
image: docker:stable
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
allow_failure: true
|
||||
services:
|
||||
- docker:stable-dind
|
||||
script:
|
||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
||||
- docker run
|
||||
--env SOURCE_CODE="$PWD"
|
||||
--volume "$PWD":/code
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/ci-cd/codequality:$SP_VERSION" /code
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: gl-code-quality-report.json
|
||||
```
|
||||
|
||||
In GitLab 12.6, Code Quality switched to the
|
||||
[new versioning scheme](https://gitlab.com/gitlab-org/ci-cd/codequality#versioning-and-release-cycle).
|
||||
It's highly recommended to include the Code Quality template as shown in the
|
||||
[example configuration](#example-configuration), which uses the new versioning scheme.
|
||||
If not using the template, the `SP_VERSION` variable can be hardcoded to use the
|
||||
new image versions:
|
||||
|
||||
```yaml
|
||||
code_quality:
|
||||
image: docker:stable
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
SP_VERSION: 0.85.6
|
||||
allow_failure: true
|
||||
services:
|
||||
- docker:stable-dind
|
||||
script:
|
||||
- docker run
|
||||
--env SOURCE_CODE="$PWD"
|
||||
--volume "$PWD":/code
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/ci-cd/codequality:$SP_VERSION" /code
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: gl-code-quality-report.json
|
||||
```
|
||||
|
||||
For GitLab 11.4 and earlier, the job should look like:
|
||||
|
||||
```yaml
|
||||
code_quality:
|
||||
image: docker:stable
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
allow_failure: true
|
||||
services:
|
||||
- docker:stable-dind
|
||||
script:
|
||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
||||
- docker run
|
||||
--env SOURCE_CODE="$PWD"
|
||||
--volume "$PWD":/code
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/ci-cd/codequality:$SP_VERSION" /code
|
||||
artifacts:
|
||||
paths: [gl-code-quality-report.json]
|
||||
```
|
||||
|
||||
Alternatively the job name could be `codeclimate` or `codequality` and the artifact
|
||||
name could be `codeclimate.json`. These names have been deprecated with GitLab 11.0
|
||||
and may be removed in the next major release, GitLab 12.0.
|
||||
|
||||
For GitLab 10.3 and earlier, the job should look like:
|
||||
|
||||
```yaml
|
||||
codequality:
|
||||
image: docker:latest
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- docker pull codeclimate/codeclimate:0.69.0
|
||||
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
|
||||
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
|
||||
artifacts:
|
||||
paths: [codeclimate.json]
|
||||
```
|
||||
|
||||
## Configuring jobs using variables
|
||||
|
||||
The Code Quality job supports environment variables that users can set to
|
||||
|
|
|
@ -40,12 +40,8 @@ module API
|
|||
|
||||
repository = user_project.repository
|
||||
|
||||
if Feature.enabled?(:branch_list_keyset_pagination, user_project)
|
||||
branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute(gitaly_pagination: true)
|
||||
else
|
||||
branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute
|
||||
branches = paginate(::Kaminari.paginate_array(branches))
|
||||
end
|
||||
branches_finder = BranchesFinder.new(repository, declared_params(include_missing: false))
|
||||
branches = Gitlab::Pagination::GitalyKeysetPager.new(self, user_project).paginate(branches_finder)
|
||||
|
||||
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ module Gitlab
|
|||
THREAD_KEY = :_gitlab_metrics_transaction
|
||||
|
||||
SMALL_BUCKETS = [0.1, 0.25, 0.5, 1.0, 2.5, 5.0].freeze
|
||||
BIG_BUCKETS = [100, 1000, 10000, 100000, 1000000, 10000000].freeze
|
||||
|
||||
# The series to store events (e.g. Git pushes) in.
|
||||
EVENT_SERIES = 'events'
|
||||
|
@ -43,9 +42,6 @@ module Gitlab
|
|||
|
||||
@started_at = nil
|
||||
@finished_at = nil
|
||||
|
||||
@memory_before = 0
|
||||
@memory_after = 0
|
||||
end
|
||||
|
||||
def duration
|
||||
|
@ -56,20 +52,14 @@ module Gitlab
|
|||
System.thread_cpu_duration(@thread_cputime_start)
|
||||
end
|
||||
|
||||
def allocated_memory
|
||||
@memory_after - @memory_before
|
||||
end
|
||||
|
||||
def run
|
||||
Thread.current[THREAD_KEY] = self
|
||||
|
||||
@memory_before = System.memory_usage_rss
|
||||
@started_at = System.monotonic_time
|
||||
@thread_cputime_start = System.thread_cpu_time
|
||||
|
||||
yield
|
||||
ensure
|
||||
@memory_after = System.memory_usage_rss
|
||||
@finished_at = System.monotonic_time
|
||||
|
||||
observe(:gitlab_transaction_cputime_seconds, thread_cpu_duration) do
|
||||
|
@ -78,9 +68,6 @@ module Gitlab
|
|||
observe(:gitlab_transaction_duration_seconds, duration) do
|
||||
buckets SMALL_BUCKETS
|
||||
end
|
||||
observe(:gitlab_transaction_allocated_memory_bytes, allocated_memory * 1024.0) do
|
||||
buckets BIG_BUCKETS
|
||||
end
|
||||
|
||||
Thread.current[THREAD_KEY] = nil
|
||||
end
|
||||
|
|
54
lib/gitlab/pagination/gitaly_keyset_pager.rb
Normal file
54
lib/gitlab/pagination/gitaly_keyset_pager.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Pagination
|
||||
class GitalyKeysetPager
|
||||
attr_reader :request_context, :project
|
||||
delegate :params, to: :request_context
|
||||
|
||||
def initialize(request_context, project)
|
||||
@request_context = request_context
|
||||
@project = project
|
||||
end
|
||||
|
||||
# It is expected that the given finder will respond to `execute` method with `gitaly_pagination: true` option
|
||||
# and supports pagination via gitaly.
|
||||
def paginate(finder)
|
||||
return paginate_via_gitaly(finder) if keyset_pagination_enabled?
|
||||
|
||||
branches = ::Kaminari.paginate_array(finder.execute)
|
||||
Gitlab::Pagination::OffsetPagination
|
||||
.new(request_context)
|
||||
.paginate(branches)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keyset_pagination_enabled?
|
||||
Feature.enabled?(:branch_list_keyset_pagination, project) && params[:pagination] == 'keyset'
|
||||
end
|
||||
|
||||
def paginate_via_gitaly(finder)
|
||||
finder.execute(gitaly_pagination: true).tap do |records|
|
||||
apply_headers(records)
|
||||
end
|
||||
end
|
||||
|
||||
def apply_headers(records)
|
||||
if records.count == params[:per_page]
|
||||
Gitlab::Pagination::Keyset::HeaderBuilder
|
||||
.new(request_context)
|
||||
.add_next_page_header(
|
||||
query_params_for(records.last)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def query_params_for(record)
|
||||
# NOTE: page_token is name for now, but it could be dynamic if we have other gitaly finders
|
||||
# that is based on something other than name
|
||||
{ page_token: record.name }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
45
lib/gitlab/pagination/keyset/header_builder.rb
Normal file
45
lib/gitlab/pagination/keyset/header_builder.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Pagination
|
||||
module Keyset
|
||||
class HeaderBuilder
|
||||
attr_reader :request_context
|
||||
delegate :params, :header, :request, to: :request_context
|
||||
|
||||
def initialize(request_context)
|
||||
@request_context = request_context
|
||||
end
|
||||
|
||||
def add_next_page_header(query_params)
|
||||
link = next_page_link(page_href(query_params))
|
||||
header('Links', link)
|
||||
header('Link', link)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def next_page_link(href)
|
||||
%(<#{href}>; rel="next")
|
||||
end
|
||||
|
||||
def page_href(query_params)
|
||||
base_request_uri.tap do |uri|
|
||||
uri.query = updated_params(query_params).to_query
|
||||
end.to_s
|
||||
end
|
||||
|
||||
def base_request_uri
|
||||
@base_request_uri ||= URI.parse(request.url).tap do |uri|
|
||||
uri.host = Gitlab.config.gitlab.host
|
||||
uri.port = Gitlab.config.gitlab.port
|
||||
end
|
||||
end
|
||||
|
||||
def updated_params(query_params)
|
||||
params.merge(query_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,9 +24,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def apply_headers(next_page)
|
||||
link = pagination_links(next_page)
|
||||
request.header('Links', link)
|
||||
request.header('Link', link)
|
||||
Gitlab::Pagination::Keyset::HeaderBuilder
|
||||
.new(request)
|
||||
.add_next_page_header(
|
||||
query_params_for(next_page)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -63,25 +65,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def page_href(page)
|
||||
base_request_uri.tap do |uri|
|
||||
uri.query = query_params_for(page).to_query
|
||||
end.to_s
|
||||
end
|
||||
|
||||
def pagination_links(next_page)
|
||||
%(<#{page_href(next_page)}>; rel="next")
|
||||
end
|
||||
|
||||
def base_request_uri
|
||||
@base_request_uri ||= URI.parse(request.request.url).tap do |uri|
|
||||
uri.host = Gitlab.config.gitlab.host
|
||||
uri.port = Gitlab.config.gitlab.port
|
||||
end
|
||||
end
|
||||
|
||||
def query_params_for(page)
|
||||
request.params.merge(lower_bounds_params(page))
|
||||
lower_bounds_params(page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12743,12 +12743,21 @@ msgstr ""
|
|||
msgid "IncidentManagement|Open"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Published"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Published to status page"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|There was an error displaying the incidents."
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Unassigned"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Unpublished"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentSettings|Alert integration"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.156.0",
|
||||
"@gitlab/ui": "17.39.0",
|
||||
"@gitlab/ui": "17.40.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
|
|
|
@ -11,11 +11,7 @@ RSpec.describe 'Group issues page' do
|
|||
let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) }
|
||||
let(:path) { issues_group_path(group) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vue_issuables_list: false)
|
||||
end
|
||||
|
||||
context 'with shared examples' do
|
||||
context 'with shared examples', :js do
|
||||
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
|
||||
|
||||
include_examples 'project features apply to issuables', Issue
|
||||
|
@ -30,19 +26,33 @@ RSpec.describe 'Group issues page' do
|
|||
user_in_group
|
||||
end
|
||||
|
||||
it_behaves_like "it has an RSS button with current_user's feed token"
|
||||
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
|
||||
|
||||
# Note: The one from rss_shared_example.rb uses a css pseudo-class `:has`
|
||||
# which is VERY experimental and only supported in Nokogiri used by Capybara
|
||||
# However,`:js` option forces Capybara to use Selenium that doesn't support`:has`
|
||||
context "it has an RSS button with current_user's feed token" do
|
||||
it "shows the RSS button with current_user's feed token" do
|
||||
expect(find('[data-testid="rss-feed-link"]')['href']).to have_content(user.feed_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
let(:user) { nil }
|
||||
|
||||
it_behaves_like "it has an RSS button without a feed token"
|
||||
it_behaves_like "an autodiscoverable RSS feed without a feed token"
|
||||
|
||||
# Note: please see the above
|
||||
context "it has an RSS button without a feed token" do
|
||||
it "shows the RSS button without a feed token" do
|
||||
expect(find('[data-testid="rss-feed-link"]')['href']).not_to have_content('feed_token')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'assignee', :js do
|
||||
context 'assignee' do
|
||||
let(:access_level) { ProjectFeature::ENABLED }
|
||||
let(:user) { user_in_group }
|
||||
let(:user2) { user_outside_group }
|
||||
|
@ -56,7 +66,7 @@ RSpec.describe 'Group issues page' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'issues list' do
|
||||
context 'issues list', :js do
|
||||
let(:subgroup) { create(:group, parent: group) }
|
||||
let(:subgroup_project) { create(:project, :public, group: subgroup)}
|
||||
let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
|
||||
|
@ -110,7 +120,7 @@ RSpec.describe 'Group issues page' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'manual ordering' do
|
||||
context 'manual ordering', :js do
|
||||
let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
|
||||
|
||||
let!(:issue1) { create(:issue, project: project, title: 'Issue #1', relative_position: 1) }
|
||||
|
@ -143,9 +153,11 @@ RSpec.describe 'Group issues page' do
|
|||
end
|
||||
end
|
||||
|
||||
it 'issues should be draggable and persist order', :js do
|
||||
it 'issues should be draggable and persist order' do
|
||||
visit issues_group_path(group, sort: 'relative_position')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
drag_to(selector: '.manual-ordering',
|
||||
from_index: 0,
|
||||
to_index: 2)
|
||||
|
@ -159,11 +171,13 @@ RSpec.describe 'Group issues page' do
|
|||
check_issue_order
|
||||
end
|
||||
|
||||
it 'issues should not be draggable when user is not logged in', :js do
|
||||
it 'issues should not be draggable when user is not logged in' do
|
||||
sign_out(user_in_group)
|
||||
|
||||
visit issues_group_path(group, sort: 'relative_position')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
drag_to(selector: '.manual-ordering',
|
||||
from_index: 0,
|
||||
to_index: 2)
|
||||
|
@ -187,7 +201,7 @@ RSpec.describe 'Group issues page' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'issues pagination' do
|
||||
context 'issues pagination', :js do
|
||||
let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
|
||||
|
||||
let!(:issues) do
|
||||
|
@ -204,7 +218,9 @@ RSpec.describe 'Group issues page' do
|
|||
end
|
||||
|
||||
it 'first pagination item is active' do
|
||||
expect(page).to have_css(".js-first-button a.page-link.active")
|
||||
page.within('.gl-pagination') do
|
||||
expect(find('.active')).to have_content('1')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -487,7 +487,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
find('.tribute-container .highlight').click
|
||||
find('.tribute-container .highlight', visible: true).click
|
||||
|
||||
click_button 'Save changes'
|
||||
|
||||
|
@ -501,7 +501,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
find('#note-body').native.send_keys('@')
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.tribute-container')
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
end
|
||||
|
||||
it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
|
||||
|
@ -511,20 +511,9 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.tribute-container')
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
|
||||
page.within '.tribute-container ul' do
|
||||
expect(find('li').text).to have_content(user_xss.username)
|
||||
end
|
||||
end
|
||||
|
||||
it 'doesnt open autocomplete menu character is prefixed with text' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys('testing')
|
||||
find('#note-body').native.send_keys('@')
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container')
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content(user_xss.username)
|
||||
end
|
||||
|
||||
it 'selects the first item for assignee dropdowns' do
|
||||
|
@ -532,11 +521,11 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
find('#note-body').native.send_keys('@')
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.tribute-container')
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul')).to have_selector('.highlight:first-of-type')
|
||||
expect(find('.tribute-container ul', visible: true)).to have_selector('.highlight:first-of-type')
|
||||
end
|
||||
|
||||
it 'includes items for assignee dropdowns with non-ASCII characters in name' do
|
||||
|
@ -545,14 +534,26 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
simulate_input('#note-body', "@#{user.name[0...8]}")
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.tribute-container')
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container')).to have_content(user.name)
|
||||
expect(find('.tribute-container ul', visible: true)).to have_content(user.name)
|
||||
end
|
||||
|
||||
context 'if a selected value has special characters' do
|
||||
it 'wraps the result in double quotes' do
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys('')
|
||||
simulate_input('#note-body', "~#{label.title[0]}")
|
||||
end
|
||||
|
||||
label_item = find('.tribute-container ul', text: label.title, visible: true)
|
||||
|
||||
expect_to_wrap(true, label_item, note, label.title)
|
||||
end
|
||||
|
||||
it "shows dropdown after a new line" do
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
|
@ -562,7 +563,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
note.native.send_keys('@')
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.tribute-container')
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
end
|
||||
|
||||
it "does not show dropdown when preceded with a special character" do
|
||||
|
@ -571,12 +572,21 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
note.native.send_keys("@")
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.tribute-container')
|
||||
expect(page).to have_selector('.tribute-container', visible: true)
|
||||
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys("@")
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container')
|
||||
end
|
||||
|
||||
it "does not throw an error if no labels exist" do
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys('~')
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.tribute-container', visible: false)
|
||||
end
|
||||
|
||||
|
@ -586,7 +596,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
note.native.send_keys("@#{user.username[0]}")
|
||||
end
|
||||
|
||||
user_item = find('.tribute-container li', text: user.username)
|
||||
user_item = find('.tribute-container ul', text: user.username, visible: true)
|
||||
|
||||
expect_to_wrap(false, user_item, note, user.username)
|
||||
end
|
||||
|
@ -611,7 +621,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
user_item = find('.tribute-container li', text: user.username)
|
||||
user_item = find('.tribute-container ul', text: user.username, visible: true)
|
||||
expect(user_item).to have_content(user.username)
|
||||
end
|
||||
end
|
||||
|
@ -640,8 +650,99 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul')).not_to have_content(user.username)
|
||||
expect(find('.tribute-container ul')).to have_content(unassigned_user.username)
|
||||
expect(find('.tribute-container ul', visible: true)).not_to have_content(user.username)
|
||||
expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username)
|
||||
end
|
||||
|
||||
it 'lists users who are currently not assigned to the issue when using /assign on the second line' do
|
||||
visit project_issue_path(project, issue_assignee)
|
||||
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys('/assign @user2')
|
||||
note.native.send_keys(:enter)
|
||||
note.native.send_keys('/assign @')
|
||||
note.native.send_keys(:right)
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true)).not_to have_content(user.username)
|
||||
expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username)
|
||||
end
|
||||
end
|
||||
|
||||
context 'labels' do
|
||||
it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
|
||||
label_xss_title = 'alert label <img src=x onerror="alert(\'Hello xss\');" a'
|
||||
create(:label, project: project, title: label_xss_title)
|
||||
|
||||
note = find('#note-body')
|
||||
|
||||
# It should show all the labels on "~".
|
||||
type(note, '~')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content('alert label')
|
||||
end
|
||||
|
||||
it 'allows colons when autocompleting scoped labels' do
|
||||
create(:label, project: project, title: 'scoped:label')
|
||||
|
||||
note = find('#note-body')
|
||||
type(note, '~scoped:')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content('scoped:label')
|
||||
end
|
||||
|
||||
it 'allows colons when autocompleting scoped labels with double colons' do
|
||||
create(:label, project: project, title: 'scoped::label')
|
||||
|
||||
note = find('#note-body')
|
||||
type(note, '~scoped::')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content('scoped::label')
|
||||
end
|
||||
|
||||
it 'autocompletes multi-word labels' do
|
||||
create(:label, project: project, title: 'Accepting merge requests')
|
||||
|
||||
note = find('#note-body')
|
||||
type(note, '~Acceptingmerge')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content('Accepting merge requests')
|
||||
end
|
||||
|
||||
it 'only autocompletes the latest label' do
|
||||
create(:label, project: project, title: 'documentation')
|
||||
create(:label, project: project, title: 'feature')
|
||||
|
||||
note = find('#note-body')
|
||||
type(note, '~documentation foo bar ~feat')
|
||||
note.native.send_keys(:right)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true).text).to have_content('feature')
|
||||
expect(find('.tribute-container ul', visible: true).text).not_to have_content('documentation')
|
||||
end
|
||||
|
||||
it 'does not autocomplete labels if no tilde is typed' do
|
||||
create(:label, project: project, title: 'documentation')
|
||||
|
||||
note = find('#note-body')
|
||||
type(note, 'document')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,10 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe "Internal Project Snippets Access" do
|
||||
include AccessMatchers
|
||||
|
||||
let(:project) { create(:project, :internal) }
|
||||
|
||||
let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) }
|
||||
let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
|
||||
let_it_be(:project) { create(:project, :internal) }
|
||||
let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: project.owner) }
|
||||
let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
|
||||
|
||||
describe "GET /:project_path/snippets" do
|
||||
subject { project_snippets_path(project) }
|
||||
|
|
|
@ -56,6 +56,7 @@ describe('Incidents List', () => {
|
|||
newIssuePath,
|
||||
incidentTemplateName,
|
||||
issuePath: '/project/isssues',
|
||||
publishedAvailable: true,
|
||||
},
|
||||
stubs: {
|
||||
GlButton: true,
|
||||
|
|
|
@ -26,13 +26,14 @@ describe('Ref selector component', () => {
|
|||
let tagsApiCallSpy;
|
||||
let commitApiCallSpy;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
const createComponent = (props = {}, attrs = {}) => {
|
||||
wrapper = mount(RefSelector, {
|
||||
propsData: {
|
||||
projectId,
|
||||
value: '',
|
||||
...props,
|
||||
},
|
||||
attrs,
|
||||
listeners: {
|
||||
// simulate a parent component v-model binding
|
||||
input: selectedRef => {
|
||||
|
@ -164,6 +165,20 @@ describe('Ref selector component', () => {
|
|||
});
|
||||
|
||||
describe('post-initialization behavior', () => {
|
||||
describe('when the parent component provides an `id` binding', () => {
|
||||
const id = 'git-ref';
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({}, { id });
|
||||
|
||||
return waitForRequests();
|
||||
});
|
||||
|
||||
it('adds the provided ID to the GlNewDropdown instance', () => {
|
||||
expect(wrapper.attributes().id).toBe(id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a ref is pre-selected', () => {
|
||||
const preselectedRef = fixtures.branches[0].name;
|
||||
|
||||
|
|
|
@ -28,14 +28,6 @@ RSpec.describe Gitlab::Metrics::Transaction do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#allocated_memory' do
|
||||
it 'returns the allocated memory in bytes' do
|
||||
transaction.run { 'a' * 32 }
|
||||
|
||||
expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
it 'yields the supplied block' do
|
||||
expect { |b| transaction.run(&b) }.to yield_control
|
||||
|
|
|
@ -48,16 +48,6 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#allocated_memory' do
|
||||
include_context 'transaction observe metrics'
|
||||
|
||||
it 'returns the allocated memory in bytes' do
|
||||
transaction.run { 'a' * 32 }
|
||||
|
||||
expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
include_context 'transaction observe metrics'
|
||||
|
||||
|
|
106
spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
Normal file
106
spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
Normal file
|
@ -0,0 +1,106 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
|
||||
let(:pager) { described_class.new(request_context, project) }
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
let(:request_context) { double("request context") }
|
||||
let(:finder) { double("branch finder") }
|
||||
let(:custom_port) { 8080 }
|
||||
let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:#{custom_port}/api/v4/projects" }
|
||||
|
||||
before do
|
||||
stub_config_setting(port: custom_port)
|
||||
end
|
||||
|
||||
describe '.paginate' do
|
||||
let(:base_query) { { per_page: 2 } }
|
||||
let(:query) { base_query }
|
||||
|
||||
before do
|
||||
allow(request_context).to receive(:params).and_return(query)
|
||||
allow(request_context).to receive(:header)
|
||||
end
|
||||
|
||||
shared_examples_for 'offset pagination' do
|
||||
let(:paginated_array) { double 'paginated array' }
|
||||
let(:branches) { [] }
|
||||
|
||||
it 'uses offset pagination' do
|
||||
expect(finder).to receive(:execute).and_return(branches)
|
||||
expect(Kaminari).to receive(:paginate_array).with(branches).and_return(paginated_array)
|
||||
expect_next_instance_of(Gitlab::Pagination::OffsetPagination) do |offset_pagination|
|
||||
expect(offset_pagination).to receive(:paginate).with(paginated_array)
|
||||
end
|
||||
|
||||
pager.paginate(finder)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with branch_list_keyset_pagination feature off' do
|
||||
before do
|
||||
stub_feature_flags(branch_list_keyset_pagination: false)
|
||||
end
|
||||
|
||||
context 'without keyset pagination option' do
|
||||
it_behaves_like 'offset pagination'
|
||||
end
|
||||
|
||||
context 'with keyset pagination option' do
|
||||
let(:query) { base_query.merge(pagination: 'keyset') }
|
||||
|
||||
it_behaves_like 'offset pagination'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with branch_list_keyset_pagination feature on' do
|
||||
before do
|
||||
stub_feature_flags(branch_list_keyset_pagination: project)
|
||||
end
|
||||
|
||||
context 'without keyset pagination option' do
|
||||
it_behaves_like 'offset pagination'
|
||||
end
|
||||
|
||||
context 'with keyset pagination option' do
|
||||
let(:query) { base_query.merge(pagination: 'keyset') }
|
||||
let(:fake_request) { double(url: "#{incoming_api_projects_url}?#{query.to_query}") }
|
||||
|
||||
before do
|
||||
allow(request_context).to receive(:request).and_return(fake_request)
|
||||
expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches)
|
||||
end
|
||||
|
||||
context 'when next page could be available' do
|
||||
let(:branch1) { double 'branch', name: 'branch1' }
|
||||
let(:branch2) { double 'branch', name: 'branch2' }
|
||||
let(:branches) { [branch1, branch2] }
|
||||
|
||||
let(:expected_next_page_link) { %Q(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") }
|
||||
|
||||
it 'uses keyset pagination and adds link headers' do
|
||||
expect(request_context).to receive(:header).with('Links', expected_next_page_link)
|
||||
expect(request_context).to receive(:header).with('Link', expected_next_page_link)
|
||||
|
||||
pager.paginate(finder)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current page is the last page' do
|
||||
let(:branch1) { double 'branch', name: 'branch1' }
|
||||
let(:branches) { [branch1] }
|
||||
|
||||
it 'uses keyset pagination without link headers' do
|
||||
expect(request_context).not_to receive(:header).with('Links', anything)
|
||||
expect(request_context).not_to receive(:header).with('Link', anything)
|
||||
|
||||
pager.paginate(finder)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
25
spec/models/audit_event_partitioned_spec.rb
Normal file
25
spec/models/audit_event_partitioned_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe AuditEventPartitioned do
|
||||
let(:source_table) { AuditEvent }
|
||||
let(:partitioned_table) { described_class }
|
||||
|
||||
it 'has the same columns as the source table' do
|
||||
expect(partitioned_table.column_names).to match_array(source_table.column_names)
|
||||
end
|
||||
|
||||
it 'inserts the same record as the one in the source table', :aggregate_failures do
|
||||
expect { create(:audit_event) }.to change { partitioned_table.count }.by(1)
|
||||
|
||||
event_from_source_table = source_table.connection.select_one(
|
||||
"SELECT * FROM #{source_table.table_name} ORDER BY created_at desc LIMIT 1"
|
||||
)
|
||||
event_from_partitioned_table = partitioned_table.connection.select_one(
|
||||
"SELECT * FROM #{partitioned_table.table_name} ORDER BY created_at desc LIMIT 1"
|
||||
)
|
||||
|
||||
expect(event_from_partitioned_table).to eq(event_from_source_table)
|
||||
end
|
||||
end
|
|
@ -39,9 +39,11 @@ RSpec.describe API::Branches do
|
|||
end
|
||||
|
||||
context 'with branch_list_keyset_pagination feature off' do
|
||||
context 'with legacy pagination params' do
|
||||
let(:base_params) { {} }
|
||||
|
||||
context 'with offset pagination params' do
|
||||
it 'returns the repository branches' do
|
||||
get api(route, current_user), params: { per_page: 100 }
|
||||
get api(route, current_user), params: base_params.merge(per_page: 100)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('public_api/v4/branches')
|
||||
|
@ -53,7 +55,7 @@ RSpec.describe API::Branches do
|
|||
it 'determines only a limited number of merged branch names' do
|
||||
expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
|
||||
|
||||
get api(route, current_user), params: { per_page: 2 }
|
||||
get api(route, current_user), params: base_params.merge(per_page: 2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq 2
|
||||
|
@ -64,7 +66,7 @@ RSpec.describe API::Branches do
|
|||
it 'merge status matches reality on paginated input' do
|
||||
expected_first_branch_name = project.repository.branches_sorted_by('name')[20].name
|
||||
|
||||
get api(route, current_user), params: { per_page: 20, page: 2 }
|
||||
get api(route, current_user), params: base_params.merge(per_page: 20, page: 2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq 20
|
||||
|
@ -74,11 +76,11 @@ RSpec.describe API::Branches do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with gitaly pagination params ' do
|
||||
context 'with gitaly pagination params' do
|
||||
it 'merge status matches reality on paginated input' do
|
||||
expected_first_branch_name = project.repository.branches_sorted_by('name').first.name
|
||||
|
||||
get api(route, current_user), params: { per_page: 20, page_token: 'feature' }
|
||||
get api(route, current_user), params: base_params.merge(per_page: 20, page_token: 'feature')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq 20
|
||||
|
@ -91,52 +93,58 @@ RSpec.describe API::Branches do
|
|||
|
||||
context 'with branch_list_keyset_pagination feature on' do
|
||||
before do
|
||||
stub_feature_flags(branch_list_keyset_pagination: true)
|
||||
stub_feature_flags(branch_list_keyset_pagination: project)
|
||||
end
|
||||
|
||||
context 'with gitaly pagination params ' do
|
||||
it 'returns the repository branches' do
|
||||
get api(route, current_user), params: { per_page: 100 }
|
||||
context 'with keyset pagination option' do
|
||||
let(:base_params) { { pagination: 'keyset' } }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('public_api/v4/branches')
|
||||
branch_names = json_response.map { |x| x['name'] }
|
||||
expect(branch_names).to match_array(project.repository.branch_names)
|
||||
context 'with gitaly pagination params ' do
|
||||
it 'returns the repository branches' do
|
||||
get api(route, current_user), params: base_params.merge(per_page: 100)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('public_api/v4/branches')
|
||||
expect(response.headers).not_to include('Link', 'Links')
|
||||
branch_names = json_response.map { |x| x['name'] }
|
||||
expect(branch_names).to match_array(project.repository.branch_names)
|
||||
end
|
||||
|
||||
it 'determines only a limited number of merged branch names' do
|
||||
expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
|
||||
|
||||
get api(route, current_user), params: base_params.merge(per_page: 2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers).to include('Link', 'Links')
|
||||
expect(json_response.count).to eq 2
|
||||
|
||||
check_merge_status(json_response)
|
||||
end
|
||||
|
||||
it 'merge status matches reality on paginated input' do
|
||||
expected_first_branch_name = project.repository.branches_sorted_by('name').drop_while { |b| b.name <= 'feature' }.first.name
|
||||
|
||||
get api(route, current_user), params: base_params.merge(per_page: 20, page_token: 'feature')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq 20
|
||||
expect(json_response.first['name']).to eq(expected_first_branch_name)
|
||||
|
||||
check_merge_status(json_response)
|
||||
end
|
||||
end
|
||||
|
||||
it 'determines only a limited number of merged branch names' do
|
||||
expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
|
||||
context 'with offset pagination params' do
|
||||
it 'ignores legacy pagination params' do
|
||||
expected_first_branch_name = project.repository.branches_sorted_by('name').first.name
|
||||
get api(route, current_user), params: base_params.merge(per_page: 20, page: 2)
|
||||
|
||||
get api(route, current_user), params: { per_page: 2 }
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.first['name']).to eq(expected_first_branch_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq 2
|
||||
|
||||
check_merge_status(json_response)
|
||||
end
|
||||
|
||||
it 'merge status matches reality on paginated input' do
|
||||
expected_first_branch_name = project.repository.branches_sorted_by('name').drop_while { |b| b.name <= 'feature' }.first.name
|
||||
|
||||
get api(route, current_user), params: { per_page: 20, page_token: 'feature' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq 20
|
||||
expect(json_response.first['name']).to eq(expected_first_branch_name)
|
||||
|
||||
check_merge_status(json_response)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with legacy pagination params' do
|
||||
it 'ignores legacy pagination params' do
|
||||
expected_first_branch_name = project.repository.branches_sorted_by('name').first.name
|
||||
get api(route, current_user), params: { per_page: 20, page: 2 }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.first['name']).to eq(expected_first_branch_name)
|
||||
|
||||
check_merge_status(json_response)
|
||||
check_merge_status(json_response)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -848,10 +848,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.156.0.tgz#2af56246b5d71000ec81abb1281e811a921cdfd1"
|
||||
integrity sha512-+b670Sxkjo80Wb4GKMZQ+xvuwu9sVvql8aS9nzw63FLn84QyqXS+jMjvyDqPAW5kly6B1Eg4Kljq0YawJ0ySBg==
|
||||
|
||||
"@gitlab/ui@17.39.0":
|
||||
version "17.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.39.0.tgz#04417471ac094323581482d354a33cdf0a21ec86"
|
||||
integrity sha512-3KPrw+1cwF+ibY5zo01b6EsSOE2Kgflu7FGmrvJMvEgpK4w2shloGEts4vEJbPEGBpUzpjr3gQinNcoeIYu/JA==
|
||||
"@gitlab/ui@17.40.0":
|
||||
version "17.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.40.0.tgz#70c89a31d5e98382a9b30aaeac13caf924f38b6d"
|
||||
integrity sha512-0Jf1TwE572cZBgWubCkD9F7YE4Vok+r4Jbtx9ORBxmLaxq1XKpGf/TAd3iMftRQ+pr4T011/z0rJYLqde1mUgw==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
Loading…
Reference in a new issue