Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-25 18:08:15 +00:00
parent 4dc41ac252
commit 28b119a4b4
144 changed files with 2009 additions and 1278 deletions

View file

@ -102,7 +102,6 @@ Layout/LineLength:
- 'app/controllers/projects/pipelines_controller.rb'
- 'app/controllers/projects/prometheus/metrics_controller.rb'
- 'app/controllers/projects/raw_controller.rb'
- 'app/controllers/projects/services_controller.rb'
- 'app/controllers/projects/settings/ci_cd_controller.rb'
- 'app/controllers/projects/settings/operations_controller.rb'
- 'app/controllers/projects/settings/repository_controller.rb'
@ -4203,7 +4202,6 @@ Layout/LineLength:
- 'spec/controllers/projects/serverless/functions_controller_spec.rb'
- 'spec/controllers/projects/service_desk_controller_spec.rb'
- 'spec/controllers/projects/service_ping_controller_spec.rb'
- 'spec/controllers/projects/services_controller_spec.rb'
- 'spec/controllers/projects/settings/ci_cd_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/projects/settings/repository_controller_spec.rb'

View file

@ -36,7 +36,6 @@ Rails/LexicallyScopedActionFilter:
- 'app/controllers/projects/project_members_controller.rb'
- 'app/controllers/projects/prometheus/alerts_controller.rb'
- 'app/controllers/projects/releases_controller.rb'
- 'app/controllers/projects/service_hook_logs_controller.rb'
- 'app/controllers/projects/snippets_controller.rb'
- 'app/controllers/projects/tags_controller.rb'
- 'app/controllers/projects/todos_controller.rb'

View file

@ -152,8 +152,8 @@ RSpec/AnyInstanceOf:
- spec/controllers/projects/labels_controller_spec.rb
- spec/controllers/projects/merge_requests_controller_spec.rb
- spec/controllers/projects/pipelines_controller_spec.rb
- spec/controllers/projects/service_hook_logs_controller_spec.rb
- spec/controllers/projects/services_controller_spec.rb
- spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb
- spec/controllers/projects/settings/integrations_controller_spec.rb
- spec/controllers/projects/tags_controller_spec.rb
- spec/controllers/registrations/experience_levels_controller_spec.rb
- spec/controllers/registrations_controller_spec.rb
@ -177,7 +177,6 @@ RSpec/AnyInstanceOf:
- spec/features/projects/jobs_spec.rb
- spec/features/projects/navbar_spec.rb
- spec/features/projects/pages_spec.rb
- spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
- spec/features/projects/settings/service_desk_setting_spec.rb
- spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
- spec/features/snippets/embedded_snippet_spec.rb

View file

@ -3855,7 +3855,6 @@ RSpec/ContextWording:
- 'spec/views/projects/hooks/edit.html.haml_spec.rb'
- 'spec/views/projects/hooks/index.html.haml_spec.rb'
- 'spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- 'spec/views/projects/services/edit.html.haml_spec.rb'
- 'spec/views/projects/settings/operations/show.html.haml_spec.rb'
- 'spec/views/projects/tags/index.html.haml_spec.rb'
- 'spec/views/projects/tree/show.html.haml_spec.rb'

View file

@ -220,9 +220,7 @@ Style/ClassAndModuleChildren:
- 'app/controllers/projects/runner_projects_controller.rb'
- 'app/controllers/projects/runners_controller.rb'
- 'app/controllers/projects/service_desk_controller.rb'
- 'app/controllers/projects/service_hook_logs_controller.rb'
- 'app/controllers/projects/service_ping_controller.rb'
- 'app/controllers/projects/services_controller.rb'
- 'app/controllers/projects/snippets/application_controller.rb'
- 'app/controllers/projects/snippets/blobs_controller.rb'
- 'app/controllers/projects/snippets_controller.rb'

View file

@ -53,7 +53,7 @@ Style/EmptyMethod:
- 'app/controllers/projects/pipeline_schedules_controller.rb'
- 'app/controllers/projects/product_analytics_controller.rb'
- 'app/controllers/projects/runners_controller.rb'
- 'app/controllers/projects/services_controller.rb'
- 'app/controllers/projects/settings/integrations_controller.rb'
- 'app/controllers/projects/settings/packages_and_registries_controller.rb'
- 'app/controllers/projects/tags/releases_controller.rb'
- 'app/controllers/projects/terraform_controller.rb'

View file

@ -41,7 +41,6 @@ Style/FormatString:
- 'app/controllers/projects/merge_requests_controller.rb'
- 'app/controllers/projects/performance_monitoring/dashboards_controller.rb'
- 'app/controllers/projects/pipeline_schedules_controller.rb'
- 'app/controllers/projects/services_controller.rb'
- 'app/controllers/projects/settings/ci_cd_controller.rb'
- 'app/controllers/projects_controller.rb'
- 'app/controllers/search_controller.rb'

View file

@ -57,7 +57,6 @@ Style/IfUnlessModifier:
- 'app/controllers/projects/protected_refs_controller.rb'
- 'app/controllers/projects/releases_controller.rb'
- 'app/controllers/projects/runners_controller.rb'
- 'app/controllers/projects/services_controller.rb'
- 'app/controllers/registrations_controller.rb'
- 'app/controllers/repositories/git_http_controller.rb'
- 'app/controllers/repositories/lfs_api_controller.rb'

View file

@ -1,6 +1,5 @@
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import initUserPopovers from '../../user_popovers';
import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
@ -22,7 +21,6 @@ $.fn.renderGFM = function renderGFM() {
renderMermaid(this.find('.js-render-mermaid'));
}
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.js-user-link').get());
const issuablePopoverElements = this.find('.gfm-issue, .gfm-merge_request').get();
if (issuablePopoverElements.length) {

View file

@ -2,29 +2,9 @@
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { s__ } from '~/locale';
import SplitButton from '~/vue_shared/components/split_button.vue';
const splitButtonActionItems = [
{
title: s__('ClusterIntegration|Remove integration and resources'),
description: s__(
'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal',
),
eventName: 'remove-cluster-and-cleanup',
},
{
title: s__('ClusterIntegration|Remove integration'),
description: s__(
'ClusterIntegration|Removes cluster from project but keeps associated resources',
),
eventName: 'remove-cluster',
},
];
export default {
splitButtonActionItems,
components: {
SplitButton,
GlModal,
GlButton,
GlFormInput,
@ -79,6 +59,9 @@ export default {
canCleanupResources() {
return !this.hasManagementProject;
},
buttonCategory() {
return !this.hasManagementProject ? 'secondary' : 'primary';
},
},
methods: {
handleClickRemoveCluster(cleanup = false) {
@ -99,19 +82,20 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-justify-content-end">
<split-button
v-if="canCleanupResources"
:action-items="$options.splitButtonActionItems"
menu-class="dropdown-menu-large"
variant="danger"
@remove-cluster="handleClickRemoveCluster(false)"
@remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
/>
<div class="gl-display-flex">
<gl-button
v-else
v-if="canCleanupResources"
data-testid="remove-integration-and-resources-button"
class="gl-mr-3"
variant="danger"
@click="handleClickRemoveCluster(true)"
>
{{ s__('ClusterIntegration|Remove integration and resources') }}
</gl-button>
<gl-button
data-testid="remove-integration-button"
:category="buttonCategory"
variant="danger"
data-testid="btnRemove"
@click="handleClickRemoveCluster(false)"
>
{{ s__('ClusterIntegration|Remove integration') }}
@ -163,13 +147,7 @@ export default {
<template v-if="confirmCleanup">
<gl-button
:disabled="!canSubmit"
variant="warning"
category="primary"
@click="handleSubmit"
>{{ s__('ClusterIntegration|Remove integration') }}</gl-button
>
<gl-button
:disabled="!canSubmit"
data-testid="remove-integration-and-resources-modal-button"
variant="danger"
category="primary"
@click="handleSubmit(true)"
@ -179,6 +157,7 @@ export default {
<template v-else>
<gl-button
:disabled="!canSubmit"
data-testid="remove-integration-modal-button"
variant="danger"
category="primary"
@click="handleSubmit"

View file

@ -13,6 +13,7 @@ import Link from './link';
import ListItem from './list_item';
import OrderedList from './ordered_list';
import Paragraph from './paragraph';
import Strike from './strike';
export default Extension.create({
addGlobalAttributes() {
@ -33,6 +34,7 @@ export default Extension.create({
ListItem.name,
OrderedList.name,
Paragraph.name,
Strike.name,
],
attributes: {
sourceMarkdown: {

View file

@ -65,6 +65,7 @@ import {
italic,
link,
code,
strike,
} from './serialization_helpers';
const defaultSerializerConfig = {
@ -89,12 +90,7 @@ const defaultSerializerConfig = {
close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`,
escape: false,
},
[Strike.name]: {
open: '~~',
close: '~~',
mixable: true,
expelEnclosingWhitespace: true,
},
[Strike.name]: strike,
...HTMLMarks.reduce(
(acc, { name }) => ({
...acc,

View file

@ -66,6 +66,10 @@ const factorySpecs = {
title: hastNode.properties.title,
}),
},
strike: {
type: 'mark',
selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName),
},
};
export default () => {

View file

@ -364,7 +364,7 @@ export function preserveUnchanged(render) {
};
}
const generateBoldTags = (open = true) => {
const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
@ -375,7 +375,7 @@ const generateBoldTags = (open = true) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
case '<strong':
case '<b':
return (open ? openTag : closeTag)(type.substring(1));
return wrapTagName(type.substring(1));
default:
return '**';
}
@ -384,12 +384,12 @@ const generateBoldTags = (open = true) => {
export const bold = {
open: generateBoldTags(),
close: generateBoldTags(false),
close: generateBoldTags(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};
const generateItalicTag = (open = true) => {
const generateItalicTag = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*|_|<em|<i).*/.exec(mark.attrs.sourceMarkdown)?.[1];
@ -400,7 +400,7 @@ const generateItalicTag = (open = true) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
case '<em':
case '<i':
return (open ? openTag : closeTag)(type.substring(1));
return wrapTagName(type.substring(1));
default:
return '_';
}
@ -409,17 +409,17 @@ const generateItalicTag = (open = true) => {
export const italic = {
open: generateItalicTag(),
close: generateItalicTag(false),
close: generateItalicTag(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};
const generateCodeTag = (open = true) => {
const generateCodeTag = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1];
if (type === '<code') {
return (open ? openTag : closeTag)(type.substring(1));
return wrapTagName(type.substring(1));
}
return '`';
@ -428,7 +428,7 @@ const generateCodeTag = (open = true) => {
export const code = {
open: generateCodeTag(),
close: generateCodeTag(false),
close: generateCodeTag(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};
@ -480,3 +480,28 @@ export const link = {
return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`;
},
};
const generateStrikeTag = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(~~|<del|<strike|<s).*/.exec(mark.attrs.sourceMarkdown)?.[1];
switch (type) {
case '~~':
return type;
/* eslint-disable @gitlab/require-i18n-strings */
case '<del':
case '<strike':
case '<s':
return wrapTagName(type.substring(1));
default:
return '~~';
}
};
};
export const strike = {
open: generateStrikeTag(),
close: generateStrikeTag(closeTag),
mixable: true,
expelEnclosingWhitespace: true,
};

View file

@ -22,7 +22,7 @@ export default {
type: Boolean,
required: true,
},
editProjectServicePath: {
editIntegrationPath: {
type: String,
required: true,
},
@ -79,7 +79,7 @@ export default {
<gl-button variant="success" category="primary" :disabled="!formIsValid" @click="submit">
{{ saveButtonText }}
</gl-button>
<gl-button class="float-right" :href="editProjectServicePath">{{ __('Cancel') }}</gl-button>
<gl-button class="float-right" :href="editIntegrationPath">{{ __('Cancel') }}</gl-button>
<delete-custom-metric-modal
v-if="metricPersisted"
:delete-metric-url="customMetricsPath"

View file

@ -13,7 +13,7 @@ export default () => {
const domEl = document.querySelector(this.$options.el);
const {
customMetricsPath,
editProjectServicePath,
editIntegrationPath,
validateQueryPath,
title,
query,
@ -30,7 +30,7 @@ export default () => {
props: {
customMetricsPath,
metricPersisted,
editProjectServicePath,
editIntegrationPath,
validateQueryPath,
formData: {
title,

View file

@ -7,8 +7,6 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import initUserPopovers from '../../user_popovers';
/**
* CommitItem
*
@ -82,11 +80,6 @@ export default {
return this.commit.description_html.replace(/^&#x000A;/, '');
},
},
created() {
this.$nextTick(() => {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji'],
},

View file

@ -1,10 +1,15 @@
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
const createParser = () => {
return unified().use(remarkParse).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw);
return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw);
};
const compilerFactory = (renderer) =>

View file

@ -29,8 +29,11 @@ class UsersCache extends Cache {
}
return getUser(userId).then(({ data }) => {
this.internalStorage[userId] = data;
return data;
this.internalStorage[userId] = {
...this.get(userId),
...data,
};
return this.internalStorage[userId];
});
// missing catch is intentional, error handling depends on use case
}

View file

@ -4,7 +4,6 @@ import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
FIELD_KEY_ACTIONS,
@ -85,9 +84,6 @@ export default {
return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite;
},
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
},
methods: {
hasActionButtons(member) {
return (

View file

@ -3,7 +3,6 @@ import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import createFlash from '~/flash';
import { __ } from '~/locale';
import initUserPopovers from '~/user_popovers';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -169,7 +168,6 @@ export default {
updated() {
this.$nextTick(() => {
highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
beforeDestroy() {

View file

@ -36,12 +36,12 @@ export default {
};
</script>
<template>
<div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle gl-my-1">
<div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle">
<div
v-for="stage in stages"
:key="stage.name"
:class="stagesClass"
class="stage-container dropdown"
class="dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle stage-container"
>
<pipeline-stage
:stage="stage"

View file

@ -21,6 +21,9 @@ import eventHub from '../../event_hub';
import JobItem from './job_item.vue';
export default {
i18n: {
stage: __('Stage:'),
},
components: {
CiIcon,
GlLoadingIcon,
@ -48,20 +51,26 @@ export default {
},
data() {
return {
isDropdownOpen: false,
isLoading: false,
dropdownContent: [],
stageName: '',
};
},
watch: {
updateDropdown() {
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
if (this.updateDropdown && this.isDropdownOpen && !this.isLoading) {
this.fetchJobs();
}
},
},
methods: {
onHideDropdown() {
this.isDropdownOpen = false;
},
onShowDropdown() {
eventHub.$emit('clickedDropdown');
this.isDropdownOpen = true;
this.isLoading = true;
this.fetchJobs();
},
@ -70,6 +79,7 @@ export default {
.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.latest_statuses;
this.stageName = data.name;
this.isLoading = false;
})
.catch(() => {
@ -81,9 +91,6 @@ export default {
});
});
},
isDropdownOpen() {
return this.$el.classList.contains('show');
},
pipelineActionRequestComplete() {
// close the dropdown in MR widget
this.$refs.dropdown.hide();
@ -112,15 +119,17 @@ export default {
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:toggle-class="['gl-rounded-full!']"
menu-class="mini-pipeline-graph-dropdown-menu"
@hide="onHideDropdown"
@show="onShowDropdown"
>
<template #button-content>
<ci-icon
is-interactive
css-classes="gl-rounded-full"
:is-active="isDropdownOpen"
:size="24"
:status="stage.status"
class="gl-align-items-center gl-display-inline-flex"
class="gl-align-items-center gl-display-inline-flex gl-z-index-1"
/>
</template>
<gl-loading-icon v-if="isLoading" size="sm" />
@ -129,6 +138,12 @@ export default {
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
<div
class="gl-align-items-center gl-border-b gl-display-flex gl-font-weight-bold gl-justify-content-center gl-pb-3"
>
<span class="gl-mr-1">{{ $options.i18n.stage }}</span>
<span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
</div>
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"

View file

@ -1,6 +1,8 @@
import Vue from 'vue';
import { debounce } from 'lodash';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
import { USER_POPOVER_DELAY } from './vue_shared/components/user_popover/constants';
const removeTitle = (el) => {
// Removing titles so its not showing tooltips also
@ -59,87 +61,78 @@ const populateUserInfo = (user) => {
);
};
const initializedPopovers = new Map();
let domObservedForChanges = false;
function createPopover(el, user) {
removeTitle(el);
const preloadedUserInfo = getPreloadedUserInfo(el.dataset);
const addPopoversToModifiedTree = new MutationObserver(() => {
const userLinks = document?.querySelectorAll('.js-user-link, .gfm-project_member');
Object.assign(user, preloadedUserInfo);
if (userLinks) {
addPopovers(userLinks); /* eslint-disable-line no-use-before-define */
if (preloadedUserInfo.userId) {
populateUserInfo(user);
}
});
function observeBody() {
if (!domObservedForChanges) {
addPopoversToModifiedTree.observe(document.body, {
subtree: true,
childList: true,
});
domObservedForChanges = true;
}
}
export default function addPopovers(elements = document.querySelectorAll('.js-user-link')) {
const userLinks = Array.from(elements);
const UserPopoverComponent = Vue.extend(UserPopover);
observeBody();
return userLinks
.filter(({ dataset }) => dataset.user || dataset.userId)
.map((el) => {
if (initializedPopovers.has(el)) {
return initializedPopovers.get(el);
}
const user = {
location: null,
bio: null,
workInformation: null,
status: null,
isFollowed: false,
loaded: false,
};
const renderedPopover = new UserPopoverComponent({
propsData: {
target: el,
user,
placement: el.dataset.placement || 'top',
},
});
const { userId } = el.dataset;
renderedPopover.$on('follow', () => {
UsersCache.updateById(userId, { is_followed: true });
user.isFollowed = true;
});
renderedPopover.$on('unfollow', () => {
UsersCache.updateById(userId, { is_followed: false });
user.isFollowed = false;
});
initializedPopovers.set(el, renderedPopover);
renderedPopover.$mount();
el.addEventListener('mouseenter', ({ target }) => {
removeTitle(target);
const preloadedUserInfo = getPreloadedUserInfo(target.dataset);
Object.assign(user, preloadedUserInfo);
if (preloadedUserInfo.userId) {
populateUserInfo(user);
}
});
el.addEventListener('mouseleave', ({ target }) => {
target.removeAttribute('aria-describedby');
});
return renderedPopover;
});
return new UserPopoverComponent({
propsData: {
target: el,
user,
show: true,
placement: el.dataset.placement || 'top',
},
});
}
function launchPopover(el, mountPopover) {
if (el.user) return;
const emptyUser = {
location: null,
bio: null,
workInformation: null,
status: null,
isFollowed: false,
loaded: false,
};
el.user = emptyUser;
el.addEventListener(
'mouseleave',
({ target }) => {
target.removeAttribute('aria-describedby');
},
{ once: true },
);
const popoverInstance = createPopover(el, emptyUser);
const { userId } = el.dataset;
popoverInstance.$on('follow', () => {
UsersCache.updateById(userId, { is_followed: true });
el.user.isFollowed = true;
});
popoverInstance.$on('unfollow', () => {
UsersCache.updateById(userId, { is_followed: false });
el.user.isFollowed = false;
});
mountPopover(popoverInstance);
}
const userLinkSelector = 'a.js-user-link, a.gfm-project_member';
const getUserLinkNode = (node) => node.closest(userLinkSelector);
const lazyLaunchPopover = debounce((mountPopover, event) => {
const userLink = getUserLinkNode(event.target);
if (userLink) {
launchPopover(userLink, mountPopover);
}
}, USER_POPOVER_DELAY);
let hasAddedLazyPopovers = false;
export default function addPopovers(mountPopover = (instance) => instance.$mount()) {
if (!hasAddedLazyPopovers) {
document.addEventListener('mouseover', (event) => lazyLaunchPopover(mountPopover, event));
hasAddedLazyPopovers = true;
}
}

View file

@ -18,7 +18,6 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import initUserPopovers from '~/user_popovers';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
@ -175,7 +174,6 @@ export default {
updated() {
this.$nextTick(() => {
highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
methods: {

View file

@ -50,6 +50,11 @@ export default {
required: false,
default: false,
},
isActive: {
type: Boolean,
required: false,
default: false,
},
isInteractive: {
type: Boolean,
required: false,
@ -74,8 +79,9 @@ export default {
</script>
<template>
<span
:class="[wrapperStyleClasses, { interactive: isInteractive }]"
:class="[wrapperStyleClasses, { interactive: isInteractive, active: isActive }]"
:style="{ height: `${size}px`, width: `${size}px` }"
data-testid="ci-icon-wrapper"
>
<gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
</span>

View file

@ -0,0 +1 @@
export const USER_POPOVER_DELAY = 200;

View file

@ -14,12 +14,14 @@ import { glEmojiTag } from '~/emoji';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { USER_POPOVER_DELAY } from './constants';
const MAX_SKELETON_LINES = 4;
export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
USER_POPOVER_DELAY,
components: {
GlIcon,
GlLink,
@ -48,6 +50,11 @@ export default {
required: false,
default: 'top',
},
show: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -133,8 +140,15 @@ export default {
</script>
<template>
<!-- 200ms delay so not every mouseover triggers Popover -->
<gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport">
<!-- delay so not every mouseover triggers Popover -->
<gl-popover
:show="show"
:target="target"
:delay="$options.USER_POPOVER_DELAY"
:placement="placement"
boundary="viewport"
triggers="hover focus manual"
>
<div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
<div
class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p"

View file

@ -1,94 +1,55 @@
.ci-status-icon-success,
.ci-status-icon-passed {
svg {
fill: $green-500;
@mixin icon-styles($primary-color, $svg-color) {
svg,
.gl-icon {
fill: $primary-color;
}
&.interactive {
&:hover {
background: $green-500;
background: $primary-color;
svg {
--svg-status-bg: #{$green-100};
box-shadow: 0 0 0 1px $green-500;
.gl-icon {
--svg-status-bg: #{$svg-color};
box-shadow: 0 0 0 1px $primary-color;
}
}
&.active {
background: $primary-color;
.gl-icon {
box-shadow: 0 0 0 1px $primary-color;
}
}
}
}
.ci-status-icon-success,
.ci-status-icon-passed {
@include icon-styles($green-500, $green-100);
}
.ci-status-icon-error,
.ci-status-icon-failed {
svg {
fill: $red-500;
}
&.interactive {
&:hover {
background: $red-500;
svg {
--svg-status-bg: #{$red-100};
box-shadow: 0 0 0 1px $red-500;
}
}
}
@include icon-styles($red-500, $red-100);
}
.ci-status-icon-pending,
.ci-status-icon-waiting-for-resource,
.ci-status-icon-failed-with-warnings,
.ci-status-icon-success-with-warnings {
svg {
fill: $orange-500;
}
&.interactive {
&:hover {
background: $orange-500;
svg {
--svg-status-bg: #{$orange-100};
box-shadow: 0 0 0 1px $orange-500;
}
}
}
@include icon-styles($orange-500, $orange-100);
}
.ci-status-icon-running {
svg {
fill: $blue-500;
}
&.interactive {
&:hover {
background: $blue-500;
svg {
--svg-status-bg: #{$blue-100};
box-shadow: 0 0 0 1px $blue-500;
}
}
}
@include icon-styles($blue-500, $blue-100);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-scheduled,
.ci-status-icon-manual {
svg {
fill: $gray-900;
}
&.interactive {
&:hover {
background: $gray-900;
svg {
--svg-status-bg: #{$gray-100};
box-shadow: 0 0 0 1px $gray-900;
}
}
}
@include icon-styles($gray-900, $gray-100);
}
.ci-status-icon-notification,
@ -96,20 +57,7 @@
.ci-status-icon-created,
.ci-status-icon-skipped,
.ci-status-icon-notfound {
svg {
fill: $gray-500;
}
&.interactive {
&:hover {
background: $gray-500;
svg {
--svg-status-bg: #{$gray-100};
box-shadow: 0 0 0 1px $gray-500;
}
}
}
@include icon-styles($gray-500, $gray-100);
}
.icon-link {

View file

@ -74,11 +74,8 @@
.stage-cell {
.stage-container {
align-items: center;
display: inline-flex;
+ .stage-container {
margin-left: 4px;
&:last-child {
margin-right: 0;
}
// Hack to show a button tooltip inline
@ -94,10 +91,11 @@
&:not(:last-child) {
&::after {
content: '';
width: 4px;
border-bottom: 2px solid $gray-200;
position: absolute;
right: -4px;
border-bottom: 2px solid $gray-200;
top: 11px;
width: 4px;
}
}
}

View file

@ -20,7 +20,7 @@ class Projects::MattermostsController < Projects::ApplicationController
if result
flash[:notice] = 'This service is now configured'
redirect_to edit_project_integration_path(@project, integration)
redirect_to edit_project_settings_integration_path(@project, integration)
else
flash[:alert] = message || 'Failed to configure service'
redirect_to new_project_mattermost_path(@project)

View file

@ -67,7 +67,7 @@ module Projects
)
if @metric.persisted?
redirect_to edit_project_integration_path(project, ::Integrations::Prometheus),
redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus),
notice: _('Metric was successfully added.')
else
render 'new'
@ -78,7 +78,7 @@ module Projects
@metric = prometheus_metric
if @metric.update(metrics_params)
redirect_to edit_project_integration_path(project, ::Integrations::Prometheus),
redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus),
notice: _('Metric was successfully updated.')
else
render 'edit'
@ -94,7 +94,7 @@ module Projects
respond_to do |format|
format.html do
redirect_to edit_project_integration_path(project, ::Integrations::Prometheus), status: :see_other
redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus), status: :see_other
end
format.json do
head :ok

View file

@ -1,23 +0,0 @@
# frozen_string_literal: true
class Projects::ServiceHookLogsController < Projects::HookLogsController
extend Gitlab::Utils::Override
before_action :integration, only: [:show, :retry]
def retry
execute_hook
redirect_to edit_project_integration_path(@project, @integration)
end
private
def integration
@integration ||= @project.find_or_initialize_integration(params[:integration_id])
end
override :hook
def hook
@hook ||= integration.service_hook || not_found
end
end

View file

@ -1,122 +0,0 @@
# frozen_string_literal: true
class Projects::ServicesController < Projects::ApplicationController
include Integrations::Params
include InternalRedirect
# Authorize
before_action :authorize_admin_project!
before_action :ensure_service_enabled
before_action :integration
before_action :default_integration, only: [:edit, :update]
before_action :web_hook_logs, only: [:edit, :update]
respond_to :html
layout "project_settings"
feature_category :integrations
urgency :low, [:test]
def edit
end
def update
attributes = integration_params[:integration]
if use_inherited_settings?(attributes)
integration.inherit_from_id = default_integration.id
if saved = integration.save(context: :manual_change)
BulkUpdateIntegrationService.new(default_integration, [integration]).execute
end
else
attributes[:inherit_from_id] = nil
integration.attributes = attributes
saved = integration.save(context: :manual_change)
end
respond_to do |format|
format.html do
if saved
redirect_to redirect_path, notice: success_message
else
render 'edit'
end
end
format.json do
status = saved ? :ok : :unprocessable_entity
render json: serialize_as_json, status: status
end
end
end
def test
if integration.testable?
render json: service_test_response, status: :ok
else
render json: {}, status: :not_found
end
end
private
def redirect_path
safe_redirect_path(params[:redirect_to]).presence || edit_project_integration_path(project, integration)
end
def service_test_response
unless integration.update(integration_params[:integration])
return { error: true, message: _('Validations failed.'), service_response: integration.errors.full_messages.join(','), test_failed: false }
end
result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute
unless result[:success]
return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true }
end
result[:data].presence || {}
rescue *Gitlab::HTTP::HTTP_ERRORS => e
{ error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true }
end
def success_message
if integration.active?
s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title }
else
s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title }
end
end
def integration
@integration ||= project.find_or_initialize_integration(params[:id])
end
alias_method :service, :integration
def default_integration
@default_integration ||= Integration.default_integration(integration.type, project)
end
def web_hook_logs
return unless integration.service_hook.present?
@web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page])
end
def ensure_service_enabled
render_404 unless service
end
def serialize_as_json
integration
.as_json(only: integration.json_fields)
.merge(errors: integration.errors.as_json)
end
def use_inherited_settings?(attributes)
default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Projects
module Settings
class IntegrationHookLogsController < Projects::HookLogsController
extend Gitlab::Utils::Override
before_action :integration, only: [:show, :retry]
def retry
execute_hook
redirect_to edit_project_settings_integration_path(@project, @integration)
end
private
def integration
@integration ||= @project.find_or_initialize_integration(params[:integration_id])
end
override :hook
def hook
@hook ||= integration.service_hook || not_found
end
end
end
end

View file

@ -3,14 +3,142 @@
module Projects
module Settings
class IntegrationsController < Projects::ApplicationController
include ::Integrations::Params
include ::InternalRedirect
before_action :authorize_admin_project!
before_action :ensure_integration_enabled, only: [:edit, :update, :test]
before_action :integration, only: [:edit, :update, :test]
before_action :default_integration, only: [:edit, :update]
before_action :web_hook_logs, only: [:edit, :update]
respond_to :html
layout "project_settings"
feature_category :integrations
urgency :low, [:test]
def show
def index
@integrations = @project.find_or_initialize_integrations
end
def edit
end
def update
attributes = integration_params[:integration]
if use_inherited_settings?(attributes)
integration.inherit_from_id = default_integration.id
if saved = integration.save(context: :manual_change)
BulkUpdateIntegrationService.new(default_integration, [integration]).execute
end
else
attributes[:inherit_from_id] = nil
integration.attributes = attributes
saved = integration.save(context: :manual_change)
end
respond_to do |format|
format.html do
if saved
redirect_to redirect_path, notice: success_message
else
render 'edit'
end
end
format.json do
status = saved ? :ok : :unprocessable_entity
render json: serialize_as_json, status: status
end
end
end
def test
if integration.testable?
render json: integration_test_response, status: :ok
else
render json: {}, status: :not_found
end
end
private
def redirect_path
safe_redirect_path(params[:redirect_to]).presence ||
edit_project_settings_integration_path(project, integration)
end
def integration_test_response
unless integration.update(integration_params[:integration])
return {
error: true,
message: _('Validations failed.'),
service_response: integration.errors.full_messages.join(','),
test_failed: false
}
end
result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute
unless result[:success]
return {
error: true,
message: s_('Integrations|Connection failed. Please check your settings.'),
service_response: result[:message].to_s,
test_failed: true
}
end
result[:data].presence || {}
rescue *Gitlab::HTTP::HTTP_ERRORS => e
{
error: true,
message: s_('Integrations|Connection failed. Please check your settings.'),
service_response: e.message,
test_failed: true
}
end
def success_message
if integration.active?
format(s_('Integrations|%{integration} settings saved and active.'), integration: integration.title)
else
format(s_('Integrations|%{integration} settings saved, but not active.'), integration: integration.title)
end
end
def integration
@integration ||= project.find_or_initialize_integration(params[:id])
end
def default_integration
@default_integration ||= Integration.default_integration(integration.type, project)
end
def web_hook_logs
return unless integration.service_hook.present?
@web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page])
end
def ensure_integration_enabled
render_404 unless integration
end
def serialize_as_json
integration
.as_json(only: integration.json_fields)
.merge(errors: integration.errors.as_json)
end
def use_inherited_settings?(attributes)
default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
end
end
end
end

View file

@ -5,7 +5,7 @@ module CustomMetricsHelper
{
'custom-metrics-path' => url_for([project, metric]),
'metric-persisted' => metric.persisted?.to_s,
'edit-project-service-path' => edit_project_integration_path(project, ::Integrations::Prometheus),
'edit-integration-path' => edit_project_settings_integration_path(project, ::Integrations::Prometheus),
'validate-query-path' => validate_query_project_prometheus_metrics_path(project),
'title' => metric.title.to_s,
'query' => metric.query.to_s,

View file

@ -59,7 +59,7 @@ module EnvironmentsHelper
return {} unless project
{
'settings_path' => edit_project_integration_path(project, 'prometheus'),
'settings_path' => edit_project_settings_integration_path(project, 'prometheus'),
'clusters_path' => project_clusters_path(project),
'dashboards_endpoint' => project_performance_monitoring_dashboards_path(project, format: :json),
'default_branch' => project.default_branch,

View file

@ -58,7 +58,7 @@ module IntegrationsHelper
def scoped_integration_path(integration, project: nil, group: nil)
if project.present?
project_integration_path(project, integration)
project_settings_integration_path(project, integration)
elsif group.present?
group_settings_integration_path(group, integration)
else
@ -68,7 +68,7 @@ module IntegrationsHelper
def scoped_edit_integration_path(integration, project: nil, group: nil)
if project.present?
edit_project_integration_path(project, integration)
edit_project_settings_integration_path(project, integration)
elsif group.present?
edit_group_settings_integration_path(group, integration)
else
@ -82,7 +82,7 @@ module IntegrationsHelper
def scoped_test_integration_path(integration, project: nil, group: nil)
if project.present?
test_project_integration_path(project, integration)
test_project_settings_integration_path(project, integration)
elsif group.present?
test_group_settings_integration_path(group, integration)
else

View file

@ -4,10 +4,10 @@ class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
presents ::ServiceHook, as: :service_hook
def logs_details_path(log)
project_integration_hook_log_path(integration.project, integration, log)
project_settings_integration_hook_log_path(integration.project, integration, log)
end
def logs_retry_path(log)
retry_project_integration_hook_log_path(integration.project, integration, log)
retry_project_settings_integration_hook_log_path(integration.project, integration, log)
end
end

View file

@ -35,7 +35,7 @@
= s_("ClusterIntegration|This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts.")
- else
= s_("ClusterIntegration|This is necessary to clear existing environment-namespace associations from clusters previously managed by GitLab.")
= link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn gl-button btn-info')
= link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn gl-button btn-confirm')
.sub-section.form-group
%h4.text-danger

View file

@ -10,7 +10,7 @@
%td
%strong
- if can?(current_user, :admin_project, project)
= link_to integration.title, edit_project_integration_path(project, integration)
= link_to integration.title, edit_project_settings_integration_path(project, integration)
- else
= integration.title
%td

View file

@ -1,6 +1,6 @@
.js-jira-import-root{ data: { project_path: @project.full_path,
issues_path: project_issues_path(@project),
jira_integration_path: edit_project_integration_path(@project, :jira),
jira_integration_path: edit_project_settings_integration_path(@project, :jira),
is_jira_configured: @project.jira_integration&.configured?.to_s,
in_progress_illustration: image_path('illustrations/export-import.svg'),
project_id: @project.id,

View file

@ -9,4 +9,4 @@
and try again.
%hr
.clearfix
= link_to 'Go back', edit_project_integration_path(@project, @integration), class: 'gl-button btn btn-lg float-right'
= link_to 'Go back', edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg float-right'

View file

@ -42,5 +42,5 @@
%hr
.clearfix
.float-right
= link_to _('Cancel'), edit_project_integration_path(@project, @integration), class: 'gl-button btn btn-lg'
= link_to _('Cancel'), edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg'
= f.submit 'Install', class: 'gl-button btn btn-success btn-lg'

View file

@ -1,6 +1,6 @@
- add_to_breadcrumbs _("Settings"), edit_project_path(@project)
- add_to_breadcrumbs _("Integrations"), project_settings_integrations_path(@project)
- add_to_breadcrumbs "Prometheus", edit_project_integration_path(@project, ::Integrations::Prometheus)
- add_to_breadcrumbs "Prometheus", edit_project_settings_integration_path(@project, ::Integrations::Prometheus)
- breadcrumb_title s_('Metrics|Edit metric')
- page_title @metric.title, s_('Metrics|Edit metric')
= render 'form', project: @project, metric: @metric

View file

@ -1,6 +1,6 @@
- add_to_breadcrumbs _("Settings"), edit_project_path(@project)
- add_to_breadcrumbs _("Integrations"), project_settings_integrations_path(@project)
- add_to_breadcrumbs "Prometheus", edit_project_integration_path(@project, ::Integrations::Prometheus)
- add_to_breadcrumbs "Prometheus", edit_project_settings_integration_path(@project, ::Integrations::Prometheus)
- breadcrumb_title s_('Metrics|New metric')
- page_title s_('Metrics|New metric')
= render 'form', project: @project, metric: @metric

View file

@ -13,7 +13,7 @@
= create_link
- if show_enable_confluence_integration?(@wiki.container)
= link_to s_('WikiEmpty|Enable the Confluence Wiki integration'),
edit_project_integration_path(@project, :confluence),
edit_project_settings_integration_path(@project, :confluence),
class: 'btn gl-button', title: s_('WikiEmpty|Enable the Confluence Wiki integration')
- elsif @project && can?(current_user, :read_issue, @project)

View file

@ -99,7 +99,6 @@ module Gitlab
#{config.root}/app/models/badges
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
#{config.root}/app/graphql/resolvers/concerns
#{config.root}/app/graphql/mutations/concerns
#{config.root}/app/graphql/types/concerns])

View file

@ -1,6 +1,7 @@
---
data_category: optional
key_path: counts.jira_imports_total_imported_issues_count
instrumentation_class: JiraImportsTotalImportedIssuesCountMetric
description: Count of total issues imported via the Jira Importer
product_section: dev
product_stage: ecosystem

View file

@ -130,7 +130,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resource :integrations, only: [:show]
resources :integrations, constraints: { id: %r{[^/]+} }, only: [:index, :edit, :update] do
member do
put :test
end
resources :hook_logs, only: [:show], controller: :integration_hook_logs do
member do
post :retry
end
end
end
resource :repository, only: [:show], controller: :repository do
# TODO: Removed this "create_deploy_token" route after change was made in app/helpers/ci_variables_helper.rb:14
@ -209,12 +219,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :integrations, controller: :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
# Legacy routes for `/-/integrations` which are now in `/-/settings/integrations`.
# Can be removed in 15.2, see https://gitlab.com/gitlab-org/gitlab/-/issues/334846
resources :integrations, controller: 'settings/integrations', constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
member do
put :test
end
resources :hook_logs, only: [:show], controller: :service_hook_logs do
resources :hook_logs, only: [:show], controller: 'settings/integration_hook_logs' do
member do
post :retry
end

View file

@ -5,12 +5,12 @@
removal_date: "2022-05-22"
breaking_change: true
body: | # Do not modify this line, instead modify the lines below.
A breaking change was made to the Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints
in 15.0.
The Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints have changed in 15.0.
Instead of the GitLab Runner API endpoints returning `offline` and `not_connected` for runners that have not
contacted the GitLab instance in the past three months, the API endpoints now return the `stale` value,
which was introduced in 14.6.
If a runner has not contacted the GitLab instance in more than three months, the API returns `stale` instead of `offline` or `not_connected`.
The `stale` status was introduced in 14.6.
The `not_connected` status is no longer valid. It was replaced with `never_contacted`. Available statuses are `online`, `offline`, `stale`, and `never_contacted`.
stage: Verify
tiers: [Core, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347303

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class DropIndexOnDeploymentsOnCreatedAtClusterIdAndProjectId < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'tp_index_created_at_cluster_id_project_id_on_deployments'
def up
remove_concurrent_index_by_name :deployments, INDEX_NAME
end
def down
# no-op
#
# There's no need to re-add this index as it's purpose was temporary, served only
# for a specific CR query which is now closed, and should not be re-opened.
end
end

View file

@ -0,0 +1 @@
21f37004086f6d7f606791dd7caeb7c5ca701b009689932eb9ea4eb653e3e0dc

View file

@ -29930,8 +29930,6 @@ CREATE INDEX tmp_index_projects_on_id_and_runners_token ON projects USING btree
CREATE INDEX tmp_index_projects_on_id_and_runners_token_encrypted ON projects USING btree (id, runners_token_encrypted) WHERE (runners_token_encrypted IS NOT NULL);
CREATE INDEX tp_index_created_at_cluster_id_project_id_on_deployments ON deployments USING btree (created_at, cluster_id, project_id) WHERE ((cluster_id IS NOT NULL) AND (created_at > '2022-04-03 00:00:00'::timestamp without time zone));
CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name);
CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name);

View file

@ -266,7 +266,7 @@ In this example:
When running manual jobs you can supply additional job specific variables.
You can do this from the job page of the manual job you want to run with
additional variables. To access this page, click on the **name** of the manual job in
additional variables. To access this page, select the **name** of the manual job in
the pipeline view, *not* the play (**{play}**) button.
This is useful when you want to alter the execution of a job that uses
@ -337,7 +337,7 @@ In the example above:
- `my_first_section`: The name given to the section.
- `\r\e[0K`: Prevents the section markers from displaying in the rendered (colored)
job log, but they are displayed in the raw job log. To see them, in the top right
of the job log, click **{doc-text}** (**Show complete raw**).
of the job log, select **{doc-text}** (**Show complete raw**).
- `\r`: carriage return.
- `\e[0K`: clear line ANSI escape code.

View file

@ -79,7 +79,7 @@ To create a `.gitlab-ci.yml` file:
1. On the left sidebar, select **Project information > Details**.
1. Above the file list, select the branch you want to commit to,
click the plus icon, then select **New file**:
select the plus icon, then select **New file**:
![New file](img/new_file_v13_6.png)
@ -115,7 +115,7 @@ To create a `.gitlab-ci.yml` file:
[predefined variables](../variables/predefined_variables.md)
that populate when the job runs.
1. Click **Commit changes**.
1. Select **Commit changes**.
The pipeline starts when the commit is committed.
@ -172,11 +172,11 @@ To view your pipeline:
![Three stages](img/three_stages_v13_6.png)
- To view a visual representation of your pipeline, click the pipeline ID.
- To view a visual representation of your pipeline, select the pipeline ID.
![Pipeline graph](img/pipeline_graph_v13_6.png)
- To view details of a job, click the job name, for example, `deploy-prod`.
- To view details of a job, select the job name, for example, `deploy-prod`.
![Job details](img/job_details_v13_6.png)

View file

@ -93,7 +93,7 @@ below the query. You can see multiple duplicate cached queries in this modal win
![Performance Bar Cached Queries Modal](img/performance_bar_cached_queries.png)
Click **...** to expand the actual stack trace:
Select **...** to expand the actual stack trace:
```ruby
[

View file

@ -25,7 +25,7 @@ A database review is required for:
generally up to the author of a merge request to decide whether or
not complex queries are being introduced and if they require a
database review.
- Changes in Service Data metrics that use `count`, `distinct_count` and `estimate_batch_distinct_count`.
- Changes in Service Data metrics that use `count`, `distinct_count`, `estimate_batch_distinct_count` and `sum`.
These metrics could have complex queries over large tables.
See the [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/)
for implementation details.

View file

@ -25,7 +25,7 @@ To view versions that are not available on `docs.gitlab.com`:
## Documenting version-specific features
When a feature is added or updated, you can include its version information
either as a **Version history** bullet or as an inline text reference.
either as a **Version history** list item or as an inline text reference.
You do not need to add version information on the pages in the `/development` directory.

View file

@ -187,8 +187,8 @@ Be sure to add these polyfills to `app/assets/javascripts/commons/polyfills.js`.
To see what polyfills are being used:
1. Navigate to your merge request.
1. In the secondary menu below the title of the merge request, click **Pipelines**, then
click the pipeline you want to view, to display the jobs in that pipeline.
1. In the secondary menu below the title of the merge request, select **Pipelines**, then
select the pipeline you want to view, to display the jobs in that pipeline.
1. Select the [`compile-production-assets`](https://gitlab.com/gitlab-org/gitlab/-/jobs/641770154) job.
1. In the right-hand sidebar, scroll to **Job Artifacts**, and select **Browse**.
1. Select the **webpack-report** folder to open it, and select **index.html**.

View file

@ -36,8 +36,8 @@ GitLab does not allow requests to localhost or the local network by default. Whe
Jenkins uses the GitLab API and needs an access token.
1. Sign in to your GitLab instance.
1. Click on your profile picture, then click **Settings**.
1. Click **Access Tokens**.
1. Select your profile picture, then select **Settings**.
1. Select **Access Tokens**.
1. Create a new Access Token with the **API** scope enabled. Note the value of the token.
## Configure Jenkins

View file

@ -37,13 +37,13 @@ To install the app in Jira:
Marketplace:
1. In Jira, navigate to **Jira settings > Apps > Manage apps**.
1. Scroll to the bottom of the **Manage apps** page and click **Settings**.
1. Select **Enable development mode** and click **Apply**.
1. Scroll to the bottom of the **Manage apps** page and select **Settings**.
1. Select **Enable development mode** and select **Apply**.
1. Install the app:
1. In Jira, navigate to **Jira settings > Apps > Manage apps**.
1. Click **Upload app**.
1. Select **Upload app**.
1. In the **From this URL** field, provide a link to the app descriptor. The host and port must point to your GitLab instance.
For example:
@ -52,10 +52,10 @@ To install the app in Jira:
https://xxxx.gitpod.io/-/jira_connect/app_descriptor.json
```
1. Click **Upload**.
1. Select **Upload**.
If the install was successful, you should see the **GitLab.com for Jira Cloud** app under **Manage apps**.
You can also click **Getting Started** to open the configuration page rendered from your GitLab instance.
You can also select **Getting Started** to open the configuration page rendered from your GitLab instance.
_Note that any changes to the app descriptor requires you to uninstall then reinstall the app._

View file

@ -102,29 +102,13 @@ Examples using `usage_data.rb` have been [deprecated](usage_data.md). We recomme
#### Sum batch operation
There is no support for `sum` for database metrics.
Sum the values of a given ActiveRecord_Relation on given column and handles errors.
Handles the `ActiveRecord::StatementInvalid` error
Method:
```ruby
sum(relation, column, batch_size: nil, start: nil, finish: nil)
```
Arguments:
- `relation`: the ActiveRecord_Relation to perform the operation
- `column`: the column to sum on
- `batch_size`: if none set it uses default value 1000 from `Gitlab::Database::BatchCounter`
- `start`: custom start of the batch counting to avoid complex min calculations
- `end`: custom end of the batch counting to avoid complex min calculations
Examples:
```ruby
sum(JiraImportState.finished, :imported_issues_count)
add_metric('JiraImportsTotalImportedIssuesCountMetric')
```
#### Grouping and batch operations

View file

@ -35,7 +35,7 @@ We have built a domain-specific language (DSL) to define the metrics instrumenta
## Database metrics
- `operation`: Operations for the given `relation`, one of `count`, `distinct_count`.
- `operation`: Operations for the given `relation`, one of `count`, `distinct_count`, `sum`.
- `relation`: `ActiveRecord::Relation` for the objects we want to perform the `operation`.
- `start`: Specifies the start value of the batch counting, by default is `relation.minimum(:id)`.
- `finish`: Specifies the end value of the batch counting, by default is `relation.maximum(:id)`.
@ -104,6 +104,26 @@ module Gitlab
end
```
### Sum Example
```ruby
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class JiraImportsTotalImportedIssuesCountMetric < DatabaseMetric
operation :sum, column: :imported_issues_count
relation { JiraImportState.finished }
end
end
end
end
end
```
## Redis metrics
[Example of a merge request that adds a `Redis` metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66582).
@ -228,7 +248,7 @@ end
There is support for:
- `count`, `distinct_count`, `estimate_batch_distinct_count` for [database metrics](#database-metrics).
- `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum` for [database metrics](#database-metrics).
- [Redis metrics](#redis-metrics).
- [Redis HLL metrics](#redis-hyperloglog-metrics).
- [Generic metrics](#generic-metrics), which are metrics based on settings or configurations.
@ -246,7 +266,7 @@ To create a stub instrumentation for a Service Ping metric, you can use a dedica
The generator takes the class name as an argument and the following options:
- `--type=TYPE` Required. Indicates the metric type. It must be one of: `database`, `generic`, `redis`.
- `--operation` Required for `database` type. It must be one of: `count`, `distinct_count`, `estimate_batch_distinct_count`.
- `--operation` Required for `database` type. It must be one of: `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum`.
- `--ee` Indicates if the metric is for EE.
```shell

View file

@ -94,8 +94,8 @@ the GitLab handbook information for the [shared 1Password account](https://about
1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.exec` permission first.
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps). For example, `review-qa-raise-e-12chm0`.
1. Find and open the `toolbox` Deployment. For example, `review-qa-raise-e-12chm0-toolbox`.
1. Click on the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-toolbox-d5455cc8-2lsvz`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `toolbox`.
1. Select the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-toolbox-d5455cc8-2lsvz`.
1. Select the `KUBECTL` dropdown, then `Exec` -> `toolbox`.
1. Replace `-c toolbox -- ls` with `-it -- gitlab-rails console` from the
default command or
- Run `kubectl exec --namespace review-qa-raise-e-12chm0 review-qa-raise-e-12chm0-toolbox-d5455cc8-2lsvz -it -- gitlab-rails console` and
@ -107,8 +107,8 @@ the GitLab handbook information for the [shared 1Password account](https://about
1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.getLogs` permission first.
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps). For example, `review-qa-raise-e-12chm0`.
1. Find and open the `migrations` Deployment. For example, `review-qa-raise-e-12chm0-migrations.1`.
1. Click on the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-migrations.1-nqwtx`.
1. Click on the `Container logs` link.
1. Select the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-migrations.1-nqwtx`.
1. Select `Container logs`.
Alternatively, you could use the [Logs Explorer](https://console.cloud.google.com/logs/query;query=?project=gitlab-review-apps) which provides more utility to search logs. An example query for a pod name is as follows:

View file

@ -65,21 +65,21 @@ Build a Google Cloud image with the above shared runners repository by doing the
1. In a web browser, go to the [Google Cloud Platform console](https://console.cloud.google.com/compute/images).
1. Filter images by the name you used when creating image, `windows` is likely all you need to filter by.
1. Click the image's name.
1. Click the **CREATE INSTANCE** link.
1. Select the image's name.
1. Select **CREATE INSTANCE**.
1. Important: Change name to what you'd like as you can't change it later.
1. Optional: Change Region to be closest to you as well as any other option you'd like.
1. Click **Create** at the bottom of the page.
1. Click the name of your newly created VM Instance (optionally you can filter to find it).
1. Click **Set Windows password**.
1. Select **Create** at the bottom of the page.
1. Select the name of your newly created VM Instance (optionally you can filter to find it).
1. Select **Set Windows password**.
1. Optional: Set a username or use default.
1. Click **Next**.
1. Select **Next**.
1. Copy and save the password as it is not shown again.
1. Click **RDP** down arrow.
1. Click **Download the RDP file**.
1. Select **RDP** down arrow.
1. Select **Download the RDP file**.
1. Open the downloaded RDP file with the Windows remote desktop app (<https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-clients>).
1. Click **Continue** to accept the certificate.
1. Enter the password and click **Next**.
1. Select **Continue** to accept the certificate.
1. Enter the password and select **Next**.
You should now be connected into a Windows machine with a command prompt.

View file

@ -265,7 +265,7 @@ On the EC2 dashboard, look for Load Balancer in the left navigation bar:
1. Select **Configure Security Settings** and set the following:
1. Select an SSL/TLS certificate from ACM or upload a certificate to IAM.
1. Under **Select a Cipher**, pick a predefined security policy from the dropdown. You can see a breakdown of [Predefined SSL Security Policies for Classic Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html) in the AWS docs. Check the GitLab codebase for a list of [supported SSL ciphers and protocols](https://gitlab.com/gitlab-org/gitlab/-/blob/9ee7ad433269b37251e0dd5b5e00a0f00d8126b4/lib/support/nginx/gitlab-ssl#L97-99).
1. Click **Configure Health Check** and set up a health check for your EC2 instances.
1. Select **Configure Health Check** and set up a health check for your EC2 instances.
1. For **Ping Protocol**, select HTTP.
1. For **Ping Port**, enter 80.
1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You'll need to add [the VPC IP Address Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP Allowlist](../../administration/monitoring/ip_whitelist.md) for the [Health Check endpoints](../../user/admin_area/monitoring/health_check.md)
@ -282,7 +282,7 @@ you might have.
On the Route 53 dashboard, select **Hosted zones** in the left navigation bar:
1. Select an existing hosted zone or, if you do not already have one for your domain, click **Create Hosted Zone**, enter your domain name, and click **Create**.
1. Select an existing hosted zone or, if you do not already have one for your domain, select **Create Hosted Zone**, enter your domain name, and select **Create**.
1. Select **Create Record Set** and provide the following values:
1. **Name:** Use the domain name (the default value) or enter a subdomain.
1. **Type:** Select **A - IPv4 address**.
@ -377,7 +377,7 @@ persistence and is used to store session data, temporary cache information, and
1. Navigate to the ElastiCache dashboard from your AWS console.
1. Go to **Subnet Groups** in the left menu, and create a new subnet group (we'll name ours `gitlab-redis-group`).
Make sure to select our VPC and its [private subnets](#subnets).
Make sure to select our VPC and its [private subnets](#subnets).
1. Select **Create** when ready.
![ElastiCache subnet](img/ec_subnet.png)

View file

@ -56,7 +56,7 @@ To enable Arkose Protect:
```
1. Optional. To prevent high risk sessions from signing, enable the `arkose_labs_prevent_login` feature flag. Run the following command in the Rails console:
```ruby
Feature.enable(:arkose_labs_prevent_login)
```
@ -73,5 +73,5 @@ test suite doesn't fail. This bypass is done in the `UserVerificationService` cl
## Feedback Job
To help Arkose improve their protection service, we created a daily background job to send them the list of blocked users by us.
To help Arkose improve their protection service, we created a daily background job to send them the list of blocked users by us.
This job is performed by the `Arkose::BlockedUsersReportWorker` class.

View file

@ -38,12 +38,12 @@ as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#brea
Before updating GitLab, review the details carefully to determine if you need to make any
changes to your code, settings, or workflow.
A breaking change was made to the Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints
in 15.0.
The Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints have changed in 15.0.
Instead of the GitLab Runner API endpoints returning `offline` and `not_connected` for runners that have not
contacted the GitLab instance in the past three months, the API endpoints now return the `stale` value,
which was introduced in 14.6.
If a runner has not contacted the GitLab instance in more than three months, the API returns `stale` instead of `offline` or `not_connected`.
The `stale` status was introduced in 14.6.
The `not_connected` status is no longer valid. It was replaced with `never_contacted`. Available statuses are `online`, `offline`, `stale`, and `never_contacted`.
### Audit events for repository push events

View file

@ -110,7 +110,7 @@ You can also remove the Package Registry for your project specifically:
1. In your project, go to **Settings > General**.
1. Expand the **Visibility, project features, permissions** section and disable the
**Packages** feature.
1. Click **Save changes**.
1. Select **Save changes**.
The **Packages & Registries > Package Registry** entry is removed from the sidebar.

View file

@ -1126,6 +1126,15 @@ Payload example:
"email": "user@gitlab.com"
}
},
"source_pipeline":{
"project":{
"id": 41,
"web_url": "https://gitlab.example.com/gitlab-org/upstream-project",
"path_with_namespace": "gitlab-org/upstream-project",
},
"pipeline_id": 30,
"job_id": 3401
},
"builds":[
{
"id": 380,

View file

@ -111,6 +111,7 @@ module.exports = (path, options = {}) => {
'remark-.*',
'hast*',
'unist.*',
'markdown-table',
'mdast-util-.*',
'micromark.*',
'vfile.*',

View file

@ -0,0 +1,71 @@
# frozen_string_literal: true
module BulkImports
module Projects
module Pipelines
class DesignBundlePipeline
include Pipeline
file_extraction_pipeline!
relation_name BulkImports::FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION
def extract(_context)
download_service.execute
decompression_service.execute
extraction_service.execute
bundle_path = File.join(tmpdir, "#{self.class.relation}.bundle")
BulkImports::Pipeline::ExtractedData.new(data: bundle_path)
end
def load(_context, bundle_path)
Gitlab::Utils.check_path_traversal!(bundle_path)
Gitlab::Utils.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir])
return unless portable.lfs_enabled?
return unless File.exist?(bundle_path)
return if File.directory?(bundle_path)
return if File.lstat(bundle_path).symlink?
portable.design_repository.create_from_bundle(bundle_path)
end
def after_run(_)
FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
end
private
def download_service
BulkImports::FileDownloadService.new(
configuration: context.configuration,
relative_url: context.entity.relation_download_url_path(self.class.relation),
tmpdir: tmpdir,
filename: targz_filename
)
end
def decompression_service
BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename)
end
def extraction_service
BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename)
end
def tar_filename
"#{self.class.relation}.tar"
end
def targz_filename
"#{tar_filename}.gz"
end
def tmpdir
@tmpdir ||= Dir.mktmpdir('bulk_imports')
end
end
end
end
end

View file

@ -95,6 +95,10 @@ module BulkImports
pipeline: BulkImports::Common::Pipelines::LfsObjectsPipeline,
stage: 5
},
design: {
pipeline: BulkImports::Projects::Pipelines::DesignBundlePipeline,
stage: 5
},
auto_devops: {
pipeline: BulkImports::Projects::Pipelines::AutoDevopsPipeline,
stage: 5

View file

@ -15,7 +15,7 @@ module Gitlab
redis: 'Redis'
}.freeze
ALLOWED_OPERATIONS = %w(count distinct_count estimate_batch_distinct_count).freeze
ALLOWED_OPERATIONS = %w(count distinct_count estimate_batch_distinct_count sum).freeze
source_root File.expand_path('usage_metric/templates', __dir__)

View file

@ -12,7 +12,7 @@ module Gitlab
def initialize(pipeline)
@pipeline = pipeline
super(
attrs = {
object_kind: 'pipeline',
object_attributes: hook_attrs(pipeline),
merge_request: pipeline.merge_request && merge_request_attrs(pipeline.merge_request),
@ -23,7 +23,13 @@ module Gitlab
preload_builds(pipeline, :latest_builds)
pipeline.latest_builds.map(&method(:build_hook_attrs))
end
)
}
if pipeline.source_pipeline.present?
attrs[:source_pipeline] = source_pipeline_attrs(pipeline.source_pipeline)
end
super(attrs)
end
def with_retried_builds
@ -72,6 +78,20 @@ module Gitlab
}
end
def source_pipeline_attrs(source_pipeline)
project = source_pipeline.source_project
{
project: {
id: project.id,
web_url: project.web_url,
path_with_namespace: project.full_path
},
job_id: source_pipeline.source_job_id,
pipeline_id: source_pipeline.source_pipeline_id
}
end
def merge_request_attrs(merge_request)
{
id: merge_request.id,

View file

@ -37,6 +37,7 @@ module Gitlab
class V1_0 < ActiveRecord::Migration[6.1] # rubocop:disable Naming/ClassAndModuleCamelCase
include LockRetriesConcern
include Gitlab::Database::MigrationHelpers::V2
include Gitlab::Database::MigrationHelpers::AnnounceDatabase
# When running migrations, the `db:migrate` switches connection of
# ActiveRecord::Base depending where the migration runs.

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module Database
module MigrationHelpers
module AnnounceDatabase
extend ActiveSupport::Concern
def write(text = "")
if text.present? # announce/say
super("#{db_config_name}: #{text}")
else
super(text)
end
end
def db_config_name
@db_config_name ||= Gitlab::Database.db_config_name(connection)
end
end
end
end
end

View file

@ -21,7 +21,7 @@ module Gitlab
end
end
def migrate(direction)
def exec_migration(conn, direction)
if unmatched_schemas.any?
migration_skipped
return
@ -37,8 +37,9 @@ module Gitlab
private
def migration_skipped
say "Current migration is skipped since it modifies "\
"'#{self.class.allowed_gitlab_schemas}' which is outside of '#{allowed_schemas_for_connection}'"
say "The migration is skipped since it modifies the schemas: #{self.class.allowed_gitlab_schemas}."
say "This database can only apply migrations in one of the following schemas: " \
"#{allowed_schemas_for_connection}."
end
def validator_class

View file

@ -18,7 +18,7 @@ module Gitlab
UnimplementedOperationError = Class.new(StandardError) # rubocop:disable UsageData/InstrumentationSuperclass
class << self
IMPLEMENTED_OPERATIONS = %i(count distinct_count estimate_batch_distinct_count).freeze
IMPLEMENTED_OPERATIONS = %i(count distinct_count estimate_batch_distinct_count sum).freeze
private_constant :IMPLEMENTED_OPERATIONS

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class JiraImportsTotalImportedIssuesCountMetric < DatabaseMetric
operation :sum, column: :imported_issues_count
relation { JiraImportState.finished }
end
end
end
end
end

View file

@ -25,19 +25,19 @@ module Gitlab
private
def count(relation, column = nil)
raw_sql(relation, column)
raw_count_sql(relation, column)
end
def distinct_count(relation, column = nil)
raw_sql(relation, column, true)
raw_count_sql(relation, column, true)
end
def sum(relation, column)
relation.select(relation.all.table[column].sum).to_sql
raw_sum_sql(relation, column)
end
def estimate_batch_distinct_count(relation, column = nil)
raw_sql(relation, column, true)
raw_count_sql(relation, column, true)
end
# rubocop: disable CodeReuse/ActiveRecord
@ -62,15 +62,23 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def raw_sql(relation, column, distinct = false)
def raw_count_sql(relation, column, distinct = false)
column ||= relation.primary_key
node = node_to_count(relation, column)
node = node_to_operate(relation, column)
relation.unscope(:order).select(node.count(distinct)).to_sql
end
# rubocop: enable CodeReuse/ActiveRecord
def node_to_count(relation, column)
# rubocop: disable CodeReuse/ActiveRecord
def raw_sum_sql(relation, column)
node = node_to_operate(relation, column)
relation.unscope(:order).select(node.sum).to_sql
end
# rubocop: enable CodeReuse/ActiveRecord
def node_to_operate(relation, column)
if join_relation?(relation) && joined_column?(column)
table_name, column_name = column.split(".")
Arel::Table.new(table_name)[column_name]

View file

@ -407,7 +407,7 @@ module Gitlab
{
jira_imports_total_imported_count: count(finished_jira_imports),
jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
jira_imports_total_imported_issues_count: sum(JiraImportState.finished, :imported_issues_count)
jira_imports_total_imported_issues_count: add_metric('JiraImportsTotalImportedIssuesCountMetric')
}
# rubocop: enable UsageData/LargeTable
end

View file

@ -54,7 +54,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Integrations'),
link: project_settings_integrations_path(context.project),
active_routes: { path: %w[integrations#show services#edit] },
active_routes: { path: %w[integrations#index integrations#edit] },
item_id: :integrations
)
end

View file

@ -8624,9 +8624,6 @@ msgstr ""
msgid "ClusterIntegration|Create a Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal"
msgstr ""
msgid "ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared."
msgstr ""
@ -8786,9 +8783,6 @@ msgstr ""
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Removes cluster from project but keeps associated resources"
msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
@ -36031,6 +36025,9 @@ msgstr ""
msgid "Stage"
msgstr ""
msgid "Stage:"
msgstr ""
msgid "Standard"
msgstr ""
@ -41563,6 +41560,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Total number of deploys to production."
msgstr ""
msgid "ValueStreamAnalytics|Value Stream"
msgstr ""
msgid "ValueStreamEvent|Items in stage"
msgstr ""

View file

@ -164,6 +164,7 @@
"raphael": "^2.2.7",
"raw-loader": "^4.0.2",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"scrollparent": "^2.0.1",

View file

@ -62,7 +62,7 @@ RSpec.describe Projects::MattermostsController do
subject
integration = project.integrations.last
expect(subject).to redirect_to(edit_project_integration_path(project, integration))
expect(subject).to redirect_to(edit_project_settings_integration_path(project, integration))
end
end
end

View file

@ -141,7 +141,7 @@ RSpec.describe Projects::Prometheus::MetricsController do
expect(flash[:notice]).to include('Metric was successfully added.')
expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus))
expect(response).to redirect_to(edit_project_settings_integration_path(project, ::Integrations::Prometheus))
end
end
@ -168,7 +168,7 @@ RSpec.describe Projects::Prometheus::MetricsController do
expect(metric.reload.title).to eq('new_title')
expect(flash[:notice]).to include('Metric was successfully updated.')
expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus))
expect(response).to redirect_to(edit_project_settings_integration_path(project, ::Integrations::Prometheus))
end
end
end
@ -180,7 +180,7 @@ RSpec.describe Projects::Prometheus::MetricsController do
it 'destroys the metric' do
delete :destroy, params: project_params(id: metric.id)
expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus))
expect(response).to redirect_to(edit_project_settings_integration_path(project, ::Integrations::Prometheus))
expect(PrometheusMetric.find_by(id: metric.id)).to be_nil
end
end

View file

@ -1,356 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ServicesController do
include JiraServiceHelper
include AfterNextHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:jira_integration) { create(:jira_integration, project: project) }
let(:integration) { jira_integration }
let(:integration_params) { { username: 'username', password: 'password', url: 'http://example.com' } }
before do
sign_in(user)
project.add_maintainer(user)
end
it_behaves_like Integrations::Actions do
let(:integration_attributes) { { project: project } }
let(:routing_params) do
{
namespace_id: project.namespace,
project_id: project,
id: integration.to_param
}
end
end
describe '#test' do
context 'when the integration is not testable' do
it 'renders 404' do
allow_any_instance_of(Integration).to receive(:testable?).and_return(false)
put :test, params: project_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when validations fail' do
let(:integration_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do
put :test, params: project_params(service: integration_params)
expect(json_response['message']).to eq 'Validations failed.'
expect(json_response['service_response']).to include "Url can't be blank"
expect(response).to be_successful
end
end
context 'when successful' do
context 'with empty project' do
let_it_be(:project) { create(:project) }
context 'with chat notification integration' do
let_it_be(:teams_integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') }
let(:integration) { teams_integration }
it 'returns success' do
allow_next(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
put :test, params: project_params
expect(response).to be_successful
end
end
it 'returns success' do
stub_jira_integration_test
expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
end
end
it 'returns success' do
stub_jira_integration_test
expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
end
context 'when service is configured for the first time' do
let(:integration_params) do
{
'active' => '1',
'push_events' => '1',
'token' => 'token',
'project_url' => 'https://buildkite.com/organization/pipeline'
}
end
before do
allow_any_instance_of(ServiceHook).to receive(:execute).and_return(true)
end
it 'persist the object' do
do_put
expect(response).to be_successful
expect(json_response).to be_empty
expect(Integrations::Buildkite.first).to be_present
end
it 'creates the ServiceHook object' do
do_put
expect(response).to be_successful
expect(json_response).to be_empty
expect(Integrations::Buildkite.first.service_hook).to be_present
end
def do_put
put :test, params: project_params(id: 'buildkite',
service: integration_params)
end
end
end
context 'when unsuccessful' do
it 'returns an error response when the integration test fails' do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404)
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => '',
'test_failed' => true
)
end
context 'with the Slack integration' do
let_it_be(:integration) { build(:integrations_slack) }
it 'returns an error response when the URL is blocked' do
put :test, params: project_params(service: { webhook: 'http://127.0.0.1' })
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed",
'test_failed' => true
)
end
it 'returns an error response when a network exception is raised' do
expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED)
put :test, params: project_params
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => 'Connection refused',
'test_failed' => true
)
end
end
end
end
describe 'PUT #update' do
describe 'as HTML' do
let(:integration_params) { { active: true } }
let(:params) { project_params(service: integration_params) }
let(:message) { 'Jira settings saved and active.' }
let(:redirect_url) { edit_project_integration_path(project, integration) }
before do
stub_jira_integration_test
put :update, params: params
end
shared_examples 'integration update' do
it 'redirects to the correct url with a flash message' do
expect(response).to redirect_to(redirect_url)
expect(flash[:notice]).to eq(message)
end
end
context 'when param `active` is set to true' do
let(:params) { project_params(service: integration_params, redirect_to: redirect) }
context 'when redirect_to param is present' do
let(:redirect) { '/redirect_here' }
let(:redirect_url) { redirect }
it_behaves_like 'integration update'
end
context 'when redirect_to is an external domain' do
let(:redirect) { 'http://examle.com' }
it_behaves_like 'integration update'
end
context 'when redirect_to param is an empty string' do
let(:redirect) { '' }
it_behaves_like 'integration update'
end
end
context 'when param `active` is set to false' do
let(:integration_params) { { active: false } }
let(:message) { 'Jira settings saved, but not active.' }
it_behaves_like 'integration update'
end
context 'when param `inherit_from_id` is set to empty string' do
let(:integration_params) { { inherit_from_id: '' } }
it 'sets inherit_from_id to nil' do
expect(integration.reload.inherit_from_id).to eq(nil)
end
end
context 'when param `inherit_from_id` is set to an instance integration' do
let(:instance_integration) { create(:jira_integration, :instance, url: 'http://instance.com', password: 'instance') }
let(:integration_params) { { inherit_from_id: instance_integration.id, url: 'http://custom.com', password: 'custom' } }
it 'ignores submitted params and inherits instance settings' do
expect(integration.reload).to have_attributes(
inherit_from_id: instance_integration.id,
url: instance_integration.url,
password: instance_integration.password
)
end
end
context 'when param `inherit_from_id` is set to a group integration' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:jira_integration) { create(:jira_integration, project: project) }
let(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') }
let(:integration_params) { { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } }
it 'ignores submitted params and inherits group settings' do
expect(integration.reload).to have_attributes(
inherit_from_id: group_integration.id,
url: group_integration.url,
password: group_integration.password
)
end
end
context 'when param `inherit_from_id` is set to an unrelated group' do
let_it_be(:group) { create(:group) }
let(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') }
let(:integration_params) { { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } }
it 'ignores the param and saves the submitted settings' do
expect(integration.reload).to have_attributes(
inherit_from_id: nil,
url: 'http://custom.com',
password: 'custom'
)
end
end
end
describe 'as JSON' do
before do
stub_jira_integration_test
put :update, params: project_params(service: integration_params, format: :json)
end
context 'when update succeeds' do
let(:integration_params) { { url: 'http://example.com', password: 'password' } }
it 'returns success response' do
expect(response).to be_successful
expect(json_response).to include(
'active' => true,
'errors' => {}
)
end
end
context 'when update fails with missing password' do
let(:integration_params) { { url: 'http://example.com' } }
it 'returns JSON response errors' do
expect(response).not_to be_successful
expect(json_response).to include(
'active' => true,
'errors' => {
'password' => ["can't be blank"]
}
)
end
end
context 'when update fails with invalid URL' do
let(:integration_params) { { url: '', password: 'password' } }
it 'returns JSON response with errors' do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
'active' => true,
'errors' => { 'url' => ['must be a valid URL', "can't be blank"] }
)
end
end
end
end
describe 'GET #edit' do
context 'with Jira service' do
let(:integration_param) { 'jira' }
before do
get :edit, params: project_params(id: integration_param)
end
context 'with approved services' do
it 'renders edit page' do
expect(response).to be_successful
end
end
end
end
private
def project_params(opts = {})
opts.reverse_merge(
namespace_id: project.namespace,
project_id: project,
id: integration.to_param
)
end
end

View file

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::ServiceHookLogsController do
RSpec.describe Projects::Settings::IntegrationHookLogsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:integration) { create(:drone_ci_integration, project: project) }
@ -44,7 +44,8 @@ RSpec.describe Projects::ServiceHookLogsController do
it 'executes the hook and redirects to the service form' do
expect_any_instance_of(ServiceHook).to receive(:execute)
expect_any_instance_of(described_class).to receive(:set_hook_execution_notice)
expect(subject).to redirect_to(edit_project_integration_path(project, integration))
expect(subject).to redirect_to(edit_project_settings_integration_path(project, integration))
end
it 'renders a 404 if the hook does not exist' do

View file

@ -3,20 +3,388 @@
require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
include JiraServiceHelper
include AfterNextHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:jira_integration) { create(:jira_integration, project: project) }
let(:integration) { jira_integration }
let(:integration_params) { { username: 'username', password: 'password', url: 'http://example.com' } }
before do
project.add_maintainer(user)
sign_in(user)
project.add_maintainer(user)
end
describe 'GET show' do
it 'renders show with 200 status code' do
get :show, params: { namespace_id: project.namespace, project_id: project }
it_behaves_like Integrations::Actions do
let(:integration_attributes) { { project: project } }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
let(:routing_params) do
{
namespace_id: project.namespace,
project_id: project,
id: integration.to_param
}
end
end
describe 'GET index' do
it 'renders index with 200 status code' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
describe '#test' do
context 'when the integration is not testable' do
it 'renders 404' do
allow_any_instance_of(Integration).to receive(:testable?).and_return(false)
put :test, params: project_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when validations fail' do
let(:integration_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do
put :test, params: project_params(service: integration_params)
expect(json_response['message']).to eq 'Validations failed.'
expect(json_response['service_response']).to include "Url can't be blank"
expect(response).to be_successful
end
end
context 'when successful' do
context 'with empty project' do
let_it_be(:project) { create(:project) }
context 'with chat notification integration' do
let_it_be(:teams_integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') }
let(:integration) { teams_integration }
it 'returns success' do
allow_next(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
put :test, params: project_params
expect(response).to be_successful
end
end
it 'returns success' do
stub_jira_integration_test
expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
end
end
it 'returns success' do
stub_jira_integration_test
expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
end
context 'when service is configured for the first time' do
let(:integration_params) do
{
'active' => '1',
'push_events' => '1',
'token' => 'token',
'project_url' => 'https://buildkite.com/organization/pipeline'
}
end
before do
allow_next(ServiceHook).to receive(:execute).and_return(true)
end
it 'persist the object' do
do_put
expect(response).to be_successful
expect(json_response).to be_empty
expect(Integrations::Buildkite.first).to be_present
end
it 'creates the ServiceHook object' do
do_put
expect(response).to be_successful
expect(json_response).to be_empty
expect(Integrations::Buildkite.first.service_hook).to be_present
end
def do_put
put :test, params: project_params(id: 'buildkite',
service: integration_params)
end
end
end
context 'when unsuccessful' do
it 'returns an error response when the integration test fails' do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404)
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => '',
'test_failed' => true
)
end
context 'with the Slack integration' do
let_it_be(:integration) { build(:integrations_slack) }
it 'returns an error response when the URL is blocked' do
put :test, params: project_params(service: { webhook: 'http://127.0.0.1' })
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed",
'test_failed' => true
)
end
it 'returns an error response when a network exception is raised' do
expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED)
put :test, params: project_params
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Connection failed. Please check your settings.',
'service_response' => 'Connection refused',
'test_failed' => true
)
end
end
end
end
describe 'PUT #update' do
describe 'as HTML' do
let(:integration_params) { { active: true } }
let(:params) { project_params(service: integration_params) }
let(:message) { 'Jira settings saved and active.' }
let(:redirect_url) { edit_project_settings_integration_path(project, integration) }
before do
stub_jira_integration_test
put :update, params: params
end
shared_examples 'integration update' do
it 'redirects to the correct url with a flash message' do
expect(response).to redirect_to(redirect_url)
expect(flash[:notice]).to eq(message)
end
end
context 'when update fails' do
let(:integration_params) { { url: 'https://new.com', password: '' } }
it 'renders the edit form' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:edit)
expect(integration.reload.url).not_to eq('https://new.com')
end
end
context 'when param `active` is set to true' do
let(:params) { project_params(service: integration_params, redirect_to: redirect) }
context 'when redirect_to param is present' do
let(:redirect) { '/redirect_here' }
let(:redirect_url) { redirect }
it_behaves_like 'integration update'
end
context 'when redirect_to is an external domain' do
let(:redirect) { 'http://examle.com' }
it_behaves_like 'integration update'
end
context 'when redirect_to param is an empty string' do
let(:redirect) { '' }
it_behaves_like 'integration update'
end
end
context 'when param `active` is set to false' do
let(:integration_params) { { active: false } }
let(:message) { 'Jira settings saved, but not active.' }
it_behaves_like 'integration update'
end
context 'when param `inherit_from_id` is set to empty string' do
let(:integration_params) { { inherit_from_id: '' } }
it 'sets inherit_from_id to nil' do
expect(integration.reload.inherit_from_id).to eq(nil)
end
end
context 'when param `inherit_from_id` is set to an instance integration' do
let(:instance_integration) do
create(:jira_integration, :instance, url: 'http://instance.com', password: 'instance')
end
let(:integration_params) do
{ inherit_from_id: instance_integration.id, url: 'http://custom.com', password: 'custom' }
end
it 'ignores submitted params and inherits instance settings' do
expect(integration.reload).to have_attributes(
inherit_from_id: instance_integration.id,
url: instance_integration.url,
password: instance_integration.password
)
end
end
context 'when param `inherit_from_id` is set to a group integration' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:jira_integration) { create(:jira_integration, project: project) }
let(:group_integration) do
create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group')
end
let(:integration_params) do
{ inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' }
end
it 'ignores submitted params and inherits group settings' do
expect(integration.reload).to have_attributes(
inherit_from_id: group_integration.id,
url: group_integration.url,
password: group_integration.password
)
end
end
context 'when param `inherit_from_id` is set to an unrelated group' do
let_it_be(:group) { create(:group) }
let(:group_integration) do
create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group')
end
let(:integration_params) do
{ inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' }
end
it 'ignores the param and saves the submitted settings' do
expect(integration.reload).to have_attributes(
inherit_from_id: nil,
url: 'http://custom.com',
password: 'custom'
)
end
end
end
describe 'as JSON' do
before do
stub_jira_integration_test
put :update, params: project_params(service: integration_params, format: :json)
end
context 'when update succeeds' do
let(:integration_params) { { url: 'http://example.com', password: 'password' } }
it 'returns success response' do
expect(response).to be_successful
expect(json_response).to include(
'active' => true,
'errors' => {}
)
end
end
context 'when update fails with missing password' do
let(:integration_params) { { url: 'http://example.com' } }
it 'returns JSON response errors' do
expect(response).not_to be_successful
expect(json_response).to include(
'active' => true,
'errors' => {
'password' => ["can't be blank"]
}
)
end
end
context 'when update fails with invalid URL' do
let(:integration_params) { { url: '', password: 'password' } }
it 'returns JSON response with errors' do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
'active' => true,
'errors' => { 'url' => ['must be a valid URL', "can't be blank"] }
)
end
end
end
end
describe 'GET #edit' do
context 'with Jira service' do
let(:integration_param) { 'jira' }
before do
get :edit, params: project_params(id: integration_param)
end
context 'with approved services' do
it 'renders edit page' do
expect(response).to be_successful
end
end
end
end
private
def project_params(opts = {})
opts.reverse_merge(
namespace_id: project.namespace,
project_id: project,
id: integration.to_param
)
end
end

View file

@ -112,9 +112,9 @@ RSpec.describe 'User Cluster', :js do
context 'when user destroys the cluster' do
before do
click_link 'Advanced Settings'
click_button 'Remove integration and resources'
find('[data-testid="remove-integration-button"]').click
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
find('[data-testid="remove-integration-modal-button"]').click
end
it 'user sees creation form with the successful message' do

View file

@ -66,9 +66,9 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user destroys the cluster' do
before do
click_link 'Advanced Settings'
click_button 'Remove integration and resources'
find('[data-testid="remove-integration-button"]').click
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
find('[data-testid="remove-integration-modal-button"]').click
click_link 'Certificate'
end

View file

@ -100,9 +100,9 @@ RSpec.describe 'User Cluster', :js do
context 'when user destroys the cluster' do
before do
click_link 'Advanced Settings'
click_button 'Remove integration and resources'
find('[data-testid="remove-integration-button"]').click
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
find('[data-testid="remove-integration-modal-button"]').click
click_link 'Certificate'
end

Some files were not shown because too many files have changed in this diff Show more