Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
45a8c43afe
commit
e6fed37d94
|
|
@ -1 +1 @@
|
|||
14.19.0
|
||||
14.20.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import GoogleCloudMenu from '../components/google_cloud_menu.vue';
|
||||
import IncubationBanner from '../components/incubation_banner.vue';
|
||||
import ServiceTable from './service_table.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IncubationBanner,
|
||||
GoogleCloudMenu,
|
||||
ServiceTable,
|
||||
},
|
||||
props: {
|
||||
configurationUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
deploymentsUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
databasesUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
aimlUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
visionAiUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
translationAiUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
languageAiUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<incubation-banner />
|
||||
|
||||
<google-cloud-menu
|
||||
active="aiml"
|
||||
:configuration-url="configurationUrl"
|
||||
:deployments-url="deploymentsUrl"
|
||||
:databases-url="databasesUrl"
|
||||
:aiml-url="aimlUrl"
|
||||
/>
|
||||
|
||||
<service-table
|
||||
:language-ai-url="languageAiUrl"
|
||||
:translation-ai-url="translationAiUrl"
|
||||
:vision-ai-url="visionAiUrl"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -4,11 +4,13 @@ import { s__ } from '~/locale';
|
|||
const CONFIGURATION_KEY = 'configuration';
|
||||
const DEPLOYMENTS_KEY = 'deployments';
|
||||
const DATABASES_KEY = 'databases';
|
||||
const AIML_KEY = 'aiml';
|
||||
|
||||
const i18n = {
|
||||
configuration: { title: s__('CloudSeed|Configuration') },
|
||||
deployments: { title: s__('CloudSeed|Deployments') },
|
||||
databases: { title: s__('CloudSeed|Databases') },
|
||||
aiml: { title: s__('CloudSeed|AI / ML') },
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
@ -29,6 +31,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
aimlUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isConfigurationActive() {
|
||||
|
|
@ -40,6 +47,9 @@ export default {
|
|||
isDatabasesActive() {
|
||||
return this.active === DATABASES_KEY;
|
||||
},
|
||||
isAimlActive() {
|
||||
return this.active === AIML_KEY;
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
|
|
@ -80,6 +90,17 @@ export default {
|
|||
{{ $options.i18n.databases.title }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<a
|
||||
data-testid="aimlLink"
|
||||
role="tab"
|
||||
:href="aimlUrl"
|
||||
class="nav-link gl-tab-nav-item hidden"
|
||||
:class="{ 'gl-tab-nav-item-active': isAimlActive }"
|
||||
>
|
||||
{{ $options.i18n.aiml.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import DiffStatsDropdown from '~/vue_shared/components/diff_stats_dropdown.vue';
|
||||
import { stickyMonitor } from './lib/utils/sticky';
|
||||
|
||||
export const initDiffStatsDropdown = (stickyTop) => {
|
||||
if (stickyTop) {
|
||||
// We spend quite a bit of effort in our CSS to set the correct padding-top on the
|
||||
// layout page, so we re-use the padding set there to determine at what height our
|
||||
// element should be sticky
|
||||
const pageLayout = document.querySelector('.layout-page');
|
||||
const pageLayoutTopOffset = pageLayout
|
||||
? parseFloat(window.getComputedStyle(pageLayout).getPropertyValue('padding-top') || 0)
|
||||
: 0;
|
||||
|
||||
stickyMonitor(document.querySelector('.js-diff-files-changed'), pageLayoutTopOffset, false);
|
||||
}
|
||||
|
||||
export const initDiffStatsDropdown = () => {
|
||||
const el = document.querySelector('.js-diff-stats-dropdown');
|
||||
|
||||
if (!el) {
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
export const createPlaceholder = () => {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.classList.add('sticky-placeholder');
|
||||
|
||||
return placeholder;
|
||||
};
|
||||
|
||||
export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
|
||||
const top = Math.floor(el.offsetTop - scrollY);
|
||||
|
||||
if (top <= stickyTop && !el.classList.contains('is-stuck')) {
|
||||
const placeholder = insertPlaceholder ? createPlaceholder() : null;
|
||||
const heightBefore = el.offsetHeight;
|
||||
|
||||
el.classList.add('is-stuck');
|
||||
|
||||
if (insertPlaceholder) {
|
||||
el.parentNode.insertBefore(placeholder, el.nextElementSibling);
|
||||
|
||||
placeholder.style.height = `${heightBefore - el.offsetHeight}px`;
|
||||
}
|
||||
} else if (top > stickyTop && el.classList.contains('is-stuck')) {
|
||||
el.classList.remove('is-stuck');
|
||||
|
||||
if (
|
||||
insertPlaceholder &&
|
||||
el.nextElementSibling &&
|
||||
el.nextElementSibling.classList.contains('sticky-placeholder')
|
||||
) {
|
||||
el.nextElementSibling.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a listener that will toggle a 'is-stuck' class, based on the current scroll position.
|
||||
*
|
||||
* - If the current environment does not support `position: sticky`, do nothing.
|
||||
*
|
||||
* @param {HTMLElement} el The `position: sticky` element.
|
||||
* @param {Number} stickyTop Used to determine when an element is stuck.
|
||||
* @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck?
|
||||
*/
|
||||
export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
|
||||
if (!el) return;
|
||||
|
||||
if (
|
||||
typeof CSS === 'undefined' ||
|
||||
!CSS.supports('(position: -webkit-sticky) or (position: sticky)')
|
||||
)
|
||||
return;
|
||||
|
||||
document.addEventListener(
|
||||
'scroll',
|
||||
() => isSticky(el, window.scrollY, stickyTop, insertPlaceholder),
|
||||
{
|
||||
passive: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -83,18 +83,6 @@ function scrollToContainer(container) {
|
|||
}
|
||||
}
|
||||
|
||||
function computeTopOffset(tabs) {
|
||||
const navbar = document.querySelector('.navbar-gitlab');
|
||||
const peek = document.getElementById('js-peek');
|
||||
let stickyTop;
|
||||
|
||||
stickyTop = navbar ? navbar.offsetHeight : 0;
|
||||
stickyTop = peek ? stickyTop + peek.offsetHeight : stickyTop;
|
||||
stickyTop = tabs ? stickyTop + tabs.offsetHeight : stickyTop;
|
||||
|
||||
return stickyTop;
|
||||
}
|
||||
|
||||
function mountPipelines() {
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
const { mrWidgetData } = gl;
|
||||
|
|
@ -145,11 +133,11 @@ function destroyPipelines(app) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function loadDiffs({ url, sticky, tabs }) {
|
||||
function loadDiffs({ url, tabs }) {
|
||||
return axios.get(url).then(({ data }) => {
|
||||
const $container = $('#diffs');
|
||||
$container.html(data.html);
|
||||
initDiffStatsDropdown(sticky);
|
||||
initDiffStatsDropdown();
|
||||
|
||||
localTimeAgo(document.querySelectorAll('#diffs .js-timeago'));
|
||||
syntaxHighlight($('#diffs .js-syntax-highlight'));
|
||||
|
|
@ -537,7 +525,6 @@ export default class MergeRequestTabs {
|
|||
|
||||
loadDiffs({
|
||||
url: diffUrl,
|
||||
sticky: computeTopOffset(this.mergeRequestTabs),
|
||||
tabs: this,
|
||||
})
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import '~/sourcegraph/load';
|
|||
import DiffStats from '~/diffs/components/diff_stats.vue';
|
||||
import { initReportAbuse } from '~/projects/report_abuse';
|
||||
|
||||
initDiffStatsDropdown(true);
|
||||
initDiffStatsDropdown();
|
||||
new ZenMode();
|
||||
new ShortcutsNavigation();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import syntaxHighlight from '~/syntax_highlight';
|
|||
initCompareSelector();
|
||||
|
||||
new Diff(); // eslint-disable-line no-new
|
||||
initDiffStatsDropdown(true);
|
||||
initDiffStatsDropdown();
|
||||
GpgBadges.fetch();
|
||||
|
||||
syntaxHighlight([document.querySelector('.files')]);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import syntaxHighlight from '~/syntax_highlight';
|
||||
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
|
||||
import Diff from '~/diff';
|
||||
|
||||
new Diff(); // eslint-disable-line no-new
|
||||
initDiffStatsDropdown();
|
||||
syntaxHighlight([document.querySelector('.files')]);
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default {
|
|||
:is="component"
|
||||
v-for="{ key, component } in $options.tabs"
|
||||
:key="key"
|
||||
class="container-fluid container-limited"
|
||||
class="container-fluid container-limited gl-text-left"
|
||||
:personal-projects="personalProjects"
|
||||
:personal-projects-loading="personalProjectsLoading"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -147,13 +147,5 @@ export default {
|
|||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="diff-stats-additions-deletions-collapsed gl-float-right gl-display-none"
|
||||
data-testid="diff-stats-additions-deletions-collapsed"
|
||||
>
|
||||
<span class="gl-text-green-600 gl-font-weight-bold">+{{ added }}</span>
|
||||
<span class="gl-text-red-500 gl-font-weight-bold">-{{ deleted }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ export default {
|
|||
* openIssuesCount: number;
|
||||
* permissions: {
|
||||
* projectAccess: { accessLevel: 50 };
|
||||
* }[];
|
||||
* };
|
||||
* descriptionHtml: string;
|
||||
* updatedAt: string;
|
||||
* }[]
|
||||
*/
|
||||
projects: {
|
||||
type: Array,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
|
|||
import { __ } from '~/locale';
|
||||
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
|
||||
import { truncate } from '~/lib/utils/text_utility';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
||||
const MAX_TOPICS_TO_SHOW = 3;
|
||||
const MAX_TOPIC_TITLE_LENGTH = 15;
|
||||
|
|
@ -30,6 +32,10 @@ export default {
|
|||
topics: __('Topics'),
|
||||
topicsPopoverTargetText: __('+ %{count} more'),
|
||||
moreTopics: __('More topics'),
|
||||
updated: __('Updated'),
|
||||
},
|
||||
safeHtmlConfig: {
|
||||
ADD_TAGS: ['gl-emoji'],
|
||||
},
|
||||
components: {
|
||||
GlAvatarLabeled,
|
||||
|
|
@ -39,9 +45,11 @@ export default {
|
|||
GlBadge,
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
|
|
@ -62,6 +70,9 @@ export default {
|
|||
* permissions: {
|
||||
* projectAccess: { accessLevel: 50 };
|
||||
* };
|
||||
* descriptionHtml: string;
|
||||
* updatedAt: string;
|
||||
* }
|
||||
*/
|
||||
project: {
|
||||
type: Object,
|
||||
|
|
@ -138,7 +149,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
|
||||
<li class="projects-list-item gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
|
||||
<gl-avatar-labeled
|
||||
class="gl-flex-grow-1"
|
||||
:entity-id="project.id"
|
||||
|
|
@ -158,6 +169,12 @@ export default {
|
|||
accessLevelLabel
|
||||
}}</user-access-role-badge>
|
||||
</template>
|
||||
<div
|
||||
v-if="project.descriptionHtml"
|
||||
v-safe-html:[$options.safeHtmlConfig]="project.descriptionHtml"
|
||||
class="gl-font-sm gl-overflow-hidden gl-line-height-20 description"
|
||||
data-testid="project-description"
|
||||
></div>
|
||||
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
|
||||
<div
|
||||
class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2"
|
||||
|
|
@ -197,7 +214,7 @@ export default {
|
|||
</div>
|
||||
</gl-avatar-labeled>
|
||||
<div
|
||||
class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-mt-0"
|
||||
class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-pl-10 gl-md-pl-0 gl-md-mt-0"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center gl-gap-x-3">
|
||||
<gl-badge v-if="project.archived" variant="warning">{{ $options.i18n.archived }}</gl-badge>
|
||||
|
|
@ -231,6 +248,10 @@ export default {
|
|||
<span>{{ numberToMetricPrefix(project.openIssuesCount) }}</span>
|
||||
</gl-link>
|
||||
</div>
|
||||
<div class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3">
|
||||
<span>{{ $options.i18n.updated }}</span>
|
||||
<time-ago-tooltip :time="project.updatedAt" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -51,12 +51,11 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.is-commit {
|
||||
top: calc(#{$calc-application-header-height} + #{$commit-stat-summary-height});
|
||||
}
|
||||
|
||||
&.is-compare {
|
||||
top: calc(#{$calc-application-header-height} + #{$compare-branches-sticky-header-height});
|
||||
&.is-commit,
|
||||
&.is-compare,
|
||||
&.is-wiki {
|
||||
top: calc(#{$calc-application-header-height});
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -682,40 +681,6 @@ table.code {
|
|||
}
|
||||
}
|
||||
|
||||
.diff-files-changed {
|
||||
background-color: $body-bg;
|
||||
|
||||
.inline-parallel-buttons {
|
||||
@include gl-relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
@include gl-sticky;
|
||||
top: $calc-application-header-height;
|
||||
z-index: 200;
|
||||
|
||||
&.is-stuck {
|
||||
@include gl-py-0;
|
||||
border-top: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
.diff-stats-additions-deletions-expanded,
|
||||
.inline-parallel-buttons {
|
||||
@include gl-display-none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
&.is-stuck {
|
||||
.diff-stats-additions-deletions-collapsed {
|
||||
@include gl-display-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-container {
|
||||
background-color: $gray-light;
|
||||
border-top: 1px solid $white-normal;
|
||||
|
|
|
|||
|
|
@ -739,7 +739,6 @@ $calendar-activity-colors: (
|
|||
*/
|
||||
$commit-max-width-marker-color: rgba(0, 0, 0, 0);
|
||||
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
|
||||
$commit-stat-summary-height: 32px;
|
||||
|
||||
/*
|
||||
* Files
|
||||
|
|
@ -915,11 +914,6 @@ Merge requests
|
|||
*/
|
||||
$mr-tabs-height: 48px;
|
||||
|
||||
/*
|
||||
Compare Branches
|
||||
*/
|
||||
$compare-branches-sticky-header-height: 32px;
|
||||
|
||||
/*
|
||||
Board Swimlanes
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -654,3 +654,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.projects-list-item {
|
||||
.description {
|
||||
max-height: $gl-spacing-scale-8;
|
||||
|
||||
p {
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
// Disable sticky changes bar for tests
|
||||
.diff-files-changed {
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
// Un-hide inputs for @gitlab/ui custom checkboxes and radios so Capybara can target them
|
||||
.custom-control-input {
|
||||
z-index: 500;
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class GraphqlController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
def permitted_multiplex_params
|
||||
params.permit(_json: [:query, :operationName, { variables: {} }])
|
||||
end
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ class GraphqlController < ApplicationController
|
|||
end
|
||||
|
||||
def multiplex_param
|
||||
permitted_params[:_json]
|
||||
permitted_multiplex_params[:_json]
|
||||
end
|
||||
|
||||
def multiplex_queries
|
||||
|
|
@ -221,8 +221,10 @@ class GraphqlController < ApplicationController
|
|||
Gitlab::Graphql::Variables.new(variable_info).to_h
|
||||
end
|
||||
|
||||
# We support Apollo-style query batching where an array of queries will be in the `_json:` key.
|
||||
# https://graphql-ruby.org/queries/multiplex.html#apollo-query-batching
|
||||
def multiplex?
|
||||
multiplex_param.present?
|
||||
params[:_json].is_a?(Array)
|
||||
end
|
||||
|
||||
def authorize_access_api!
|
||||
|
|
|
|||
|
|
@ -146,8 +146,7 @@
|
|||
.settings-content
|
||||
= render 'users_api_limits'
|
||||
|
||||
- if Feature.enabled?(:rate_limit_for_unauthenticated_projects_api_access)
|
||||
= render 'projects_api_limits'
|
||||
= render 'projects_api_limits'
|
||||
|
||||
%section.settings.as-import-export-limits.no-animate#js-import-export-limits-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@
|
|||
- page = local_assigns.fetch(:page, nil)
|
||||
- diff_files = conditionally_paginate_diff_files(diffs, paginate: paginate_diffs, page: page, per: paginate_diffs_per_page)
|
||||
|
||||
.files-changed.diff-files-changed.js-diff-files-changed.gl-py-3
|
||||
.js-diff-files-changed.gl-py-3
|
||||
.files-changed-inner
|
||||
.inline-parallel-buttons.gl-display-none.gl-md-display-flex
|
||||
.inline-parallel-buttons.gl-display-none.gl-md-display-flex.gl-relative
|
||||
- if !diffs_expanded? && diff_files.any?(&:collapsed?)
|
||||
= link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'gl-button btn btn-default'
|
||||
- if show_whitespace_toggle
|
||||
- if current_controller?(:commit)
|
||||
= commit_diff_whitespace_link(diffs.project, @commit, class: 'd-none d-sm-inline-block')
|
||||
- elsif current_controller?('projects/merge_requests/diffs')
|
||||
= diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'd-none d-sm-inline-block')
|
||||
- elsif current_controller?(:compare)
|
||||
= diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'd-none d-sm-inline-block')
|
||||
- elsif current_controller?(:wikis)
|
||||
|
|
|
|||
|
|
@ -28,5 +28,5 @@
|
|||
%pre.commit-description<
|
||||
= preserve(markdown_field(commit, :description))
|
||||
|
||||
= render 'projects/diffs/diffs', diffs: @diffs
|
||||
= render 'projects/diffs/diffs', diffs: @diffs, diff_page_context: "is-wiki"
|
||||
= render 'shared/wikis/sidebar'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: rate_limit_for_unauthenticated_projects_api_access
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391922
|
||||
milestone: '15.10'
|
||||
name: optimize_scope_projects_with_feature_available
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119950/
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410693
|
||||
milestone: '16.0'
|
||||
type: development
|
||||
group: group::tenant scale
|
||||
default_enabled: true
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
- title: "Jira DVCS connector for Jira Cloud and Jira 8.13 and earlier"
|
||||
announcement_milestone: "15.1"
|
||||
removal_milestone: "16.0"
|
||||
breaking_change: true
|
||||
reporter: m_frankiewicz
|
||||
stage: Manage
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362168
|
||||
body: |
|
||||
The [Jira DVCS connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) for Jira Cloud was deprecated in GitLab 15.1 and has been removed in 16.0. Use the [GitLab for Jira Cloud app](https://docs.gitlab.com/ee/integration/jira/connect-app.html) instead. The Jira DVCS connector was also deprecated for Jira 8.13 and earlier. You can only use the Jira DVCS connector with Jira Data Center or Jira Server in Jira 8.14 and later. Upgrade your Jira instance to Jira 8.14 or later, and reconfigure the Jira integration in your GitLab instance.
|
||||
If you cannot upgrade your Jira instance in time and are on GitLab self-managed version, we offer a workaround until GitLab 16.6. This breaking change is deployed in GitLab 16.0 behind a feature flag named `jira_dvcs_end_of_life_amnesty`. The flag is disabled by default, but you can ask an administrator to enable the flag at any time. For questions related to this announcement, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/408185).
|
||||
|
|
@ -18993,6 +18993,7 @@ Represents a product analytics dashboard.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="productanalyticsdashboarddescription"></a>`description` | [`String`](#string) | Description of the dashboard. |
|
||||
| <a id="productanalyticsdashboardpanels"></a>`panels` | [`ProductAnalyticsDashboardPanelConnection!`](#productanalyticsdashboardpanelconnection) | Panels shown on the dashboard. (see [Connections](#connections)) |
|
||||
| <a id="productanalyticsdashboardslug"></a>`slug` | [`String!`](#string) | Slug of the dashboard. |
|
||||
| <a id="productanalyticsdashboardtitle"></a>`title` | [`String!`](#string) | Title of the dashboard. |
|
||||
|
||||
### `ProductAnalyticsDashboardPanel`
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
|
@ -147,7 +147,7 @@ Having addressed the details of the two aformentioned problem-domains, we can mo
|
|||
|
||||
The single, biggest challenge around introducing ClickHouse and related systems would be the ability to make it avaiable to our users running GitLab in self-managed environments. The intended goals of this proposal are intentionally kept within those constraints. It is also prudent to establish that what we're *proposing* here be applicable to applications consuming ClickHouse from inside self-managed environments.
|
||||
|
||||
There are ongoing efforts to streamline distribution and deployment of ClickHouse instances for managed environment within the larger scope of [ClickHouse Usage at GitLab](../../clickhouse_usage/index.md). A few other issues tackling parts of the aforementioned problem are:
|
||||
There are ongoing efforts to streamline distribution and deployment of ClickHouse instances for managed environment within the larger scope of [ClickHouse Usage at GitLab](../clickhouse_usage/index.md). A few other issues tackling parts of the aforementioned problem are:
|
||||
|
||||
- [Research and understand component costs and maintenance requirements of running a ClickHouse instance with GitLab](https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/14384)
|
||||
- [ClickHouse maintenance and cost research](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116669)
|
||||
|
|
@ -10,6 +10,17 @@ participating-stages: []
|
|||
|
||||
# Consider an abstraction layer to interact with ClickHouse or alternatives
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Summary](#summary)
|
||||
- [Motivation](#motivation)
|
||||
- [Goals](#goals)
|
||||
- [Non-goals](#non-goals)
|
||||
- [Possible solutions](#possible-solutions)
|
||||
- [Recommended approach](#recommended-approach)
|
||||
- [Overview of open source tools](#overview-of-open-source-tools)
|
||||
- [Open Questions](#open-questions)
|
||||
|
||||
## Summary
|
||||
|
||||
Provide a solution standardizing read access to ClickHouse or its alternatives for GitLab installations that will not opt-in to install ClickHouse. After analyzing different [open-source tools](#overview-of-open-source-tools) and weighing them against an option to [build a solution internally](#recommended-approach). The current recommended approach proposes to use dedicated database-level drivers to connect to each data source. Additionally, it proposes the usage of [repository pattern](https://martinfowler.com/eaaCatalog/repository.html) to confine optionally database availability complexity to a single application layer.
|
||||
|
|
@ -24,7 +35,7 @@ offering a unified interface for interactions with underlying data stores, to a
|
|||
|
||||
## Goals
|
||||
|
||||
- Limit the impact of optionally available data stores on the overall GitLab application codebase to [single abstraction layer](../../../../development/reusing_abstractions.md#abstractions)
|
||||
- Limit the impact of optionally available data stores on the overall GitLab application codebase to [single abstraction layer](../../../development/reusing_abstractions.md#abstractions)
|
||||
- Support all data store specific features
|
||||
- Support communication for satellite services of the main GitLab application
|
||||
|
||||
|
|
@ -72,7 +83,7 @@ Following ClickHouse documentation there are the following drivers for Ruby and
|
|||
|
||||
To keep the codebase well organized and limit coupling to any specific database engine it is important to encapsulate
|
||||
interactions, including querying data to a single application layer, that would present its interface to layers above in
|
||||
similar vain to [ActiveRecord interface propagation through abstraction layers](../../../../development/reusing_abstractions.md)
|
||||
similar vain to [ActiveRecord interface propagation through abstraction layers](../../../development/reusing_abstractions.md)
|
||||
|
||||
Keeping underlying database engines encapsulated makes the recommended solution a good two-way door decision that
|
||||
keeps the opportunity to introduce other tools later on, while giving groups time to explore and understand their use cases.
|
||||
|
|
@ -81,7 +92,7 @@ At the lowest abstraction layer, it can be expected that there will be a family
|
|||
following MVC pattern implemented by Rails should be classified as _Models_.
|
||||
|
||||
Models-level abstraction builds well into existing patterns and guidelines but unfortunately does not solve the challenge of the optional availability of the ClickHouse database engine for self-managed instances. It is required to design a dedicated entity that will house responsibility of selecting best database to serve business logic request.
|
||||
From the already mentioned existing abstraction [guidelines](../../../../development/reusing_abstractions.md) `Finders` seems to be the closest to the given requirements, due to the fact that `Finders` encapsulate database specific interaction behind their own public API, hiding database vendors detail from all layers above them.
|
||||
From the already mentioned existing abstraction [guidelines](../../../development/reusing_abstractions.md) `Finders` seems to be the closest to the given requirements, due to the fact that `Finders` encapsulate database specific interaction behind their own public API, hiding database vendors detail from all layers above them.
|
||||
|
||||
However, they are closely coupled to `ActiveRecord` ORM framework, and are bound by existing GitLab convention to return `ActiveRecord::Relation` objects, that might be used to compose even more complex queries. That coupling makes `Finders` unfit to deal with the optional availability of ClickHouse because returned data might come from two different databases, and might not be compatible with each other.
|
||||
|
||||
|
|
@ -138,7 +149,7 @@ In this section authors provide an overview of existing 3rd party open-source so
|
|||
|
||||
1. It focuses on the fact whether the proposed abstraction layer can support both ClickHouse and PostgreSQL (must have)
|
||||
1. Additional consideration might be if more than the two must-have storages are supported
|
||||
1. The solution must support the [minimum required versions](../../../../install/requirements.md#postgresql-requirements) for PostgreSQL
|
||||
1. The solution must support the [minimum required versions](../../../install/requirements.md#postgresql-requirements) for PostgreSQL
|
||||
|
||||
##### 3. Protocol compatibility
|
||||
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
status: proposed
|
||||
creation-date: "2023-03-15"
|
||||
authors: [ "@furkanayhan" ]
|
||||
coach: "@grzesiek"
|
||||
approvers: [ "@jreporter", "@cheryl.li" ]
|
||||
owning-stage: "~devops::verify"
|
||||
participating-stages: [ "~devops::package", "~devops::deploy" ]
|
||||
---
|
||||
|
||||
# GitLab CI Events
|
||||
|
||||
## Summary
|
||||
|
||||
In order to unlock innovation and build more value, GitLab is expected to be
|
||||
the center of automation related to DevSecOps processes. We want to transform
|
||||
GitLab into a programming environment, that will make it possible for engineers
|
||||
to model various workflows on top of CI/CD pipelines. Today, users must create
|
||||
custom automation around webhooks or scheduled pipelines to build required
|
||||
workflows.
|
||||
|
||||
In order to make this automation easier for our users, we want to build a
|
||||
powerful CI/CD eventing system, that will make it possible to run pipelines
|
||||
whenever something happens inside or outside of GitLab.
|
||||
|
||||
A typical use-case is to run a CI/CD job whenever someone creates an issue,
|
||||
posts a comment, changes a merge request status from "draft" to "ready for
|
||||
review" or adds a new member to a group.
|
||||
|
||||
To build that new technology, we should:
|
||||
|
||||
1. Emit many hierarchical events from within GitLab in a more advanced way than we do it today.
|
||||
1. Make it affordable to run this automation, that will react to GitLab events, at scale.
|
||||
1. Provide a set of conventions and libraries to make writing the automation easier.
|
||||
|
||||
## Goals
|
||||
|
||||
While ["GitLab Events Platform"](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113700)
|
||||
aims to build new abstractions around emitting events in GitLab, "GitLab CI
|
||||
Events" blueprint is about making it possible to:
|
||||
|
||||
1. Define a way in which users will configure when an event emitted will result in a CI pipeline being run.
|
||||
1. Describe technology required to match subscriptions with events at GitLab.com scale and beyond.
|
||||
1. Describe technology we could use to reduce the cost of running automation jobs significantly.
|
||||
|
||||
## Proposals
|
||||
|
||||
For now, we have technical 4 proposals;
|
||||
|
||||
1. [Proposal 1: Using the `.gitlab-ci.yml` file](proposal-1-using-the-gitlab-ci-file.md)
|
||||
Based on;
|
||||
- [GitLab CI Workflows PoC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91244)
|
||||
- [PoC NPM CI events](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111693)
|
||||
1. [Proposal 2: Using the `rules` keyword](proposal-2-using-the-rules-keyword.md)
|
||||
Highly inefficient way.
|
||||
1. [Proposal 3: Using the `.gitlab/ci/events` folder](proposal-3-using-the-gitlab-ci-events-folder.md)
|
||||
Involves file reading for every event.
|
||||
1. [Proposal 4: Creating events via CI files](proposal-4-creating-events-via-ci-files.md)
|
||||
Combination of some proposals.
|
||||
|
||||
Each of them has its pros and cons. There could be many more proposals and we
|
||||
would like to discuss them all. We can combine the best part of those proposals
|
||||
and create a new one.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
owning-stage: "~devops::verify"
|
||||
description: 'GitLab CI Events Proposal 1: Using the .gitlab-ci.yml file'
|
||||
---
|
||||
|
||||
# GitLab CI Events Proposal 1: Using the `.gitlab-ci.yml` file
|
||||
|
||||
Currently, we have two proof-of-concept (POC) implementations:
|
||||
|
||||
- [GitLab CI Workflows PoC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91244)
|
||||
- [PoC NPM CI events](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111693)
|
||||
|
||||
They both have similar ideas;
|
||||
|
||||
1. Find a new CI Config syntax to define the pipeline events.
|
||||
|
||||
Example 1:
|
||||
|
||||
```yaml
|
||||
workflow:
|
||||
events:
|
||||
- events/package/published
|
||||
|
||||
# or
|
||||
|
||||
workflow:
|
||||
on:
|
||||
- events/package/published
|
||||
```
|
||||
|
||||
Example 2:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
on:
|
||||
- events/package/published
|
||||
- events/package/removed
|
||||
# on:
|
||||
# package: [published, removed]
|
||||
---
|
||||
do_something:
|
||||
script: echo "Hello World"
|
||||
```
|
||||
|
||||
1. Upsert an event to the database when creating a pipeline.
|
||||
1. Create [EventStore subscriptions](../../../development/event_store.md) to handle the events.
|
||||
|
||||
## Problems & Questions
|
||||
|
||||
1. The CI config of a project can be anything;
|
||||
- `.gitlab-ci.yml` by default
|
||||
- another file in the project
|
||||
- another file in another project
|
||||
- completely a remote/external file
|
||||
|
||||
How do we handle these cases?
|
||||
1. Since we have these problems above, should we keep the events in its own file? (`.gitlab-ci-events.yml`)
|
||||
1. Do we only accept the changes in the main branch?
|
||||
1. We try to create event subscriptions every time a pipeline is created.
|
||||
1. Can we move the existing workflows into the new CI events, for example, `merge_request_event`?
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
owning-stage: "~devops::verify"
|
||||
description: 'GitLab CI Events Proposal 2: Using the rules keyword'
|
||||
---
|
||||
|
||||
# GitLab CI Events Proposal 2: Using the `rules` keyword
|
||||
|
||||
Can we do it with our current [`rules`](../../../ci/yaml/index.md#rules) system?
|
||||
|
||||
```yaml
|
||||
workflow:
|
||||
rules:
|
||||
- events: ["package/*"]
|
||||
|
||||
test_package_published:
|
||||
script: echo testing published package
|
||||
rules:
|
||||
- events: ["package/published"]
|
||||
|
||||
test_package_removed:
|
||||
script: echo testing removed package
|
||||
rules:
|
||||
- events: ["package/removed"]
|
||||
```
|
||||
|
||||
1. We don't upsert anything to the database.
|
||||
1. We'll have a single worker which subcribes to events
|
||||
like `store.subscribe ::Ci::CreatePipelineFromEventWorker, to: ::Issues::CreatedEvent`.
|
||||
1. The worker just runs `Ci::CreatePipelineService` with the correct parameters, the rest
|
||||
will be handled by the `rules` system. Of course, we'll need modifications to the `rules` system to support `events`.
|
||||
|
||||
## Problems & Questions
|
||||
|
||||
1. For every defined event run, we need to enqueue a new `Ci::CreatePipelineFromEventWorker` job.
|
||||
1. The worker will need to run `Ci::CreatePipelineService` for every event run.
|
||||
This may be costly because we go through every cycle of `Ci::CreatePipelineService`.
|
||||
1. This would be highly inefficient.
|
||||
1. Can we move the existing workflows into the new CI events, for example, `merge_request_event`?
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
owning-stage: "~devops::verify"
|
||||
description: 'GitLab CI Events Proposal 3: Using the .gitlab/ci/events folder'
|
||||
---
|
||||
|
||||
# GitLab CI Events Proposal 3: Using the `.gitlab/ci/events` folder
|
||||
|
||||
We can also approach this problem by creating separate files for events.
|
||||
|
||||
Let's say we'll have the `.gitlab/ci/events` folder (or `.gitlab/workflows/ci`).
|
||||
|
||||
We can define events in the following format:
|
||||
|
||||
```yaml
|
||||
# .gitlab/ci/events/package-published.yml
|
||||
|
||||
spec:
|
||||
events:
|
||||
- name: package/published
|
||||
|
||||
---
|
||||
|
||||
include:
|
||||
- local: .gitlab-ci.yml
|
||||
with:
|
||||
event: $[[ gitlab.event.name ]]
|
||||
```
|
||||
|
||||
And in the `.gitlab-ci.yml` file, we can use the input;
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
spec:
|
||||
inputs:
|
||||
event:
|
||||
default: push
|
||||
|
||||
---
|
||||
|
||||
job1:
|
||||
script: echo "Hello World"
|
||||
|
||||
job2:
|
||||
script: echo "Hello World"
|
||||
|
||||
job-for-package-published:
|
||||
script: echo "Hello World"
|
||||
rules:
|
||||
- if: $[[ inputs.event ]] == "package/published"
|
||||
```
|
||||
|
||||
When an event happens;
|
||||
|
||||
1. We'll enqueue a new job for the event.
|
||||
1. The job will search for the event file in the `.gitlab/ci/events` folder.
|
||||
1. The job will run `Ci::CreatePipelineService` for the event file.
|
||||
|
||||
## Problems & Questions
|
||||
|
||||
1. For every defined event run, we need to enqueue a new job.
|
||||
1. Every event-job will need to search for files.
|
||||
1. This would be only for the project-scope events.
|
||||
1. This can be inefficient because of searching for files for the project for every event.
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
owning-stage: "~devops::verify"
|
||||
description: 'GitLab CI Events Proposal 4: Creating events via CI files'
|
||||
---
|
||||
|
||||
# GitLab CI Events Proposal 4: Creating events via CI files
|
||||
|
||||
Each project can have its own event configuration file. Let's call it `.gitlab-ci-event.yml` for now.
|
||||
In this file, we can define events in the following format:
|
||||
|
||||
```yaml
|
||||
events:
|
||||
- package/published
|
||||
- issue/created
|
||||
```
|
||||
|
||||
When this file is changed in the project repository, it is parsed and the events are created, updated, or deleted.
|
||||
This is highly similar to [Proposal 1](proposal-1-using-the-gitlab-ci-file.md) except that we don't need to
|
||||
track pipeline creations every time.
|
||||
|
||||
1. Upsert events to the database when `.gitlab-ci-event.yml` is updated.
|
||||
1. Create [EventStore subscriptions](../../../development/event_store.md) to handle the events.
|
||||
|
||||
## Filtering jobs
|
||||
|
||||
We can filter jobs by using the `rules` keyword. For example:
|
||||
|
||||
```yaml
|
||||
test_package_published:
|
||||
script: echo testing published package
|
||||
rules:
|
||||
- events: ["package/published"]
|
||||
|
||||
test_package_removed:
|
||||
script: echo testing removed package
|
||||
rules:
|
||||
- events: ["package/removed"]
|
||||
```
|
||||
|
||||
Otherwise, we can make it work either a CI variable;
|
||||
|
||||
```yaml
|
||||
test_package_published:
|
||||
script: echo testing published package
|
||||
rules:
|
||||
- if: $CI_EVENT == "package/published"
|
||||
|
||||
test_package_removed:
|
||||
script: echo testing removed package
|
||||
rules:
|
||||
- if: $CI_EVENT == "package/removed"
|
||||
```
|
||||
|
||||
or an input like in the [Proposal 3](proposal-3-using-the-gitlab-ci-events-folder.md);
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
inputs:
|
||||
event:
|
||||
default: push
|
||||
|
||||
---
|
||||
|
||||
test_package_published:
|
||||
script: echo testing published package
|
||||
rules:
|
||||
- if: $[[ inputs.event ]] == "package/published"
|
||||
|
||||
test_package_removed:
|
||||
script: echo testing removed package
|
||||
rules:
|
||||
- if: $[[ inputs.event ]] == "package/removed"
|
||||
```
|
||||
|
|
@ -55,6 +55,8 @@ All AI features are experimental.
|
|||
```ruby
|
||||
Feature.enable(:ai_related_settings)
|
||||
Feature.enable(:openai_experimentation)
|
||||
Feature.enable(:tofa_experimentation_main_flag)
|
||||
Feature.enable(:anthropic_experimentation)
|
||||
```
|
||||
|
||||
1. Simulate the GDK to [simulate SaaS](ee_features.md#simulate-a-saas-instance) and ensure the group you want to test has an Ultimate license
|
||||
|
|
@ -87,31 +89,49 @@ To populate the embedding database for GitLab chat:
|
|||
1. Open a rails console
|
||||
1. Run [this script](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/10588#note_1373586079) to populate the embedding database
|
||||
|
||||
### Internal-Only GCP account access
|
||||
### Configure GCP Vertex access
|
||||
|
||||
In order to obtain a GCP service key for local development, please follow the steps below:
|
||||
|
||||
- Create a sandbox GCP environment by visiting [this page](https://about.gitlab.com/handbook/infrastructure-standards/#individual-environment) and following the instructions
|
||||
- In the GCP console, go to `IAM & Admin` > `Service Accounts` and click on the "Create new service account" button
|
||||
- Name the service account something specific to what you're using it for. Select Create and Continue. Under `Grant this service account access to project`, select the role `Vertex AI User`. Select `Continue` then `Done`
|
||||
- Select your new service account and `Manage keys` > `Add Key` > `Create new key`. This will download the **private** JSON credentials for your service account.
|
||||
- In the rails console, you will use this by `Gitlab::CurrentSettings.update(tofa_credentials: File.read('/YOUR_FILE.json'))`
|
||||
- Select your new service account and `Manage keys` > `Add Key` > `Create new key`. This will download the **private** JSON credentials for your service account. Your full settings should then be:
|
||||
|
||||
```ruby
|
||||
Gitlab::CurrentSettings.update(tofa_credentials: File.read('/YOUR_FILE.json'))
|
||||
|
||||
# Note: These credential examples will not work locally for all models
|
||||
Gitlab::CurrentSettings.update(tofa_host: "<root-domain>") # Example: us-central1-aiplatform.googleapis.com
|
||||
Gitlab::CurrentSettings.update(tofa_url: "<full-api-endpoint>") # Example: https://ROOT-DOMAIN/v1/projects/MY-COOL-PROJECT/locations/us-central1/publishers/google/models/MY-SPECIAL-MODEL:predict
|
||||
```
|
||||
|
||||
Internal team members can [use this snippet](https://gitlab.com/gitlab-com/gl-infra/production/-/snippets/2541742) for help configuring these endpoints.
|
||||
|
||||
### Configure OpenAI access
|
||||
|
||||
```ruby
|
||||
Gitlab::CurrentSettings.update(openai_api_key: "<open-ai-key>")
|
||||
```
|
||||
|
||||
### Configure Anthropic access
|
||||
|
||||
```ruby
|
||||
Feature.enable(:anthropic_experimentation)
|
||||
Gitlab::CurrentSettings.update!(anthropic_api_key: <insert API key>)
|
||||
```
|
||||
|
||||
## Experimental REST API
|
||||
|
||||
Use the [experimental REST API endpoints](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/experimentation/open_ai.rb) to quickly experiment and prototype AI features.
|
||||
Use the [experimental REST API endpoints](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/experimentation) to quickly experiment and prototype AI features.
|
||||
|
||||
The endpoints are:
|
||||
|
||||
- `https://gitlab.example.com/api/v4/ai/experimentation/openai/completions`
|
||||
- `https://gitlab.example.com/api/v4/ai/experimentation/openai/embeddings`
|
||||
- `https://gitlab.example.com/api/v4/ai/experimentation/openai/chat/completions`
|
||||
|
||||
To use these endpoints locally, set the OpenAI API key in the application settings:
|
||||
|
||||
```ruby
|
||||
Gitlab::CurrentSettings.update(openai_api_key: "<open-ai-key>")
|
||||
```
|
||||
- `https://gitlab.example.com/api/v4/ai/experimentation/anthropic/complete`
|
||||
- `https://gitlab.example.com/api/v4/ai/experimentation/tofa/chat`
|
||||
|
||||
These endpoints are only for prototyping, not for rolling features out to customers.
|
||||
The experimental endpoint is only available to GitLab team members on production. Use the
|
||||
|
|
|
|||
|
|
@ -217,6 +217,15 @@ In GitLab 16.0 and later, the GraphQL query for runners will no longer return th
|
|||
- `PAUSED` has been replaced with the field, `paused: true`.
|
||||
- `ACTIVE` has been replaced with the field, `paused: false`.
|
||||
|
||||
### Jira DVCS connector for Jira Cloud and Jira 8.13 and earlier
|
||||
|
||||
WARNING:
|
||||
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||
Review the details carefully before upgrading.
|
||||
|
||||
The [Jira DVCS connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) for Jira Cloud was deprecated in GitLab 15.1 and has been removed in 16.0. Use the [GitLab for Jira Cloud app](https://docs.gitlab.com/ee/integration/jira/connect-app.html) instead. The Jira DVCS connector was also deprecated for Jira 8.13 and earlier. You can only use the Jira DVCS connector with Jira Data Center or Jira Server in Jira 8.14 and later. Upgrade your Jira instance to Jira 8.14 or later, and reconfigure the Jira integration in your GitLab instance.
|
||||
If you cannot upgrade your Jira instance in time and are on GitLab self-managed version, we offer a workaround until GitLab 16.6. This breaking change is deployed in GitLab 16.0 behind a feature flag named `jira_dvcs_end_of_life_amnesty`. The flag is disabled by default, but you can ask an administrator to enable the flag at any time. For questions related to this announcement, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/408185).
|
||||
|
||||
### License-Check and the Policies tab on the License Compliance page
|
||||
|
||||
WARNING:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10 with a [flag](../../../administration/feature_flags.md) named `rate_limit_for_unauthenticated_projects_api_access`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/391922) on May 08, 2023.
|
||||
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119603) in GitLab 16.0 by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120445) in GitLab 16.0. Feature flag `rate_limit_for_unauthenticated_projects_api_access` removed.
|
||||
|
||||
You can configure the rate limit per IP address for unauthenticated requests to the [list all projects API](../../../api/projects.md#list-all-projects).
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ Payload example:
|
|||
"human_time_estimate": null,
|
||||
"human_time_change": null,
|
||||
"weight": null,
|
||||
"health_status": "at_risk",
|
||||
"iid": 23,
|
||||
"url": "http://example.com/diaspora/issues/23",
|
||||
"state": "opened",
|
||||
|
|
|
|||
|
|
@ -112,8 +112,6 @@ module API
|
|||
end
|
||||
|
||||
def validate_projects_api_rate_limit_for_unauthenticated_users!
|
||||
return unless Feature.enabled?(:rate_limit_for_unauthenticated_projects_api_access)
|
||||
|
||||
check_rate_limit!(:projects_api_rate_limit_unauthenticated, scope: [ip_address]) if current_user.blank?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18974,6 +18974,9 @@ msgstr ""
|
|||
msgid "Forbidden"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forecast horizon must be %{max_horizon} days at the most."
|
||||
msgstr ""
|
||||
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24210,6 +24213,9 @@ msgstr ""
|
|||
msgid "Invalid URL: %{url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid context. Project is expected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid date"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47888,6 +47894,9 @@ msgstr ""
|
|||
msgid "Unsubscribes from this %{quick_action_target}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unsupported forecast type. Supported types: %{types}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unsupported sort value."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ RSpec.describe GraphqlController, feature_category: :integrations do
|
|||
])
|
||||
end
|
||||
|
||||
it 'does not allow string as _json parameter' do
|
||||
it 'does not allow string as _json parameter (a malformed multiplex query)' do
|
||||
post :execute, params: { _json: 'bad' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
|
||||
import { createStore } from '~/boards/stores';
|
||||
|
|
@ -32,6 +32,7 @@ const TEST_ISSUE_B = {
|
|||
describe('BoardSidebarTitle', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let storeDispatch;
|
||||
let mockApollo;
|
||||
|
||||
const issueSetTitleMutationHandlerSuccess = jest.fn().mockResolvedValue(updateIssueTitleResponse);
|
||||
|
|
@ -52,8 +53,9 @@ describe('BoardSidebarTitle', () => {
|
|||
[issueSetTitleMutation, issueSetTitleMutationHandlerSuccess],
|
||||
[updateEpicTitleMutation, updateEpicTitleMutationHandlerSuccess],
|
||||
]);
|
||||
storeDispatch = jest.spyOn(store, 'dispatch');
|
||||
|
||||
wrapper = shallowMount(BoardSidebarTitle, {
|
||||
wrapper = shallowMountExtended(BoardSidebarTitle, {
|
||||
store,
|
||||
apolloProvider: mockApollo,
|
||||
provide: {
|
||||
|
|
@ -78,9 +80,9 @@ describe('BoardSidebarTitle', () => {
|
|||
const findFormInput = () => wrapper.findComponent(GlFormInput);
|
||||
const findGlLink = () => wrapper.findComponent(GlLink);
|
||||
const findEditableItem = () => wrapper.findComponent(BoardEditableItem);
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
|
||||
const findTitle = () => wrapper.find('[data-testid="item-title"]');
|
||||
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
||||
const findTitle = () => wrapper.findByTestId('item-title');
|
||||
const findCollapsed = () => wrapper.findByTestId('collapsed-content');
|
||||
|
||||
it('renders title and reference', () => {
|
||||
createWrapper();
|
||||
|
|
@ -105,9 +107,6 @@ describe('BoardSidebarTitle', () => {
|
|||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
|
||||
store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
|
||||
});
|
||||
findFormInput().vm.$emit('input', TEST_TITLE);
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await nextTick();
|
||||
|
|
@ -116,29 +115,34 @@ describe('BoardSidebarTitle', () => {
|
|||
it('collapses sidebar and renders new title', async () => {
|
||||
await waitForPromises();
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findTitle().text()).toContain(TEST_TITLE);
|
||||
});
|
||||
|
||||
it('commits change to the server', () => {
|
||||
expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({
|
||||
title: TEST_TITLE,
|
||||
expect(storeDispatch).toHaveBeenCalledWith('setActiveItemTitle', {
|
||||
projectPath: 'h/b',
|
||||
title: 'New item title',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correct title', async () => {
|
||||
createWrapper({ item: { ...TEST_ISSUE_A, title: TEST_TITLE } });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTitle().text()).toContain(TEST_TITLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting and invalid title', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
|
||||
findFormInput().vm.$emit('input', '');
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('commits change to the server', () => {
|
||||
expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
|
||||
expect(storeDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -169,7 +173,7 @@ describe('BoardSidebarTitle', () => {
|
|||
});
|
||||
|
||||
it('sets title, expands item and shows alert', () => {
|
||||
expect(wrapper.vm.title).toBe(TEST_TITLE);
|
||||
expect(findFormInput().attributes('value')).toBe(TEST_TITLE);
|
||||
expect(findCollapsed().isVisible()).toBe(false);
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
});
|
||||
|
|
@ -179,16 +183,13 @@ describe('BoardSidebarTitle', () => {
|
|||
beforeEach(async () => {
|
||||
createWrapper({ item: TEST_ISSUE_B });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
|
||||
store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
|
||||
});
|
||||
findFormInput().vm.$emit('input', TEST_TITLE);
|
||||
findCancelButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and render former title', () => {
|
||||
expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
|
||||
expect(storeDispatch).not.toHaveBeenCalled();
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
|
||||
});
|
||||
|
|
@ -198,10 +199,6 @@ describe('BoardSidebarTitle', () => {
|
|||
beforeEach(async () => {
|
||||
createWrapper({ item: TEST_ISSUE_B });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
|
||||
throw new Error(['failed mutation']);
|
||||
});
|
||||
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
|
||||
findFormInput().vm.$emit('input', 'Invalid title');
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await nextTick();
|
||||
|
|
@ -210,7 +207,10 @@ describe('BoardSidebarTitle', () => {
|
|||
it('collapses sidebar and renders former item title', () => {
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
|
||||
expect(wrapper.vm.setError).toHaveBeenCalled();
|
||||
expect(storeDispatch).toHaveBeenCalledWith(
|
||||
'setError',
|
||||
expect.objectContaining({ message: 'An error occurred when updating the title' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import Panel from '~/google_cloud/aiml/panel.vue';
|
||||
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
|
||||
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
|
||||
import ServiceTable from '~/google_cloud/aiml/service_table.vue';
|
||||
|
||||
describe('google_cloud/databases/panel', () => {
|
||||
let wrapper;
|
||||
|
||||
const props = {
|
||||
configurationUrl: 'configuration-url',
|
||||
deploymentsUrl: 'deployments-url',
|
||||
databasesUrl: 'databases-url',
|
||||
aimlUrl: 'aiml-url',
|
||||
visionAiUrl: 'vision-ai-url',
|
||||
translationAiUrl: 'translation-ai-url',
|
||||
languageAiUrl: 'language-ai-url',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMountExtended(Panel, { propsData: props });
|
||||
});
|
||||
|
||||
it('contains incubation banner', () => {
|
||||
const target = wrapper.findComponent(IncubationBanner);
|
||||
expect(target.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains google cloud menu with `aiml` active', () => {
|
||||
const target = wrapper.findComponent(GoogleCloudMenu);
|
||||
expect(target.exists()).toBe(true);
|
||||
expect(target.props('active')).toBe('aiml');
|
||||
expect(target.props('configurationUrl')).toBe(props.configurationUrl);
|
||||
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
|
||||
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
|
||||
expect(target.props('aimlUrl')).toBe(props.aimlUrl);
|
||||
});
|
||||
|
||||
it('contains service table', () => {
|
||||
const target = wrapper.findComponent(ServiceTable);
|
||||
expect(target.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ describe('google_cloud/components/google_cloud_menu', () => {
|
|||
configurationUrl: 'configuration-url',
|
||||
deploymentsUrl: 'deployments-url',
|
||||
databasesUrl: 'databases-url',
|
||||
aimlUrl: 'aiml-url',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -33,4 +34,10 @@ describe('google_cloud/components/google_cloud_menu', () => {
|
|||
expect(link.text()).toBe(GoogleCloudMenu.i18n.databases.title);
|
||||
expect(link.attributes('href')).toBe(props.databasesUrl);
|
||||
});
|
||||
|
||||
it('contains ai/ml link', () => {
|
||||
const link = wrapper.findByTestId('aimlLink');
|
||||
expect(link.text()).toBe(GoogleCloudMenu.i18n.aiml.title);
|
||||
expect(link.attributes('href')).toBe(props.aimlUrl);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import { isSticky } from '~/lib/utils/sticky';
|
||||
|
||||
const TEST_OFFSET_TOP = 500;
|
||||
|
||||
describe('sticky', () => {
|
||||
let el;
|
||||
let offsetTop;
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(
|
||||
`
|
||||
<div class="parent">
|
||||
<div id="js-sticky"></div>
|
||||
</div>
|
||||
`,
|
||||
);
|
||||
|
||||
offsetTop = TEST_OFFSET_TOP;
|
||||
el = document.getElementById('js-sticky');
|
||||
Object.defineProperty(el, 'offsetTop', {
|
||||
get() {
|
||||
return offsetTop;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
el = null;
|
||||
});
|
||||
|
||||
describe('when stuck', () => {
|
||||
it('does not remove is-stuck class', () => {
|
||||
isSticky(el, 0, el.offsetTop);
|
||||
isSticky(el, 0, el.offsetTop);
|
||||
|
||||
expect(el.classList.contains('is-stuck')).toBe(true);
|
||||
});
|
||||
|
||||
it('adds is-stuck class', () => {
|
||||
isSticky(el, 0, el.offsetTop);
|
||||
|
||||
expect(el.classList.contains('is-stuck')).toBe(true);
|
||||
});
|
||||
|
||||
it('inserts placeholder element', () => {
|
||||
isSticky(el, 0, el.offsetTop, true);
|
||||
|
||||
expect(document.querySelector('.sticky-placeholder')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not stuck', () => {
|
||||
it('removes is-stuck class', () => {
|
||||
jest.spyOn(el.classList, 'remove');
|
||||
|
||||
isSticky(el, 0, el.offsetTop);
|
||||
isSticky(el, 0, 0);
|
||||
|
||||
expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
|
||||
expect(el.classList.contains('is-stuck')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not add is-stuck class', () => {
|
||||
isSticky(el, 0, 0);
|
||||
|
||||
expect(el.classList.contains('is-stuck')).toBe(false);
|
||||
});
|
||||
|
||||
it('removes placeholder', () => {
|
||||
isSticky(el, 0, el.offsetTop, true);
|
||||
isSticky(el, 0, 0, true);
|
||||
|
||||
expect(document.querySelector('.sticky-placeholder')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { GlAvatar } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import DiffsModule from '~/diffs/store/modules';
|
||||
import NoteActions from '~/notes/components/note_actions.vue';
|
||||
|
|
@ -37,7 +37,9 @@ describe('issue_note', () => {
|
|||
|
||||
const REPORT_ABUSE_PATH = '/abuse_reports/add_category';
|
||||
|
||||
const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
|
||||
const findNoteBody = () => wrapper.findComponent(NoteBody);
|
||||
|
||||
const findMultilineComment = () => wrapper.findByTestId('multiline-comment');
|
||||
|
||||
const createWrapper = (props = {}, storeUpdater = (s) => s) => {
|
||||
store = new Vuex.Store(
|
||||
|
|
@ -52,7 +54,7 @@ describe('issue_note', () => {
|
|||
store.dispatch('setNoteableData', noteableDataMock);
|
||||
store.dispatch('setNotesData', notesDataMock);
|
||||
|
||||
wrapper = mount(issueNote, {
|
||||
wrapper = mountExtended(issueNote, {
|
||||
store,
|
||||
propsData: {
|
||||
note,
|
||||
|
|
@ -250,21 +252,17 @@ describe('issue_note', () => {
|
|||
});
|
||||
|
||||
it('should render issue body', () => {
|
||||
const noteBody = wrapper.findComponent(NoteBody);
|
||||
const noteBodyProps = noteBody.props();
|
||||
|
||||
expect(noteBodyProps.note).toBe(note);
|
||||
expect(noteBodyProps.line).toBe(null);
|
||||
expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit);
|
||||
expect(noteBodyProps.isEditing).toBe(false);
|
||||
expect(noteBodyProps.helpPagePath).toBe('');
|
||||
expect(findNoteBody().props().note).toBe(note);
|
||||
expect(findNoteBody().props().line).toBe(null);
|
||||
expect(findNoteBody().props().canEdit).toBe(note.current_user.can_edit);
|
||||
expect(findNoteBody().props().isEditing).toBe(false);
|
||||
expect(findNoteBody().props().helpPagePath).toBe('');
|
||||
});
|
||||
|
||||
it('prevents note preview xss', async () => {
|
||||
const noteBody =
|
||||
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert(1)" />';
|
||||
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
const noteBodyComponent = wrapper.findComponent(NoteBody);
|
||||
|
||||
store.hotUpdate({
|
||||
modules: {
|
||||
|
|
@ -277,7 +275,7 @@ describe('issue_note', () => {
|
|||
},
|
||||
});
|
||||
|
||||
noteBodyComponent.vm.$emit('handleFormUpdate', {
|
||||
findNoteBody().vm.$emit('handleFormUpdate', {
|
||||
noteText: noteBody,
|
||||
parentElement: null,
|
||||
callback: () => {},
|
||||
|
|
@ -285,7 +283,7 @@ describe('issue_note', () => {
|
|||
|
||||
await waitForPromises();
|
||||
expect(alertSpy).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.note.note_html).toBe(
|
||||
expect(findNoteBody().props().note.note_html).toBe(
|
||||
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">',
|
||||
);
|
||||
});
|
||||
|
|
@ -321,26 +319,21 @@ describe('issue_note', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
const noteBody = wrapper.findComponent(NoteBody);
|
||||
noteBody.vm.resetAutoSave = () => {};
|
||||
|
||||
noteBody.vm.$emit('handleFormUpdate', {
|
||||
findNoteBody().vm.$emit('handleFormUpdate', {
|
||||
noteText: updatedText,
|
||||
parentElement: null,
|
||||
callback: () => {},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
let noteBodyProps = noteBody.props();
|
||||
|
||||
expect(noteBodyProps.note.note_html).toBe(`<p dir="auto">${updatedText}</p>\n`);
|
||||
expect(findNoteBody().props().note.note_html).toBe(`<p dir="auto">${updatedText}</p>\n`);
|
||||
|
||||
noteBody.vm.$emit('cancelForm', {});
|
||||
findNoteBody().vm.$emit('cancelForm', {});
|
||||
await nextTick();
|
||||
|
||||
noteBodyProps = noteBody.props();
|
||||
|
||||
expect(noteBodyProps.note.note_html).toBe(note.note_html);
|
||||
expect(findNoteBody().props().note.note_html).toBe(note.note_html);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -371,7 +364,7 @@ describe('issue_note', () => {
|
|||
it('responds to handleFormUpdate', () => {
|
||||
createWrapper();
|
||||
updateActions();
|
||||
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
|
||||
findNoteBody().vm.$emit('handleFormUpdate', params);
|
||||
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -380,16 +373,14 @@ describe('issue_note', () => {
|
|||
|
||||
createWrapper();
|
||||
updateActions();
|
||||
wrapper
|
||||
.findComponent(NoteBody)
|
||||
.vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
|
||||
findNoteBody().vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
|
||||
expect(updateNote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not stringify empty position', () => {
|
||||
createWrapper();
|
||||
updateActions();
|
||||
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
|
||||
findNoteBody().vm.$emit('handleFormUpdate', params);
|
||||
expect(updateNote.mock.calls[0][1].note.note.position).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -398,7 +389,7 @@ describe('issue_note', () => {
|
|||
const expectation = JSON.stringify(position);
|
||||
createWrapper({ note: { ...note, position } });
|
||||
updateActions();
|
||||
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
|
||||
findNoteBody().vm.$emit('handleFormUpdate', params);
|
||||
expect(updateNote.mock.calls[0][1].note.note.position).toBe(expectation);
|
||||
});
|
||||
});
|
||||
|
|
@ -423,7 +414,7 @@ describe('issue_note', () => {
|
|||
|
||||
createWrapper({ note: noteDef, discussionFile: null }, storeUpdater);
|
||||
|
||||
expect(wrapper.vm.diffFile).toBe(null);
|
||||
expect(findNoteBody().props().file).toBe(null);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -441,7 +432,7 @@ describe('issue_note', () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(wrapper.vm.diffFile.testId).toBe('diffFileTest');
|
||||
expect(findNoteBody().props().file.testId).toBe('diffFileTest');
|
||||
});
|
||||
|
||||
it('returns the provided diff file if the more robust getters fail', () => {
|
||||
|
|
@ -457,7 +448,7 @@ describe('issue_note', () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(wrapper.vm.diffFile.testId).toBe('diffFileTest');
|
||||
expect(findNoteBody().props().file.testId).toBe('diffFileTest');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ describe('Diff Stats Dropdown', () => {
|
|||
const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
|
||||
const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
|
||||
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
|
||||
const findExpanded = () => wrapper.findByTestId('diff-stats-additions-deletions-collapsed');
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
|
||||
describe('file item', () => {
|
||||
|
|
@ -88,24 +87,17 @@ describe('Diff Stats Dropdown', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedExpanded | expectedAddedDeletedCollapsed
|
||||
${0} | ${0} | ${0} | ${'0 changed files'} | ${'+0 -0'} | ${'with 0 additions and 0 deletions'}
|
||||
${2} | ${0} | ${2} | ${'2 changed files'} | ${'+0 -2'} | ${'with 0 additions and 2 deletions'}
|
||||
${2} | ${2} | ${0} | ${'2 changed files'} | ${'+2 -0'} | ${'with 2 additions and 0 deletions'}
|
||||
${2} | ${1} | ${1} | ${'2 changed files'} | ${'+1 -1'} | ${'with 1 addition and 1 deletion'}
|
||||
${1} | ${0} | ${1} | ${'1 changed file'} | ${'+0 -1'} | ${'with 0 additions and 1 deletion'}
|
||||
${1} | ${1} | ${0} | ${'1 changed file'} | ${'+1 -0'} | ${'with 1 addition and 0 deletions'}
|
||||
${4} | ${2} | ${2} | ${'4 changed files'} | ${'+2 -2'} | ${'with 2 additions and 2 deletions'}
|
||||
changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedCollapsed
|
||||
${0} | ${0} | ${0} | ${'0 changed files'} | ${'with 0 additions and 0 deletions'}
|
||||
${2} | ${0} | ${2} | ${'2 changed files'} | ${'with 0 additions and 2 deletions'}
|
||||
${2} | ${2} | ${0} | ${'2 changed files'} | ${'with 2 additions and 0 deletions'}
|
||||
${2} | ${1} | ${1} | ${'2 changed files'} | ${'with 1 addition and 1 deletion'}
|
||||
${1} | ${0} | ${1} | ${'1 changed file'} | ${'with 0 additions and 1 deletion'}
|
||||
${1} | ${1} | ${0} | ${'1 changed file'} | ${'with 1 addition and 0 deletions'}
|
||||
${4} | ${2} | ${2} | ${'4 changed files'} | ${'with 2 additions and 2 deletions'}
|
||||
`(
|
||||
'when there are $changed changed file(s), $added added and $deleted deleted file(s)',
|
||||
({
|
||||
changed,
|
||||
added,
|
||||
deleted,
|
||||
expectedDropdownHeader,
|
||||
expectedAddedDeletedExpanded,
|
||||
expectedAddedDeletedCollapsed,
|
||||
}) => {
|
||||
({ changed, added, deleted, expectedDropdownHeader, expectedAddedDeletedCollapsed }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ changed, added, deleted });
|
||||
});
|
||||
|
|
@ -114,10 +106,6 @@ describe('Diff Stats Dropdown', () => {
|
|||
expect(findChanged().props('text')).toBe(expectedDropdownHeader);
|
||||
});
|
||||
|
||||
it(`added and deleted count in expanded section should be '${expectedAddedDeletedExpanded}'`, () => {
|
||||
expect(findExpanded().text()).toBe(expectedAddedDeletedExpanded);
|
||||
});
|
||||
|
||||
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
|
||||
expect(findCollapsed().text()).toBe(expectedAddedDeletedCollapsed);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
|
||||
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
|
||||
import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
||||
jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}1`);
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ describe('ProjectsListItem', () => {
|
|||
const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks });
|
||||
const findProjectTopics = () => wrapper.findByTestId('project-topics');
|
||||
const findPopover = () => findProjectTopics().findComponent(GlPopover);
|
||||
const findProjectDescription = () => wrapper.findByTestId('project-description');
|
||||
|
||||
it('renders project avatar', () => {
|
||||
createComponent();
|
||||
|
|
@ -105,6 +107,12 @@ describe('ProjectsListItem', () => {
|
|||
expect(starsLink.findComponent(GlIcon).props('name')).toBe('star-o');
|
||||
});
|
||||
|
||||
it('renders updated at', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(project.updatedAt);
|
||||
});
|
||||
|
||||
describe('when issues are enabled', () => {
|
||||
it('renders issues count', () => {
|
||||
createComponent();
|
||||
|
|
@ -230,4 +238,29 @@ describe('ProjectsListItem', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when project has a description', () => {
|
||||
it('renders description', () => {
|
||||
const descriptionHtml = '<p>Foo bar</p>';
|
||||
|
||||
createComponent({
|
||||
propsData: {
|
||||
project: {
|
||||
...project,
|
||||
descriptionHtml,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findProjectDescription().element.innerHTML).toBe(descriptionHtml);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when project does not have a description', () => {
|
||||
it('does not render description', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findProjectDescription().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1181,30 +1181,6 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects d
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is disabled' do
|
||||
before do
|
||||
stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
|
||||
end
|
||||
|
||||
context 'when the user is not signed in' do
|
||||
let_it_be(:current_user) { nil }
|
||||
|
||||
it_behaves_like 'does not log request and does not block the request' do
|
||||
def request
|
||||
get api(path, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is signed in' do
|
||||
it_behaves_like 'does not log request and does not block the request' do
|
||||
def request
|
||||
get api(path, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ JS_CONSOLE_FILTER = Regexp.union(
|
|||
|
||||
CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
|
||||
|
||||
SCREENSHOT_FILENAME_LENGTH = ENV['CI'] || ENV['CI_SERVER'] ? 255 : 99
|
||||
SCREENSHOT_FILENAME_LENGTH = ENV['CI'] || ENV['CI_SERVER'] ? 150 : 99
|
||||
|
||||
@blackhole_tcp_server = nil
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,17 @@ module KeysetPaginationHelpers
|
|||
|
||||
link.split(',').filter_map do |link|
|
||||
match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
|
||||
break nil unless match
|
||||
next unless match
|
||||
|
||||
{ url: match[:url], rel: match[:rel] }
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_params_from_next_url(response)
|
||||
next_url = pagination_links(response).find { |link| link[:rel] == 'next' }[:url]
|
||||
next_link = pagination_links(response).find { |link| link[:rel] == 'next' }
|
||||
next_url = next_link&.fetch(:url)
|
||||
return unless next_url
|
||||
|
||||
Rack::Utils.parse_query(URI.parse(next_url).query)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe KeysetPaginationHelpers, feature_category: :api do
|
||||
include described_class
|
||||
|
||||
let(:headers) { { 'LINK' => %(<#{url}>; rel="#{rel}") } }
|
||||
let(:response) { instance_double('HTTParty::Response', headers: headers) }
|
||||
let(:rel) { 'next' }
|
||||
let(:url) do
|
||||
'http://127.0.0.1:3000/api/v4/projects/7/audit_eve' \
|
||||
'nts?cursor=eyJpZCI6IjYyMjAiLCJfa2QiOiJuIn0%3D&id=7&o' \
|
||||
'rder_by=id&page=1&pagination=keyset&per_page=2'
|
||||
end
|
||||
|
||||
describe '#pagination_links' do
|
||||
subject { pagination_links(response) }
|
||||
|
||||
let(:expected_result) { [{ url: url, rel: rel }] }
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
|
||||
context 'with a partially malformed LINK header' do
|
||||
# malformed as the regxe is expecting the url to be surrounded by `<>`
|
||||
let(:headers) do
|
||||
{ 'LINK' => %(<#{url}>; rel="next", GARBAGE, #{url}; rel="prev") }
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
|
||||
context 'with a malformed LINK header' do
|
||||
# malformed as the regxe is expecting the url to be surrounded by `<>`
|
||||
let(:headers) { { 'LINK' => %(rel="next", GARBAGE, #{url}; rel="prev") } }
|
||||
let(:expected_result) { [] }
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pagination_params_from_next_url' do
|
||||
subject { pagination_params_from_next_url(response) }
|
||||
|
||||
let(:expected_result) do
|
||||
{
|
||||
'cursor' => 'eyJpZCI6IjYyMjAiLCJfa2QiOiJuIn0=',
|
||||
'id' => '7',
|
||||
'order_by' => 'id',
|
||||
'page' => '1',
|
||||
'pagination' => 'keyset',
|
||||
'per_page' => '2'
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
|
||||
context 'with both prev and next rel links' do
|
||||
let(:prev_url) do
|
||||
'http://127.0.0.1:3000/api/v4/projects/7/audit_eve' \
|
||||
'nts?cursor=foocursor&id=8&o' \
|
||||
'rder_by=id&page=0&pagination=keyset&per_page=2'
|
||||
end
|
||||
|
||||
let(:headers) do
|
||||
{ 'LINK' => %(<#{url}>; rel="next", <#{prev_url}>; rel="prev") }
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
|
||||
context 'with a partially malformed LINK header' do
|
||||
# malformed as the regxe is expecting the url to be surrounded by `<>`
|
||||
let(:headers) do
|
||||
{ 'LINK' => %(<#{url}>; rel="next", GARBAGE, #{url}; rel="prev") }
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
|
||||
context 'with a malformed LINK header' do
|
||||
# malformed as the regxe is expecting the url to be surrounded by `<>`
|
||||
let(:headers) { { 'LINK' => %(rel="next", GARBAGE, #{url}; rel="prev") } }
|
||||
|
||||
it { is_expected.to be nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -17,17 +17,5 @@ RSpec.describe 'admin/application_settings/network.html.haml', feature_category:
|
|||
|
||||
expect(rendered).to have_field('application_setting_projects_api_rate_limit_unauthenticated')
|
||||
end
|
||||
|
||||
context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is turned off' do
|
||||
before do
|
||||
stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
|
||||
end
|
||||
|
||||
it 'does not render the `projects_api_rate_limit_unauthenticated` field' do
|
||||
render
|
||||
|
||||
expect(rendered).not_to have_field('application_setting_projects_api_rate_limit_unauthenticated')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue