Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4dc41ac252
commit
28b119a4b4
144 changed files with 2009 additions and 1278 deletions
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -66,6 +66,10 @@ const factorySpecs = {
|
|||
title: hastNode.properties.title,
|
||||
}),
|
||||
},
|
||||
strike: {
|
||||
type: 'mark',
|
||||
selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName),
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(/^
/, '');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$nextTick(() => {
|
||||
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
|
||||
});
|
||||
},
|
||||
safeHtmlConfig: {
|
||||
ADD_TAGS: ['gl-emoji'],
|
||||
},
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const USER_POPOVER_DELAY = 200;
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
1
db/schema_migrations/20220524202158
Normal file
1
db/schema_migrations/20220524202158
Normal file
|
@ -0,0 +1 @@
|
|||
21f37004086f6d7f606791dd7caeb7c5ca701b009689932eb9ea4eb653e3e0dc
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
[
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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**.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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._
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -111,6 +111,7 @@ module.exports = (path, options = {}) => {
|
|||
'remark-.*',
|
||||
'hast*',
|
||||
'unist.*',
|
||||
'markdown-table',
|
||||
'mdast-util-.*',
|
||||
'micromark.*',
|
||||
'vfile.*',
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
23
lib/gitlab/database/migration_helpers/announce_database.rb
Normal file
23
lib/gitlab/database/migration_helpers/announce_database.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue