Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4d4c33870f
commit
90ffe7528b
|
|
@ -18,7 +18,7 @@ variables:
|
|||
# Helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "6cdb0e1cd4ceb7c9fd01ffa2f62c4a7a4c77a23b"
|
||||
# Specific ref for cng-mirror project to trigger builds for
|
||||
GITLAB_CNG_MIRROR_REF: "272b2069fc386348d2474eac26060e781f2a0dd2"
|
||||
GITLAB_CNG_MIRROR_REF: "51637e0b7ab573aa0817945df517c7c0d0250d79"
|
||||
# Makes sure some of the common scripts from pipeline-common use bundler to execute commands
|
||||
RUN_WITH_BUNDLE: "true"
|
||||
# Makes sure reporting script defined in .gitlab-qa-report from pipeline-common is executed from correct folder
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
6093f9c42ad8d328c37423e3067f3c9e7cd1ddcf
|
||||
c995db1d0baa5ffa66832d48adeac8c2eabe2811
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@
|
|||
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
|
||||
{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
|
||||
{"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"},
|
||||
{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"},
|
||||
{"name":"lefthook","version":"1.11.16","platform":"ruby","checksum":"1a273f1963b4a899c56f7cf14ccea90b17af239bf7acf09cbc9e4ca6298ee685"},
|
||||
{"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"},
|
||||
{"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"},
|
||||
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},
|
||||
|
|
|
|||
|
|
@ -1105,7 +1105,7 @@ GEM
|
|||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
lefthook (1.11.14)
|
||||
lefthook (1.11.16)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@
|
|||
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
|
||||
{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
|
||||
{"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"},
|
||||
{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"},
|
||||
{"name":"lefthook","version":"1.11.16","platform":"ruby","checksum":"1a273f1963b4a899c56f7cf14ccea90b17af239bf7acf09cbc9e4ca6298ee685"},
|
||||
{"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"},
|
||||
{"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"},
|
||||
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},
|
||||
|
|
|
|||
|
|
@ -1099,7 +1099,7 @@ GEM
|
|||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
lefthook (1.11.14)
|
||||
lefthook (1.11.16)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import TabsWithList from '~/groups_projects/components/tabs_with_list.vue';
|
|||
import {
|
||||
FILTERED_SEARCH_TOKEN_LANGUAGE,
|
||||
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
PAGINATION_TYPE_KEYSET,
|
||||
} from '~/groups_projects/constants';
|
||||
import {
|
||||
|
|
@ -31,6 +32,7 @@ export default {
|
|||
filteredSearchSupportedTokens: [
|
||||
FILTERED_SEARCH_TOKEN_LANGUAGE,
|
||||
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
],
|
||||
timestampTypeMap: {
|
||||
[SORT_OPTION_CREATED.value]: TIMESTAMP_TYPE_CREATED_AT,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ query getAdminProjectCounts(
|
|||
$search: String
|
||||
$minAccessLevel: AccessLevelEnum
|
||||
$programmingLanguageName: String
|
||||
$visibilityLevel: VisibilityLevelsEnum
|
||||
) {
|
||||
active: projects(
|
||||
active: true
|
||||
search: $search
|
||||
minAccessLevel: $minAccessLevel
|
||||
programmingLanguageName: $programmingLanguageName
|
||||
visibilityLevel: $visibilityLevel
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
@ -16,6 +18,7 @@ query getAdminProjectCounts(
|
|||
search: $search
|
||||
minAccessLevel: $minAccessLevel
|
||||
programmingLanguageName: $programmingLanguageName
|
||||
visibilityLevel: $visibilityLevel
|
||||
) {
|
||||
count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ query getAdminProjects(
|
|||
$sort: String
|
||||
$search: String
|
||||
$minAccessLevel: AccessLevelEnum
|
||||
$visibilityLevel: VisibilityLevelsEnum
|
||||
$programmingLanguageName: String
|
||||
) {
|
||||
projects(
|
||||
|
|
@ -22,6 +23,7 @@ query getAdminProjects(
|
|||
search: $search
|
||||
minAccessLevel: $minAccessLevel
|
||||
programmingLanguageName: $programmingLanguageName
|
||||
visibilityLevel: $visibilityLevel
|
||||
) {
|
||||
count
|
||||
nodes {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ import {
|
|||
ACCESS_LEVEL_OWNER_INTEGER,
|
||||
ACCESS_LEVELS_INTEGER_TO_STRING,
|
||||
} from '~/access_level/constants';
|
||||
import {
|
||||
VISIBILITY_LEVEL_LABELS,
|
||||
VISIBILITY_LEVEL_PRIVATE_STRING,
|
||||
VISIBILITY_LEVEL_INTERNAL_STRING,
|
||||
VISIBILITY_LEVEL_PUBLIC_STRING,
|
||||
} from '~/visibility_level/constants';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import {
|
||||
|
|
@ -23,6 +29,7 @@ import {
|
|||
PAGINATION_TYPE_KEYSET,
|
||||
PAGINATION_TYPE_OFFSET,
|
||||
QUERY_PARAM_PAGE,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
} from '../constants';
|
||||
import userPreferencesUpdateMutation from '../graphql/mutations/user_preferences_update.mutation.graphql';
|
||||
import TabView from './tab_view.vue';
|
||||
|
|
@ -186,6 +193,22 @@ export default {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
icon: 'eye',
|
||||
title: __('Visibility'),
|
||||
token: GlFilteredSearchToken,
|
||||
unique: true,
|
||||
operators: OPERATORS_IS,
|
||||
options: [
|
||||
VISIBILITY_LEVEL_PRIVATE_STRING,
|
||||
VISIBILITY_LEVEL_INTERNAL_STRING,
|
||||
VISIBILITY_LEVEL_PUBLIC_STRING,
|
||||
].map((visibilityLevelString) => ({
|
||||
value: visibilityLevelString,
|
||||
title: VISIBILITY_LEVEL_LABELS[visibilityLevelString],
|
||||
})),
|
||||
},
|
||||
].filter((filteredSearchToken) =>
|
||||
this.filteredSearchSupportedTokens.includes(filteredSearchToken.type),
|
||||
);
|
||||
|
|
@ -236,9 +259,8 @@ export default {
|
|||
},
|
||||
filters() {
|
||||
const filters = pick(this.routeQueryWithoutPagination, [
|
||||
FILTERED_SEARCH_TOKEN_LANGUAGE,
|
||||
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
|
||||
this.filteredSearchTermKey,
|
||||
...this.filteredSearchSupportedTokens,
|
||||
]);
|
||||
|
||||
// Normalize the property to Number since Vue Router 4 will
|
||||
|
|
@ -265,10 +287,15 @@ export default {
|
|||
this.programmingLanguages.find(({ id }) => id === parseInt(programmingLanguageId, 10))?.name
|
||||
);
|
||||
},
|
||||
visibilityLevel() {
|
||||
const visibilityLevel = this.filters[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL];
|
||||
return Array.isArray(visibilityLevel) ? visibilityLevel[0] : visibilityLevel;
|
||||
},
|
||||
filtersAsQueryVariables() {
|
||||
return {
|
||||
programmingLanguageName: this.programmingLanguageName,
|
||||
minAccessLevel: this.minAccessLevel,
|
||||
visibilityLevel: this.visibilityLevel,
|
||||
};
|
||||
},
|
||||
timestampType() {
|
||||
|
|
@ -509,8 +536,9 @@ export default {
|
|||
size="sm"
|
||||
class="gl-tab-counter-badge"
|
||||
data-testid="tab-counter-badge"
|
||||
>{{ tabCount(tab) }}</gl-badge
|
||||
>
|
||||
{{ tabCount(tab) }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const SORT_LABEL_STORAGE_SIZE = __('Storage size');
|
|||
|
||||
export const FILTERED_SEARCH_TOKEN_LANGUAGE = 'language';
|
||||
export const FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL = 'min_access_level';
|
||||
export const FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL = 'visibility_level';
|
||||
|
||||
export const SORT_DIRECTION_ASC = 'asc';
|
||||
export const SORT_DIRECTION_DESC = 'desc';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import {
|
||||
GlAvatarLabeled,
|
||||
GlBadge,
|
||||
GlEmptyState,
|
||||
GlIcon,
|
||||
GlKeysetPagination,
|
||||
GlLoadingIcon,
|
||||
|
|
@ -18,6 +17,7 @@ import { fetchPolicies } from '~/lib/graphql';
|
|||
import { DEFAULT_PAGE_SIZE } from '~/members/constants';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import EmptyResult from '~/vue_shared/components/empty_result.vue';
|
||||
import {
|
||||
PLACEHOLDER_STATUS_KEPT_AS_PLACEHOLDER,
|
||||
PLACEHOLDER_STATUS_COMPLETED,
|
||||
|
|
@ -34,7 +34,6 @@ export default {
|
|||
components: {
|
||||
GlAvatarLabeled,
|
||||
GlBadge,
|
||||
GlEmptyState,
|
||||
GlIcon,
|
||||
GlKeysetPagination,
|
||||
GlLoadingIcon,
|
||||
|
|
@ -43,6 +42,7 @@ export default {
|
|||
GlLink,
|
||||
PlaceholderActions,
|
||||
HelpPopover,
|
||||
EmptyResult,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -150,11 +150,6 @@ export default {
|
|||
isSearchQueryTooShort() {
|
||||
return this.querySearch && this.querySearch.trim().length < MINIMUM_QUERY_LENGTH;
|
||||
},
|
||||
emptyText() {
|
||||
return this.isSearchQueryTooShort
|
||||
? __('Enter at least three characters to search.')
|
||||
: __('Edit your search and try again');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -217,7 +212,9 @@ export default {
|
|||
</template>
|
||||
|
||||
<template #empty>
|
||||
<gl-empty-state :title="__('No results found')" :description="emptyText" />
|
||||
<!-- EmptyResult shows minimum length message when searchMinimumLength is set,
|
||||
otherwise shows generic search message -->
|
||||
<empty-result type="search" :search-minimum-length="isSearchQueryTooShort ? 3 : null" />
|
||||
</template>
|
||||
|
||||
<template #cell(user)="{ item }">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { parseBoolean } from '~/lib/utils/common_utils';
|
|||
import initSourcegraph from '~/sourcegraph';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import initAwardsApp from '~/emoji/awards_app';
|
||||
import { initMrExperienceSurvey } from '~/surveys/merge_request_experience';
|
||||
import toast from '~/vue_shared/plugins/global_toast';
|
||||
import getStateQuery from './queries/get_state.query.graphql';
|
||||
import initCheckoutModal from './init_checkout_modal';
|
||||
|
|
@ -23,7 +22,6 @@ export default function initMergeRequestShow(store, pinia) {
|
|||
initSourcegraph();
|
||||
initIssuableSidebar();
|
||||
initAwardsApp(document.getElementById('js-vue-awards-block'));
|
||||
initMrExperienceSurvey();
|
||||
initCheckoutModal();
|
||||
|
||||
const el = document.querySelector('.js-mr-header');
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
<script>
|
||||
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'SatisfactionRate',
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
unhappy: s__('Surveys|Unhappy'),
|
||||
delighted: s__('Surveys|Delighted'),
|
||||
},
|
||||
grades: [
|
||||
{
|
||||
title: s__('Surveys|Unhappy'),
|
||||
icon: 'face-unhappy',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
title: s__('Surveys|Sad'),
|
||||
icon: 'slight-frown',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
title: s__('Surveys|Neutral'),
|
||||
icon: 'face-neutral',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
title: s__('Surveys|Happy'),
|
||||
icon: 'slight-smile',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
title: s__('Surveys|Delighted'),
|
||||
icon: 'smiley',
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ul class="gl-m-0 gl-flex gl-list-none gl-justify-between gl-p-0">
|
||||
<li v-for="grade in $options.grades" :key="grade.value">
|
||||
<gl-button
|
||||
v-gl-tooltip="grade.title"
|
||||
class="!gl-p-2"
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
:aria-label="grade.title"
|
||||
@click="$emit('rate', grade.value)"
|
||||
>
|
||||
<gl-icon class="gl-align-top" :name="grade.icon" :size="24" />
|
||||
</gl-button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="gl-flex gl-justify-between gl-pt-3 gl-text-sm gl-text-subtle">
|
||||
<div>{{ $options.i18n.unhappy }}</div>
|
||||
<div>{{ $options.i18n.delighted }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
|
||||
Vue.use(Translate);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export const startMrSurveyApp = () => {
|
||||
const mountEl = document.querySelector('#js-mr-experience-survey');
|
||||
if (!mountEl) return;
|
||||
|
||||
let channel = null;
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
const { accountAge } = mountEl.dataset;
|
||||
|
||||
const app = new Vue({
|
||||
apolloProvider,
|
||||
data() {
|
||||
return {
|
||||
hidden: false,
|
||||
};
|
||||
},
|
||||
render(h) {
|
||||
if (this.hidden) return null;
|
||||
return h(MergeRequestExperienceSurveyApp, {
|
||||
props: {
|
||||
accountAge: Number(accountAge),
|
||||
},
|
||||
on: {
|
||||
close: () => {
|
||||
channel?.postMessage('close');
|
||||
app.hidden = true;
|
||||
},
|
||||
rate: () => {
|
||||
channel?.postMessage('close');
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
app.$mount(mountEl);
|
||||
|
||||
if (window.BroadcastChannel) {
|
||||
channel = new BroadcastChannel('mr_survey');
|
||||
channel.addEventListener('message', ({ data }) => {
|
||||
if (data === 'close') {
|
||||
app.hidden = true;
|
||||
channel.close();
|
||||
channel = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
<script>
|
||||
import { GlButton, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg?raw';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { s__, __ } from '~/locale';
|
||||
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
|
||||
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
|
||||
import Tracking from '~/tracking';
|
||||
import { PROMO_URL } from '~/constants';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: 'overall',
|
||||
question: s__('MrSurvey|Overall, how satisfied are you with merge requests?'),
|
||||
},
|
||||
{
|
||||
label: 'performance',
|
||||
question: s__(
|
||||
'MrSurvey|How satisfied are you with %{strongStart}speed/performance%{strongEnd} of merge requests?',
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const MR_RENDER_LS_KEY = 'mr_survey_rendered';
|
||||
|
||||
export default {
|
||||
name: 'MergeRequestExperienceSurveyApp',
|
||||
components: {
|
||||
UserCalloutDismisser,
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
SatisfactionRate,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
tooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
accountAge: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
survey: s__('MrSurvey|Merge request experience survey'),
|
||||
close: __('Close'),
|
||||
legal: s__(
|
||||
'MrSurvey|By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the %{linkStart}GitLab Privacy Policy%{linkEnd}.',
|
||||
),
|
||||
thanks: s__('MrSurvey|Thank you for your feedback!'),
|
||||
},
|
||||
gitlabLogo,
|
||||
privacyLink: `${PROMO_URL}/privacy/`,
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
stepIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
step() {
|
||||
return steps[this.stepIndex];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keyup', this.handleKeyup);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('keyup', this.handleKeyup);
|
||||
},
|
||||
methods: {
|
||||
onQueryLoaded({ shouldShowCallout }) {
|
||||
this.visible = shouldShowCallout;
|
||||
if (!this.visible) this.$emit('close');
|
||||
else if (!localStorage?.getItem(MR_RENDER_LS_KEY)) {
|
||||
this.track('survey:mr_experience', {
|
||||
label: 'render',
|
||||
extra: {
|
||||
accountAge: this.accountAge,
|
||||
},
|
||||
});
|
||||
localStorage?.setItem(MR_RENDER_LS_KEY, '1');
|
||||
}
|
||||
},
|
||||
onRate(event) {
|
||||
this.$refs.dismisser?.dismiss();
|
||||
this.$emit('rate');
|
||||
localStorage?.removeItem(MR_RENDER_LS_KEY);
|
||||
this.track('survey:mr_experience', {
|
||||
label: this.step.label,
|
||||
value: event,
|
||||
extra: {
|
||||
accountAge: this.accountAge,
|
||||
},
|
||||
});
|
||||
this.stepIndex += 1;
|
||||
if (!this.step) {
|
||||
setTimeout(() => {
|
||||
this.$emit('close');
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
handleKeyup(e) {
|
||||
if (e.key !== 'Escape') return;
|
||||
this.dismiss();
|
||||
},
|
||||
dismiss() {
|
||||
this.$refs.dismisser?.dismiss();
|
||||
this.$emit('close');
|
||||
this.track('survey:mr_experience', {
|
||||
label: 'dismiss',
|
||||
extra: {
|
||||
accountAge: this.accountAge,
|
||||
},
|
||||
});
|
||||
localStorage?.removeItem(MR_RENDER_LS_KEY);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<user-callout-dismisser
|
||||
ref="dismisser"
|
||||
feature-name="mr_experience_survey"
|
||||
@queryResult.once="onQueryLoaded"
|
||||
>
|
||||
<aside
|
||||
class="mr-experience-survey-wrapper gl-fixed gl-bottom-0 gl-right-0 gl-p-5"
|
||||
:aria-label="$options.i18n.survey"
|
||||
>
|
||||
<transition name="survey-slide-up">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="mr-experience-survey-body gl-border gl-relative gl-flex gl-flex-col gl-rounded-base gl-bg-overlap gl-p-5"
|
||||
>
|
||||
<gl-button
|
||||
v-tooltip="$options.i18n.close"
|
||||
:aria-label="$options.i18n.close"
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
class="gl-absolute gl-right-3 gl-top-4"
|
||||
icon="close"
|
||||
@click="dismiss"
|
||||
/>
|
||||
<div
|
||||
v-if="stepIndex === 0"
|
||||
class="mr-experience-survey-legal gl-border-t gl-mt-5 gl-pt-3 gl-text-sm gl-text-subtle"
|
||||
role="note"
|
||||
>
|
||||
<p class="gl-m-0">
|
||||
<gl-sprintf :message="$options.i18n.legal">
|
||||
<template #link="{ content }">
|
||||
<a
|
||||
class="gl-text-subtle gl-underline"
|
||||
:href="$options.privacyLink"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
v-text="content"
|
||||
></a>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="gl-relative">
|
||||
<div class="gl-absolute">
|
||||
<div
|
||||
v-safe-html="$options.gitlabLogo"
|
||||
aria-hidden="true"
|
||||
class="mr-experience-survey-logo"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<section v-if="step">
|
||||
<p id="mr_survey_question" ref="question" class="gl-m-0 gl-px-7">
|
||||
<gl-sprintf :message="step.question">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<satisfaction-rate
|
||||
aria-labelledby="mr_survey_question"
|
||||
class="gl-mt-5"
|
||||
@rate="onRate"
|
||||
/>
|
||||
</section>
|
||||
<section v-else class="gl-px-7">
|
||||
{{ $options.i18n.thanks }}
|
||||
</section>
|
||||
</div>
|
||||
</transition>
|
||||
</aside>
|
||||
</user-callout-dismisser>
|
||||
</template>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Tracker } from '~/tracking/tracker';
|
||||
|
||||
const MR_SURVEY_WAIT_DURATION = 10000;
|
||||
|
||||
const broadcastNotificationVisible = () => {
|
||||
// We don't want to clutter up the UI by displaying the survey when broadcast message(s)
|
||||
// are visible as well.
|
||||
return Boolean(document.querySelector('.js-broadcast-notification-message'));
|
||||
};
|
||||
|
||||
export const initMrExperienceSurvey = () => {
|
||||
if (!gon.features?.mrExperienceSurvey) return;
|
||||
if (!gon.current_user_id) return;
|
||||
if (!Tracker.enabled()) return;
|
||||
if (broadcastNotificationVisible()) return;
|
||||
|
||||
setTimeout(() => {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./app').then(({ startMrSurveyApp }) => {
|
||||
startMrSurveyApp();
|
||||
});
|
||||
}, MR_SURVEY_WAIT_DURATION);
|
||||
};
|
||||
|
|
@ -44,11 +44,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
edited() {
|
||||
const { createdAt, lastEditedAt } = this.updatedNote;
|
||||
|
||||
return new Date(createdAt).getTime() !== new Date(lastEditedAt).getTime();
|
||||
},
|
||||
editedText() {
|
||||
return __('Edited');
|
||||
},
|
||||
|
|
@ -111,7 +106,7 @@ export default {
|
|||
></div>
|
||||
|
||||
<note-edited-text
|
||||
v-if="edited"
|
||||
v-if="updatedNote.lastEditedBy"
|
||||
:edited-at="updatedNote.lastEditedAt"
|
||||
:edited-by="updatedNote.lastEditedBy"
|
||||
:action-text="editedText"
|
||||
|
|
|
|||
|
|
@ -272,30 +272,6 @@ $merge-request-sticky-header-height: 45px;
|
|||
}
|
||||
}
|
||||
|
||||
.mr-experience-survey-wrapper {
|
||||
// setting this explicitly because:
|
||||
// diff-files-holder has z-index 203
|
||||
// z-index 9999 utility class breaks tooltips
|
||||
z-index: 210;
|
||||
}
|
||||
|
||||
.mr-experience-survey-body {
|
||||
width: 300px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mr-experience-survey-legal {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.mr-experience-survey-logo {
|
||||
width: 16px;
|
||||
|
||||
svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// These are composite classes for use with Vue Transition
|
||||
// https://vuejs.org/guide/built-ins/transition
|
||||
.survey-slide-up-enter {
|
||||
|
|
|
|||
|
|
@ -17,12 +17,7 @@ module Admin
|
|||
end
|
||||
|
||||
def installation_service
|
||||
Integrations::SlackInstallation::InstanceService.new(
|
||||
current_user: current_user,
|
||||
params: params
|
||||
.permit(:code)
|
||||
.merge(organization_id: Current.organization.id)
|
||||
)
|
||||
Integrations::SlackInstallation::InstanceService.new(current_user: current_user, params: params.permit(:code))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
|
||||
|
||||
before_action only: [:show, :diffs, :rapid_diffs, :reports] do
|
||||
push_frontend_feature_flag(:mr_experience_survey, project)
|
||||
push_frontend_feature_flag(:mr_pipelines_graphql, project)
|
||||
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
|
||||
push_frontend_feature_flag(:mr_review_batch_submit, current_user)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Namespaces
|
||||
class RegenerateNewWorkItemEmailAddress < BaseMutation
|
||||
graphql_name 'NamespacesRegenerateNewWorkItemEmailAddress'
|
||||
include ResolvesNamespace
|
||||
|
||||
argument :full_path, GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Full path of the namespace to regenerate the new work item email address for.'
|
||||
|
||||
field :namespace,
|
||||
Types::NamespaceType,
|
||||
null: true,
|
||||
description: 'Namespace after regenerating the new work item email address.'
|
||||
|
||||
authorize :read_namespace
|
||||
|
||||
def resolve(full_path:)
|
||||
namespace = authorized_find!(full_path)
|
||||
|
||||
unless namespace.is_a?(::Namespaces::ProjectNamespace)
|
||||
return { namespace: nil, errors: [_('Work item creation via email is only supported for projects')] }
|
||||
end
|
||||
|
||||
unless Gitlab::Email::IncomingEmail.supports_work_item_creation?
|
||||
return { namespace: nil, errors: [_('Work item creation via email is not supported')] }
|
||||
end
|
||||
|
||||
return { namespace: namespace, errors: [] } if current_user.reset_incoming_email_token!
|
||||
|
||||
{ namespace: nil, errors: [_('Failed to regenerate new work item email address')] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path)
|
||||
resolve_namespace(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -222,6 +222,7 @@ module Types
|
|||
milestone: '17.7'
|
||||
}
|
||||
mount_mutation Mutations::Namespace::PackageSettings::Update
|
||||
mount_mutation Mutations::Namespaces::RegenerateNewWorkItemEmailAddress, experiment: { milestone: '18.2' }
|
||||
mount_mutation Mutations::Groups::Update
|
||||
mount_mutation Mutations::UserCallouts::Create
|
||||
mount_mutation Mutations::UserPreferences::Update
|
||||
|
|
|
|||
|
|
@ -304,7 +304,6 @@ module Integrations
|
|||
new_integration = integration.dup
|
||||
|
||||
new_integration.instance = false
|
||||
new_integration.organization_id = nil
|
||||
new_integration.project_id = project_id
|
||||
new_integration.group_id = group_id
|
||||
new_integration.inherit_from_id = integration.id if integration.inheritable?
|
||||
|
|
@ -514,13 +513,12 @@ module Integrations
|
|||
|
||||
validates :project_id, presence: true, unless: -> { instance_level? || group_level? }
|
||||
validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
|
||||
validates :project_id, :group, absence: true, if: -> { instance_level? }
|
||||
validates :organization_id, presence: true, if: -> { instance_level? }
|
||||
validates :project_id, :group_id, absence: true, if: -> { instance_level? }
|
||||
validates :type, presence: true, exclusion: BASE_CLASSES
|
||||
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
|
||||
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
|
||||
validates :type, uniqueness: { scope: :group_id }, if: :group_level?
|
||||
validate :validate_belongs_to_one_of_project_group_or_organization
|
||||
validate :validate_belongs_to_project_or_group
|
||||
|
||||
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
|
||||
scope :third_party_wikis, -> { where(category: 'third_party_wiki').active }
|
||||
|
|
@ -813,10 +811,10 @@ module Integrations
|
|||
|
||||
private
|
||||
|
||||
def validate_belongs_to_one_of_project_group_or_organization
|
||||
return if [group_id, project_id, organization_id].compact.one?
|
||||
def validate_belongs_to_project_or_group
|
||||
return unless project_level? && group_level?
|
||||
|
||||
errors.add(:base, 'The integration must belong to one organization, group, or project.')
|
||||
errors.add(:project_id, 'The integration cannot belong to both a project and a group')
|
||||
end
|
||||
|
||||
def validate_recipients?
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ module Users
|
|||
user_reached_limit_free_plan_alert: 51, # EE-only
|
||||
submit_license_usage_data_banner: 52, # EE-only
|
||||
personal_project_limitations_banner: 53, # EE-only
|
||||
mr_experience_survey: 54,
|
||||
# 54 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195384
|
||||
# 55 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
|
||||
namespace_storage_limit_alert_warning_threshold: 56, # EE-only
|
||||
namespace_storage_limit_alert_alert_threshold: 57, # EE-only
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ module Integrations
|
|||
end
|
||||
|
||||
def find_or_create_integration!
|
||||
GitlabSlackApplication
|
||||
.for_instance
|
||||
.first_or_create!(organization_id: params[:organization_id]) # rubocop:disable CodeReuse/ActiveRecord: -- legacy use
|
||||
GitlabSlackApplication.for_instance.first_or_create!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -111,7 +111,4 @@
|
|||
|
||||
#js-reviewer-drawer-portal
|
||||
|
||||
- if current_user && Feature.enabled?(:mr_experience_survey, current_user)
|
||||
#js-mr-experience-survey{ data: { account_age: current_user.account_age_in_days } }
|
||||
|
||||
= render 'shared/web_ide_path'
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: mr_experience_survey
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90036
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366561
|
||||
milestone: '15.2'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: log_database_sticking_operations
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/548158
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193945
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/548413
|
||||
milestone: '18.2'
|
||||
group: group::database frameworks
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -245,6 +245,10 @@ country_access_logs:
|
|||
- table: users
|
||||
column: user_id
|
||||
on_delete: async_delete
|
||||
dast_pre_scan_verification_steps:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
dast_pre_scan_verifications:
|
||||
- table: p_ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ feature_category: package_registry
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185223
|
||||
milestone: '17.11'
|
||||
queued_migration_version: 20250320085933
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20250616235836'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
migration_job_name: EncryptMissedCiRunnerTokens
|
||||
description: >
|
||||
We've encrypted plain tokens and migrated to token_encrypted (in milestone 11.6), so we need to remove the runner token column
|
||||
and make encryption required (currently optional). However, plain tokens remain unencrypted in gitlab_com and likely in self-managed instances.
|
||||
We must encrypt these remaining tokens and nullify them before removing the column completely.
|
||||
feature_category: fleet_visibility
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196265
|
||||
milestone: '18.2'
|
||||
queued_migration_version: 20250630163722
|
||||
finalized_by: # to be updated
|
||||
|
|
@ -8,14 +8,6 @@ description: Verification step status for DAST Profiles
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105702
|
||||
milestone: '15.7'
|
||||
gitlab_schema: gitlab_sec
|
||||
desired_sharding_key:
|
||||
project_id:
|
||||
references: projects
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: dast_pre_scan_verification_id
|
||||
table: dast_pre_scan_verifications
|
||||
sharding_key: project_id
|
||||
belongs_to: dast_pre_scan_verification
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
desired_sharding_key_migration_job_name: BackfillDastPreScanVerificationStepsProjectId
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ class AddMultiNotNullConstraintToIntegrations < Gitlab::Database::Migration[2.3]
|
|||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id, validate: false)
|
||||
# No-op to fix https://gitlab.com/gitlab-com/gl-infra/production/-/issues/20126
|
||||
end
|
||||
|
||||
def down
|
||||
remove_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id)
|
||||
# no-op to fix https://gitlab.com/gitlab-com/gl-infra/production/-/issues/20126
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDuoSettingsIndexToNamespaceSettings < Gitlab::Database::Migration[2.3]
|
||||
disable_ddl_transaction!
|
||||
milestone '18.2'
|
||||
|
||||
TABLE = :namespace_settings
|
||||
COLUMNS = [:duo_features_enabled, :lock_duo_features_enabled]
|
||||
INDEX = "index_namespace_settings_on_duo_features"
|
||||
|
||||
def up
|
||||
add_concurrent_index TABLE, COLUMNS,
|
||||
where: 'duo_features_enabled IS NOT NULL', include: :namespace_id,
|
||||
name: INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name TABLE, INDEX
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveNotNullIntegrationsConstraint < Gitlab::Database::Migration[2.3]
|
||||
disable_ddl_transaction!
|
||||
|
||||
milestone '18.2'
|
||||
|
||||
def up
|
||||
remove_check_constraint :integrations, 'check_2aae034509'
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeBackfillPackagesDebianFileMetadataProjectId < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.2'
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillPackagesDebianFileMetadataProjectId',
|
||||
table_name: :packages_debian_file_metadata,
|
||||
column_name: :package_file_id,
|
||||
job_arguments: [:project_id, :packages_package_files, :project_id, :package_file_id],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDastPreScanVerificationStepsProjectIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.2'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :dast_pre_scan_verification_steps, :project_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :dast_pre_scan_verification_steps, :project_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueEncryptMissedCiRunnerTokens < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.2'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
MIGRATION = "EncryptMissedCiRunnerTokens"
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:ci_runners,
|
||||
:id
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :ci_runners, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
f5e13cacb2c1c5c16064567b512406b2a2eec8fb30ed8ba9420da247af2700d3
|
||||
|
|
@ -0,0 +1 @@
|
|||
3726cd79a816694256fa6d449855d4515971b99e4fa0d59bdea410f49b712dfb
|
||||
|
|
@ -0,0 +1 @@
|
|||
b53bc6e953754f56cd7e7e4814fe569b3986330bebd04d17fc329d642e76a7f4
|
||||
|
|
@ -0,0 +1 @@
|
|||
debcbf90aec0669ce4914fe31b2f960584380520f4966df8dab09c039ae00745
|
||||
|
|
@ -0,0 +1 @@
|
|||
06b024ebde4a483d5bf01cc65ebb9f0c4e5f36ca5fbc55675d5ac84511e15c28
|
||||
|
|
@ -13352,6 +13352,7 @@ CREATE TABLE dast_pre_scan_verification_steps (
|
|||
verification_errors text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
check_type smallint DEFAULT 0 NOT NULL,
|
||||
project_id bigint,
|
||||
CONSTRAINT check_2cf67eeb54 CHECK ((project_id IS NOT NULL)),
|
||||
CONSTRAINT check_cd216b95e4 CHECK ((char_length(name) <= 255))
|
||||
);
|
||||
|
||||
|
|
@ -29777,9 +29778,6 @@ ALTER TABLE ONLY chat_teams
|
|||
ALTER TABLE workspaces
|
||||
ADD CONSTRAINT check_2a89035b04 CHECK ((personal_access_token_id IS NOT NULL)) NOT VALID;
|
||||
|
||||
ALTER TABLE integrations
|
||||
ADD CONSTRAINT check_2aae034509 CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID;
|
||||
|
||||
ALTER TABLE security_scans
|
||||
ADD CONSTRAINT check_2d56d882f6 CHECK ((project_id IS NOT NULL)) NOT VALID;
|
||||
|
||||
|
|
@ -36683,6 +36681,8 @@ CREATE UNIQUE INDEX index_namespace_import_users_on_user_id ON namespace_import_
|
|||
|
||||
CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_namespace_settings_on_duo_features ON namespace_settings USING btree (duo_features_enabled, lock_duo_features_enabled) INCLUDE (namespace_id) WHERE (duo_features_enabled IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_namespace_settings_on_namespace_id_where_archived_true ON namespace_settings USING btree (namespace_id) WHERE (archived = true);
|
||||
|
||||
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ title: Create your GitLab Dedicated instance
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
The instructions on this page guide you through the onboarding and initial setup of your GitLab Dedicated instance using [Switchboard](https://about.gitlab.com/direction/saas-platforms/switchboard/), the GitLab Dedicated portal.
|
||||
The instructions on this page guide you through the onboarding and initial setup of your GitLab Dedicated instance using [Switchboard](https://about.gitlab.com/direction/platforms/switchboard/), the GitLab Dedicated portal.
|
||||
|
||||
## Step 1: Get access to Switchboard
|
||||
|
||||
|
|
|
|||
|
|
@ -8849,6 +8849,30 @@ Input type: `NamespaceSettingsUpdateInput`
|
|||
| <a id="mutationnamespacesettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationnamespacesettingsupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
|
||||
|
||||
### `Mutation.namespacesRegenerateNewWorkItemEmailAddress`
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 18.2.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Input type: `NamespacesRegenerateNewWorkItemEmailAddressInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationnamespacesregeneratenewworkitememailaddressclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationnamespacesregeneratenewworkitememailaddressfullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace to regenerate the new work item email address for. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationnamespacesregeneratenewworkitememailaddressclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationnamespacesregeneratenewworkitememailaddresserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
|
||||
| <a id="mutationnamespacesregeneratenewworkitememailaddressnamespace"></a>`namespace` | [`Namespace`](#namespace) | Namespace after regenerating the new work item email address. |
|
||||
|
||||
### `Mutation.noteConvertToThread`
|
||||
|
||||
Convert a standard comment to a resolvable thread.
|
||||
|
|
@ -47551,7 +47575,6 @@ Name of the feature that the callout is for.
|
|||
| <a id="usercalloutfeaturenameenumgold_trial_billings"></a>`GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. |
|
||||
| <a id="usercalloutfeaturenameenumjoining_a_project_alert"></a>`JOINING_A_PROJECT_ALERT` | Callout feature name for joining_a_project_alert. |
|
||||
| <a id="usercalloutfeaturenameenummerge_request_dashboard_display_preferences_popover"></a>`MERGE_REQUEST_DASHBOARD_DISPLAY_PREFERENCES_POPOVER` | Callout feature name for merge_request_dashboard_display_preferences_popover. |
|
||||
| <a id="usercalloutfeaturenameenummr_experience_survey"></a>`MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. |
|
||||
| <a id="usercalloutfeaturenameenumnamespace_over_storage_users_combined_alert"></a>`NAMESPACE_OVER_STORAGE_USERS_COMBINED_ALERT` | Callout feature name for namespace_over_storage_users_combined_alert. |
|
||||
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_alert_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_ALERT_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_alert_threshold. |
|
||||
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_alert_error_threshold"></a>`NAMESPACE_STORAGE_LIMIT_ALERT_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_error_threshold. |
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ The following GitLab application features are not available:
|
|||
- Service Desk
|
||||
- Some GitLab Duo AI capabilities
|
||||
- View the [list of supported AI features](../../user/ai_features.md)
|
||||
- For more information, see the [Supporting AI Features on GitLab Dedicated](https://about.gitlab.com/direction/platforms/dedicated/#supporting-ai-features-on-gitlab-dedicated)
|
||||
- For more information, see [supporting AI Features on GitLab Dedicated](https://about.gitlab.com/direction/gitlab_dedicated/#supporting-ai-features-on-gitlab-dedicated)
|
||||
- Features other than [available features](#available-features) that must be configured outside of the GitLab user interface
|
||||
- Any functionality or feature behind a feature flag that is turned `off` by default
|
||||
- [Sigstore for keyless signing and verification](../../ci/yaml/signing_examples.md)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ detected are marked **Resolved**.
|
|||
|
||||
- You can assign a maximum of five rules to each policy.
|
||||
- You can assign a maximum of five vulnerability management policies to each security policy project.
|
||||
- When a secret detection scan finds that a previously detected secret key is no longer detected,
|
||||
the vulnerability is not auto-resolved. Instead, it remains in **Needs Triage** because the removed
|
||||
secret key has already been exposed. The vulnerability status should be manually resolved only after
|
||||
the secret key is revoked or rotated.
|
||||
|
||||
## Create a vulnerability management policy
|
||||
|
||||
|
|
|
|||
|
|
@ -69,14 +69,14 @@ push protection, to minimize the delay when pushing your commits and minimize th
|
|||
alerts. For example, personal access tokens that use a custom prefix are not detected by secret push protection.
|
||||
You can [exclude](../exclusions.md) selected secrets from detection by secret push protection.
|
||||
|
||||
## Enable secret push protection
|
||||
## Getting started
|
||||
|
||||
On GitLab Dedicated and GitLab Self-Managed instances, you must:
|
||||
|
||||
1. [Allow secret push protection on the entire instance](#allow-the-use-of-secret-push-protection-in-your-gitlab-instance).
|
||||
1. Enable secret push protection. You can either:
|
||||
- [Enable secret push protection in a specific project](#enable-secret-push-protection-in-a-project).
|
||||
- Use the API to [enable secret push protection for all projects in group](../../../../api/group_security_settings.md#update-secret_push_protection_enabled-setting). Ultimate only.
|
||||
- Use the API to [enable secret push protection for all projects in group](../../../../api/group_security_settings.md#update-secret_push_protection_enabled-setting).
|
||||
|
||||
### Allow the use of secret push protection in your GitLab instance
|
||||
|
||||
|
|
@ -149,6 +149,103 @@ Secret push protection does not check a file in a commit when:
|
|||
Secret push protection scans only the diffs of commits pushed over HTTP(S) and SSH.
|
||||
If a secret is already present in a file and not part of the changes, it is not detected.
|
||||
|
||||
## Understanding the results
|
||||
|
||||
Secret push protection can identify various categories of secrets:
|
||||
|
||||
- **API keys and tokens**: Service-specific authentication credentials
|
||||
- **Database connection strings**: URLs containing embedded credentials
|
||||
- **Private keys**: Cryptographic keys for authentication or encryption
|
||||
- **Generic high-entropy strings**: Patterns that appear to be randomly generated secrets
|
||||
|
||||
When a push is blocked, secret push protection provides detailed information to help you locate and address the detected secrets:
|
||||
|
||||
- **Commit ID**: The specific commit containing the secret. Useful for tracking changes in your Git history.
|
||||
- **File path and line number**: The exact location of the detected pattern for quick navigation.
|
||||
- **Secret type**: The classification of the detected pattern. For example, `GitLab Personal Access Token` or `AWS Access Key`.
|
||||
|
||||
### Common detection categories
|
||||
|
||||
Not all detections require immediate action. Consider the following when evaluating results:
|
||||
|
||||
- **True positives**: Legitimate secrets that should be rotated and removed. For example:
|
||||
- [Valid](../../vulnerabilities/validity_check.md) API keys or tokens
|
||||
- Production database credentials
|
||||
- Private cryptographic keys
|
||||
- Any credentials that could grant unauthorized access
|
||||
|
||||
- **False positives**: Detected patterns that aren't actual secrets. For example:
|
||||
- Test data that resembles secrets but has no real-world value
|
||||
- Placeholder values in configuration templates
|
||||
- Example credentials in documentation
|
||||
- Hash values or checksums that match secret patterns
|
||||
|
||||
Document common false positive patterns in your organization to streamline future evaluations.
|
||||
|
||||
## Optimization
|
||||
|
||||
Before deploying secret push protection widely, optimize the configuration to reduce false positives and improve accuracy for your specific environment.
|
||||
|
||||
### Reduce false positives
|
||||
|
||||
False positives can significantly impact developer productivity and lead to security fatigue.
|
||||
|
||||
To reduce false positives:
|
||||
|
||||
- Configure [exclusions](../exclusions.md) strategically:
|
||||
- Create path-based exclusions for test directories, documentation, and third party dependencies.
|
||||
- Use pattern-based exclusions for known false positive patterns specific to your codebase.
|
||||
- Document your exclusion rules and review them regularly.
|
||||
- Create standards for placeholder values and test credentials.
|
||||
- Monitor false positive rates and continue to adjust exclusions accordingly.
|
||||
|
||||
### Optimize performance
|
||||
|
||||
Large repositories or frequent pushes can have performance impacts.
|
||||
|
||||
To optimize the performance of secret push protection:
|
||||
|
||||
- Monitor push times and establish baseline metrics before deployment.
|
||||
- Use diff scanning to reduce the amount of content scanned on each push.
|
||||
- Consider file size limits for repositories with large binary assets.
|
||||
- Implement exclusions for directories that are unlikely to contain secrets.
|
||||
|
||||
### Integration with existing workflows
|
||||
|
||||
Ensure secret push protection complements your existing development practices:
|
||||
|
||||
- Configure pipeline secret detection and secret push protection to be sure you have defense in depth.
|
||||
- Update developer documentation to include secret push protection procedures.
|
||||
- Align with security training to educate developers on secure coding practices to minimize leaked secrets.
|
||||
|
||||
## Roll out
|
||||
|
||||
Successfully deploying secret push protection at scale requires careful planning and a phased implementation:
|
||||
|
||||
1. Choose two or three non-critical projects with active development to test the feature and understand its impact on developer workflows.
|
||||
1. Turn on secret push protection for your selected test projects and monitor developer feedback.
|
||||
1. Document processes for handling blocked pushes and train your development teams on the new workflows.
|
||||
1. Track the number of secrets detected, false positive rates, and developer experience feedback during the pilot phase.
|
||||
|
||||
You should run the pilot phase for two to four weeks to gather sufficient data and identify any workflow adjustments needed before broader deployment.
|
||||
|
||||
Once you have completed the pilot, consider the next three phases for a scaled rollout:
|
||||
|
||||
1. Early adopters (weeks 3-6)
|
||||
- Enable on 10-20% of active projects, prioritizing security-sensitive repositories.
|
||||
- Focus on teams with strong security awareness and buy-in.
|
||||
- Monitor performance impacts and developer experience.
|
||||
- Refine processes based on real-world usage.
|
||||
1. Broad deployment (weeks 7-12)
|
||||
- Gradually enable across remaining projects in batches.
|
||||
- Provide ongoing support and training to development teams.
|
||||
- Monitor system performance and scale infrastructure if needed.
|
||||
- Continue optimizing exclusion rules based on usage patterns.
|
||||
1. Full coverage (weeks 13-16)
|
||||
- Enable secret push protection on all remaining projects.
|
||||
- Establish ongoing maintenance and review processes.
|
||||
- Implement regular audits of exclusion rules and detected patterns.
|
||||
|
||||
## Resolve a blocked push
|
||||
|
||||
When secret push protection blocks a push, you can either:
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ To configure approval rules:
|
|||
|
||||
You can also configure:
|
||||
|
||||
- Additional [merge request approval settings](settings.md) for more control of the
|
||||
- More [merge request approval settings](settings.md) for more control of the
|
||||
level of oversight and security your project needs.
|
||||
- Merge request approval rules with the
|
||||
[Merge request approvals API](../../../../api/merge_request_approvals.md).
|
||||
|
|
@ -86,8 +86,8 @@ Use cases include:
|
|||
|
||||
{{< /history >}}
|
||||
|
||||
You can see the approval status of a merge request in two places. On the [merge request itself](#for-a-single-merge-request)
|
||||
and in the [list of merge requests](#in-the-list-of-merge-requests) for your project or group.
|
||||
To view the approval status of a merge request, check the merge request itself, or the list of
|
||||
merge requests for your project or group.
|
||||
|
||||
### For a single merge request
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ To see the review and approval status for each reviewer:
|
|||
1. Open the merge request.
|
||||
1. Check the right sidebar.
|
||||
|
||||
Each reviewer's status is shown next to the their name.
|
||||
Each reviewer's status is shown next to their name.
|
||||
|
||||
- {{< icon name="dash-circle" >}} Awaiting review
|
||||
- {{< icon name="status_running" >}} Review in progress
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ This user has:
|
|||
- 8 active merge requests ({{< icon name="merge-request-open" >}})
|
||||
- 29 to-do items ({{< icon name="todo-done" >}})
|
||||
|
||||
Your merge request homepage shows more information about these merge requests. To see it, use any of these methods:
|
||||
Your merge request homepage shows more information about these merge requests. To see it,
|
||||
use any of these methods:
|
||||
|
||||
- Use the <kbd>Shift</kbd> + <kbd>m</kbd> [keyboard shortcut](../../shortcuts.md).
|
||||
- On the left sidebar, select **Merge requests** ({{< icon name="merge-request-open">}}).
|
||||
|
|
@ -106,12 +107,13 @@ into three tabs:
|
|||
|
||||
### Set your display preferences
|
||||
|
||||
On the top right of your merge request homepage, select **Display preferences** ({{< icon name="preferences">}})
|
||||
to sort your merge requests by:
|
||||
On the top right of your merge request homepage, select **Display preferences** ({{< icon name="preferences">}}):
|
||||
|
||||
- **Workflow**. This view groups merge requests by their status. GitLab shows the merge requests needing your attention
|
||||
first, regardless of whether you are the author or the reviewer.
|
||||
- **Role**. This view groups merge requests by whether you are the reviewer or the author.
|
||||
- Toggle **Show labels** to show or hide labels for each merge request.
|
||||
- Sorting preferences: **Workflow** or **Role**.
|
||||
- **Workflow** groups merge requests by their status. GitLab shows the merge requests
|
||||
needing your attention first, regardless of whether you are the author or the reviewer.
|
||||
- **Role** groups merge requests by whether you are the reviewer or the author.
|
||||
|
||||
Active merge requests count toward the total shown on the left sidebar. GitLab excludes **Inactive**
|
||||
merge requests from your review count.
|
||||
|
|
@ -124,12 +126,13 @@ These merge requests need your attention. They count toward the total shown on t
|
|||
Statuses:
|
||||
- **Draft**: The merge request is a draft.
|
||||
- **Reviewers needed**: The merge request is not a draft, but has no reviewers.
|
||||
- **Review requested**: You're a reviewer. Review the merge request. Provide feedback. Optionally, approve or request changes.
|
||||
Statuses:
|
||||
- **Changes requested**: A reviewer has requested changes.
|
||||
- **Review requested**: You're a reviewer. Review the merge request. Provide feedback. Optionally,
|
||||
approve or request changes. Statuses:
|
||||
- **Changes requested**: A reviewer has requested changes. The change request blocks the merge request,
|
||||
but [can be bypassed](reviews/_index.md#bypass-a-request-for-changes).
|
||||
- **Reviewer commented**: A reviewer has left comments but not requested changes.
|
||||
- **Returned to you**: Reviewers have provided feedback, or requested changes. Address reviewer comments, and implement suggested changes.
|
||||
Statuses:
|
||||
- **Returned to you**: Reviewers have provided feedback, or requested changes. Address reviewer comments,
|
||||
and apply suggested changes. Statuses:
|
||||
- **Changes requested**: A reviewer has requested changes.
|
||||
- **Reviewer commented**: A reviewer has left comments but not requested changes.
|
||||
|
||||
|
|
@ -137,12 +140,12 @@ These merge requests need your attention. They count toward the total shown on t
|
|||
|
||||
GitLab excludes these merge requests from the active count, because no action is required from you right now:
|
||||
|
||||
- **Waiting for assignee**: If you're the author, the merge request is awaiting review. If you're the reviewer, you've requested changes.
|
||||
Statuses:
|
||||
- **Waiting for assignee**: If you're the author, the merge request is awaiting review. If you're
|
||||
the reviewer, you've requested changes. Statuses:
|
||||
- **You requested changes**: You've completed your review and requested changes.
|
||||
- **You commented**: You've commented, but have not completed your review.
|
||||
- **Waiting for approvals**: Your assigned merge requests that are waiting for approvals, and reviews you have requested changes for.
|
||||
Statuses:
|
||||
- **Waiting for approvals**: Your assigned merge requests that are waiting for approvals, and reviews
|
||||
you have requested changes for. Statuses:
|
||||
- **Approvals required**: Number of required approvals remaining.
|
||||
- **Approved**: Either you have approved, or all required approvals are satisfied.
|
||||
- **Waiting for approvals**.
|
||||
|
|
@ -150,8 +153,8 @@ GitLab excludes these merge requests from the active count, because no action is
|
|||
Statuses:
|
||||
- **Approved**: You've approved, and required approvals are satisfied.
|
||||
- **Approval required**: You've approved, but not all required approvals are satisfied.
|
||||
- **Approved by others**: Merge requests that have received approvals from other team members. Potentially ready to merge, if all requirements are met.
|
||||
Statuses:
|
||||
- **Approved by others**: Merge requests that have received approvals from other team members.
|
||||
Potentially ready to merge, if all requirements are met. Statuses:
|
||||
- **Approved**: Your merge request has received the necessary approvals.
|
||||
|
||||
### Role view
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ module API
|
|||
def find_namespace(id)
|
||||
if INTEGER_ID_REGEX.match?(id.to_s)
|
||||
# We need to stick to an up-to-date replica or primary db here in order to properly observe the namespace
|
||||
# recently created by GitlabSubscriptions::Trials::CreateService#create_group_flow.
|
||||
# recently created by GitlabSubscriptions::Trials::UltimateCreateService.
|
||||
# See https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/9808
|
||||
::Namespace.sticking.find_caught_up_replica(:namespace, id)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class EncryptMissedCiRunnerTokens < BatchedMigrationJob
|
||||
operation_name :encrypt_missed_ci_runner_tokens
|
||||
feature_category :fleet_visibility
|
||||
|
||||
def perform
|
||||
each_sub_batch(
|
||||
batching_scope: ->(relation) { relation.where.not(token: nil).where(token_encrypted: nil) }
|
||||
) do |runners_to_encrypt|
|
||||
token_encrypted_cases = []
|
||||
runner_ids = []
|
||||
|
||||
runners_to_encrypt.pluck(:id, :token).each do |id, token|
|
||||
encrypted_token = encode(token)
|
||||
|
||||
token_encrypted_cases <<
|
||||
"WHEN id = #{connection.quote(id)} THEN #{connection.quote(encrypted_token)}"
|
||||
runner_ids << connection.quote(id)
|
||||
end
|
||||
|
||||
next if token_encrypted_cases.empty?
|
||||
|
||||
connection.execute(
|
||||
<<~SQL
|
||||
UPDATE ci_runners
|
||||
SET token_encrypted = CASE
|
||||
#{token_encrypted_cases.join(' ')}
|
||||
END,
|
||||
token = NULL
|
||||
WHERE id IN (#{runner_ids.join(',')})
|
||||
SQL
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encode(token)
|
||||
Authn::TokenField::EncryptionHelper.encrypt_token(token)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -47,6 +47,7 @@ module Gitlab
|
|||
def stick(namespace, id)
|
||||
with_primary_write_location do |location|
|
||||
set_write_location_for(namespace, id, location)
|
||||
capture_stick_logs(namespace, id, location)
|
||||
end
|
||||
use_primary!
|
||||
end
|
||||
|
|
@ -56,13 +57,31 @@ module Gitlab
|
|||
ids.each do |id|
|
||||
set_write_location_for(namespace, id, location)
|
||||
end
|
||||
capture_stick_logs(namespace, ids, location)
|
||||
end
|
||||
|
||||
use_primary!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_database_sticking_operations_enabled?
|
||||
Feature.enabled?(:log_database_sticking_operations, Feature.current_pod)
|
||||
end
|
||||
|
||||
def capture_stick_logs(namespace, ids, location)
|
||||
return unless log_database_sticking_operations_enabled?
|
||||
return unless namespace.to_sym == :user
|
||||
|
||||
id = Array(ids).first # Only log the first ID to reduce log volume for bulk operations
|
||||
|
||||
::Gitlab::Database::LoadBalancing::Logger.info(
|
||||
event: :load_balancer_stick_logging,
|
||||
client_id: "#{namespace}/#{id}",
|
||||
stick_id: id,
|
||||
stick_type: namespace,
|
||||
current_lsn: location)
|
||||
end
|
||||
|
||||
def with_primary_write_location
|
||||
# When only using the primary, there's no point in getting write
|
||||
# locations, as the primary is always in sync with itself.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ module Gitlab
|
|||
enabled? && supports_wildcard?
|
||||
end
|
||||
|
||||
alias_method :supports_work_item_creation?, :supports_issue_creation?
|
||||
|
||||
def reply_address(key)
|
||||
incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, key)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3530,9 +3530,6 @@ msgstr ""
|
|||
msgid "Activate Service Desk"
|
||||
msgstr ""
|
||||
|
||||
msgid "Activate my trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Activated"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26623,6 +26620,9 @@ msgstr ""
|
|||
msgid "Failed to refresh compliance requirement status. Error: %{error_message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to regenerate new work item email address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to regenerate unique domain"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40391,21 +40391,6 @@ msgstr ""
|
|||
msgid "MrReports|Reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "MrSurvey|By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the %{linkStart}GitLab Privacy Policy%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "MrSurvey|How satisfied are you with %{strongStart}speed/performance%{strongEnd} of merge requests?"
|
||||
msgstr ""
|
||||
|
||||
msgid "MrSurvey|Merge request experience survey"
|
||||
msgstr ""
|
||||
|
||||
msgid "MrSurvey|Overall, how satisfied are you with merge requests?"
|
||||
msgstr ""
|
||||
|
||||
msgid "MrSurvey|Thank you for your feedback!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multi-project"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -61448,21 +61433,6 @@ msgstr ""
|
|||
msgid "Support page URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Surveys|Delighted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Surveys|Happy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Surveys|Neutral"
|
||||
msgstr ""
|
||||
|
||||
msgid "Surveys|Sad"
|
||||
msgstr ""
|
||||
|
||||
msgid "Surveys|Unhappy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch Branches"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -65613,24 +65583,12 @@ msgstr ""
|
|||
msgid "TrialWidget|Your trial of Ultimate with GitLab Duo Enterprise has ended"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trials|Create a new group and start your free trial."
|
||||
msgstr ""
|
||||
|
||||
msgid "Trials|You can apply your free trial to a group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Activate my trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Allowed characters: +, 0-9, -, and spaces."
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Apply your trial to a new group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Apply your trial to a new or existing group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|By clicking \"%{buttonText}\" you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -65661,9 +65619,6 @@ msgstr ""
|
|||
msgid "Trial|Start your free trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Start your free trial on %{group_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|State or province"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -70306,6 +70261,12 @@ msgstr ""
|
|||
msgid "Work item"
|
||||
msgstr ""
|
||||
|
||||
msgid "Work item creation via email is not supported"
|
||||
msgstr ""
|
||||
|
||||
msgid "Work item creation via email is only supported for projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Work item not supported"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ gem 'zeitwerk', '~> 2.7', '>= 2.7.3'
|
|||
gem 'influxdb-client', '~> 3.2'
|
||||
gem 'terminal-table', '~> 4.0.0', require: false
|
||||
gem 'slack-notifier', '~> 2.4', require: false
|
||||
gem 'googleauth', '~> 1.9.0', require: false # see: https://gitlab.com/gitlab-org/gitlab/-/issues/449019
|
||||
gem 'fog-google', '~> 1.25', require: false
|
||||
gem "warning", "~> 1.5"
|
||||
|
||||
|
|
|
|||
|
|
@ -175,11 +175,14 @@ GEM
|
|||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.40.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-env (2.1.1)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
googleauth (1.9.2)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.14.0)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
|
|
@ -386,7 +389,6 @@ DEPENDENCIES
|
|||
gitlab-qa (~> 15, >= 15.5.0)
|
||||
gitlab-utils!
|
||||
gitlab_quality-test_tooling (~> 2.15.3)
|
||||
googleauth (~> 1.9.0)
|
||||
influxdb-client (~> 3.2)
|
||||
junit_merge (~> 0.1.2)
|
||||
knapsack (~> 4.0)
|
||||
|
|
|
|||
|
|
@ -97,11 +97,11 @@ module QA
|
|||
click_element 'expand-replies-button'
|
||||
end
|
||||
|
||||
def has_comment?(comment_text)
|
||||
def has_comment?(comment_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
|
||||
has_element?(
|
||||
'noteable-note-container',
|
||||
text: comment_text,
|
||||
wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME
|
||||
wait: wait
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -167,13 +167,13 @@ module QA
|
|||
def start_review_with_comment(text)
|
||||
fill_editor_element('comment-field', text)
|
||||
click_element('start-review-button')
|
||||
has_comment?(text)
|
||||
has_comment?(text, 3)
|
||||
end
|
||||
|
||||
def add_comment_to_review(text)
|
||||
fill_editor_element('comment-field', text)
|
||||
click_element('add-to-review-button')
|
||||
has_comment?(text)
|
||||
has_comment?(text, 3)
|
||||
end
|
||||
|
||||
def toggle_comments(position)
|
||||
|
|
|
|||
|
|
@ -180,8 +180,14 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
all_elements('review-drawer-toggle', minimum: 1).first.click
|
||||
click_element('submit-review-button')
|
||||
retry_until(sleep_interval: 2, max_attempts: 5, message: "Retry closing review drawer") do
|
||||
if has_element?('review-drawer-toggle', wait: 2)
|
||||
all_elements('review-drawer-toggle', minimum: 1).first.click
|
||||
end
|
||||
|
||||
click_element('submit-review-button')
|
||||
has_no_element?('review-drawer-toggle', wait: 2)
|
||||
end
|
||||
|
||||
# After clicking the button, wait for the review bar to disappear
|
||||
# before moving on to the next part of the test
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ module QA
|
|||
HTTP_STATUS_NOT_FOUND = 404
|
||||
HTTP_STATUS_TOO_MANY_REQUESTS = 429
|
||||
HTTP_STATUS_SERVER_ERROR = 500
|
||||
HTTP_STATUS_SERVICE_UNAVAILABLE = 503
|
||||
|
||||
def post(url, payload, args = {})
|
||||
request_args = {
|
||||
|
|
@ -197,7 +198,10 @@ module QA
|
|||
end
|
||||
|
||||
def fatal_response?(response_code)
|
||||
[HTTP_STATUS_UNAUTHORIZED, HTTP_STATUS_SERVER_ERROR, HTTP_STATUS_BAD_REQUEST].include?(response_code)
|
||||
[HTTP_STATUS_UNAUTHORIZED,
|
||||
HTTP_STATUS_SERVER_ERROR,
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
HTTP_STATUS_SERVICE_UNAVAILABLE].include?(response_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe Admin::IntegrationsController, :with_current_organization, featur
|
|||
end
|
||||
|
||||
it_behaves_like Integrations::Actions do
|
||||
let(:integration_attributes) { { instance: true, project: nil, organization: organization } }
|
||||
let(:integration_attributes) { { instance: true, project: nil } }
|
||||
|
||||
let(:routing_params) do
|
||||
{ id: integration.to_param }
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration",
|
|||
end
|
||||
|
||||
context 'when the integration is active for the instance', :enable_admin_mode do
|
||||
let(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let(:instance_integration) { create :beyond_identity_integration }
|
||||
|
||||
before do
|
||||
::Integrations::PropagateService.new(instance_integration).execute
|
||||
|
|
@ -126,7 +126,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration",
|
|||
end
|
||||
|
||||
context 'and the integration is activated for the instance' do
|
||||
let(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let(:instance_integration) { create :beyond_identity_integration }
|
||||
|
||||
before do
|
||||
::Integrations::PropagateService.new(instance_integration).execute
|
||||
|
|
@ -144,7 +144,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration",
|
|||
it { expect(project.reload.beyond_identity_integration).not_to be_activated }
|
||||
|
||||
context 'and the integration is activated for the instance' do
|
||||
let(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let(:instance_integration) { create :beyond_identity_integration }
|
||||
|
||||
before do
|
||||
::Integrations::PropagateService.new(instance_integration).execute
|
||||
|
|
|
|||
|
|
@ -75,12 +75,18 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js, feature_category: :markdown d
|
|||
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
it 'includes mermaid frame correctly', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/553529' do
|
||||
it 'includes mermaid frame correctly' do
|
||||
visit(project_path(project))
|
||||
|
||||
wait_for_requests
|
||||
wait_for_all_requests
|
||||
|
||||
expect(page.html).to include(expected)
|
||||
page.within '.js-wiki-content' do
|
||||
# the find is needed to ensure the lazy container is loaded, otherwise
|
||||
# it can be a flaky test, similar to
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408
|
||||
#
|
||||
expect(page.html).to include(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ RSpec.describe 'User Settings > GPG keys', feature_category: :user_profile do
|
|||
end
|
||||
|
||||
context 'when external verification is required' do
|
||||
let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:beyond_identity_integration) { create(:beyond_identity_integration) }
|
||||
let!(:gpg_key) do
|
||||
create :gpg_key, externally_verified: externally_verified, user: user, key: GpgHelpers::User2.public_key
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { programmingLanguages } from 'jest/groups_projects/components/mock_data'
|
|||
import {
|
||||
FILTERED_SEARCH_TOKEN_LANGUAGE,
|
||||
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
PAGINATION_TYPE_KEYSET,
|
||||
} from '~/groups_projects/constants';
|
||||
import {
|
||||
|
|
@ -45,6 +46,7 @@ describe('AdminProjectsApp', () => {
|
|||
filteredSearchSupportedTokens: [
|
||||
FILTERED_SEARCH_TOKEN_LANGUAGE,
|
||||
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
],
|
||||
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
|
||||
filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
SORT_DIRECTION_ASC,
|
||||
PAGINATION_TYPE_KEYSET,
|
||||
PAGINATION_TYPE_OFFSET,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
} from '~/groups_projects/constants';
|
||||
import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_searches_storage_keys';
|
||||
import {
|
||||
|
|
@ -50,6 +51,7 @@ import {
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
|
||||
import { VISIBILITY_LEVEL_PRIVATE_STRING } from '~/visibility_level/constants';
|
||||
import { programmingLanguages } from './mock_data';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
|
@ -67,6 +69,7 @@ const defaultPropsData = {
|
|||
filteredSearchSupportedTokens: [
|
||||
FILTERED_SEARCH_TOKEN_LANGUAGE,
|
||||
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
|
||||
FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL,
|
||||
],
|
||||
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,
|
||||
filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE,
|
||||
|
|
@ -271,6 +274,7 @@ describe('TabsWithList', () => {
|
|||
[defaultPropsData.filteredSearchTermKey]: searchTerm,
|
||||
[FILTERED_SEARCH_TOKEN_LANGUAGE]: ['5'],
|
||||
[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: ['50'],
|
||||
[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: [VISIBILITY_LEVEL_PRIVATE_STRING],
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
|
@ -279,6 +283,7 @@ describe('TabsWithList', () => {
|
|||
expect(successHandler).toHaveBeenCalledWith({
|
||||
minAccessLevel: 'OWNER',
|
||||
programmingLanguageName: 'CSS',
|
||||
visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING,
|
||||
search: searchTerm,
|
||||
skipContributed: false,
|
||||
skipStarred: true,
|
||||
|
|
@ -553,6 +558,7 @@ describe('TabsWithList', () => {
|
|||
[defaultPropsData.filteredSearchTermKey]: 'foo',
|
||||
[FILTERED_SEARCH_TOKEN_LANGUAGE]: '8',
|
||||
[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: ACCESS_LEVEL_OWNER_INTEGER,
|
||||
[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PRIVATE_STRING,
|
||||
[QUERY_PARAM_END_CURSOR]: mockEndCursor,
|
||||
[QUERY_PARAM_START_CURSOR]: mockStartCursor,
|
||||
};
|
||||
|
|
@ -578,6 +584,7 @@ describe('TabsWithList', () => {
|
|||
[defaultPropsData.filteredSearchTermKey]: query[defaultPropsData.filteredSearchTermKey],
|
||||
[FILTERED_SEARCH_TOKEN_LANGUAGE]: query[FILTERED_SEARCH_TOKEN_LANGUAGE],
|
||||
[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: query[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL],
|
||||
[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: query[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL],
|
||||
},
|
||||
filtersAsQueryVariables: {
|
||||
programmingLanguageName: 'CoffeeScript',
|
||||
|
|
|
|||
|
|
@ -3,14 +3,7 @@ import Vue, { nextTick } from 'vue';
|
|||
import Vuex from 'vuex';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import {
|
||||
GlAvatarLabeled,
|
||||
GlBadge,
|
||||
GlEmptyState,
|
||||
GlKeysetPagination,
|
||||
GlLoadingIcon,
|
||||
GlTable,
|
||||
} from '@gitlab/ui';
|
||||
import { GlAvatarLabeled, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTable } from '@gitlab/ui';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
|
|
@ -30,6 +23,7 @@ import {
|
|||
} from '~/import_entities/import_groups/constants';
|
||||
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import EmptyResult from '~/vue_shared/components/empty_result.vue';
|
||||
|
||||
import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql';
|
||||
|
||||
|
|
@ -93,7 +87,7 @@ describe('PlaceholdersTable', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
const findEmptyState = () => wrapper.findComponent(EmptyResult);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
|
|
@ -363,9 +357,8 @@ describe('PlaceholdersTable', () => {
|
|||
});
|
||||
|
||||
it('renders empty state with short query description', () => {
|
||||
expect(findEmptyState().props('description')).toBe(
|
||||
'Enter at least three characters to search.',
|
||||
);
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
expect(findEmptyState().props('searchMinimumLength')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -382,7 +375,8 @@ describe('PlaceholdersTable', () => {
|
|||
});
|
||||
|
||||
it('renders empty state with no results message', () => {
|
||||
expect(findEmptyState().props('description')).toBe('Edit your search and try again');
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
expect(findEmptyState().props('searchMinimumLength')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlButton, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
|
||||
import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue';
|
||||
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
|
||||
|
||||
const createRenderTrackedArguments = () => [
|
||||
undefined,
|
||||
'survey:mr_experience',
|
||||
{
|
||||
label: 'render',
|
||||
extra: {
|
||||
accountAge: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('MergeRequestExperienceSurveyApp', () => {
|
||||
let trackingSpy;
|
||||
let wrapper;
|
||||
let dismiss;
|
||||
let dismisserComponent;
|
||||
|
||||
const findCloseButton = () =>
|
||||
wrapper
|
||||
.findAllComponents(GlButton)
|
||||
.filter((button) => button.attributes('aria-label') === 'Close')
|
||||
.at(0);
|
||||
|
||||
const createWrapper = ({ shouldShowCallout = true } = {}) => {
|
||||
dismiss = jest.fn();
|
||||
dismisserComponent = makeMockUserCalloutDismisser({
|
||||
dismiss,
|
||||
shouldShowCallout,
|
||||
});
|
||||
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
|
||||
wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, {
|
||||
propsData: {
|
||||
accountAge: 0,
|
||||
},
|
||||
stubs: {
|
||||
UserCalloutDismisser: dismisserComponent,
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('when user callout is visible', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('shows survey', () => {
|
||||
expect(wrapper.html()).toContain('Overall, how satisfied are you with merge requests?');
|
||||
expect(wrapper.findComponent(SatisfactionRate).exists()).toBe(true);
|
||||
expect(wrapper.emitted().close).toBe(undefined);
|
||||
});
|
||||
|
||||
it('tracks render once', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
|
||||
});
|
||||
|
||||
it("doesn't track subsequent renders", () => {
|
||||
createWrapper();
|
||||
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('when close button clicked', () => {
|
||||
beforeEach(() => {
|
||||
findCloseButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('triggers user callout on close', () => {
|
||||
expect(dismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits close event on close button click', () => {
|
||||
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
|
||||
});
|
||||
|
||||
it('tracks dismissal', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
|
||||
label: 'dismiss',
|
||||
extra: {
|
||||
accountAge: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks subsequent renders', () => {
|
||||
createWrapper();
|
||||
expect(trackingSpy.mock.calls).toEqual([
|
||||
createRenderTrackedArguments(),
|
||||
expect.anything(),
|
||||
createRenderTrackedArguments(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('applies correct feature name for user callout', () => {
|
||||
expect(wrapper.findComponent(dismisserComponent).props('featureName')).toBe(
|
||||
'mr_experience_survey',
|
||||
);
|
||||
});
|
||||
|
||||
it('dismisses user callout on survey rate', () => {
|
||||
const rate = wrapper.findComponent(SatisfactionRate);
|
||||
expect(dismiss).not.toHaveBeenCalled();
|
||||
rate.vm.$emit('rate', 5);
|
||||
expect(dismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('steps through survey steps', async () => {
|
||||
const rate = wrapper.findComponent(SatisfactionRate);
|
||||
rate.vm.$emit('rate', 5);
|
||||
await nextTick();
|
||||
expect(wrapper.text()).toContain(
|
||||
'How satisfied are you with speed/performance of merge requests?',
|
||||
);
|
||||
});
|
||||
|
||||
it('tracks survey rates', () => {
|
||||
const rate = wrapper.findComponent(SatisfactionRate);
|
||||
rate.vm.$emit('rate', 5);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
|
||||
value: 5,
|
||||
label: 'overall',
|
||||
extra: {
|
||||
accountAge: 0,
|
||||
},
|
||||
});
|
||||
rate.vm.$emit('rate', 4);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
|
||||
value: 4,
|
||||
label: 'performance',
|
||||
extra: {
|
||||
accountAge: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows legal note', () => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides legal note after first step', async () => {
|
||||
const rate = wrapper.findComponent(SatisfactionRate);
|
||||
rate.vm.$emit('rate', 5);
|
||||
await nextTick();
|
||||
expect(wrapper.text()).not.toContain(
|
||||
'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows disappearing thanks message', async () => {
|
||||
const rate = wrapper.findComponent(SatisfactionRate);
|
||||
rate.vm.$emit('rate', 5);
|
||||
await nextTick();
|
||||
rate.vm.$emit('rate', 5);
|
||||
await nextTick();
|
||||
expect(wrapper.text()).toContain('Thank you for your feedback!');
|
||||
expect(wrapper.emitted()).toMatchObject({});
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user callout is hidden', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ shouldShowCallout: false });
|
||||
});
|
||||
|
||||
it('emits close event', () => {
|
||||
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
|
||||
});
|
||||
|
||||
it("doesn't track anything", () => {
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when Escape key is pressed', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
const event = new KeyboardEvent('keyup', { key: 'Escape' });
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
|
||||
it('emits close event', () => {
|
||||
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
|
||||
expect(dismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('tracks dismissal', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
|
||||
label: 'dismiss',
|
||||
extra: {
|
||||
accountAge: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -42,16 +42,16 @@ describe('NoteBody', () => {
|
|||
expect(renderGFM).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not render "Edited" text when lastEdited is the same as createdAt', () => {
|
||||
it('should not render "Edited" text when lastEditedBy is not present', () => {
|
||||
const editedComponent = wrapper.findComponent(NoteEditedText);
|
||||
|
||||
expect(editedComponent.exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render "Edited" text when lastEditedAt is not the same as createdAt', () => {
|
||||
it('should render "Edited" text when lastEditedBy is present', () => {
|
||||
// remounting to trigger mounted function
|
||||
wrapper = createWrapper({
|
||||
note: { ...note, lastEditedAt: '2024-11-11T08:11:34Z' },
|
||||
note: { ...note, lastEditedBy: { name: 'user', webPath: '/user' } },
|
||||
noteableId,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1917,7 +1917,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
it { is_expected.to be_falsey }
|
||||
|
||||
context 'when beyond identity is disabled for a project' do
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) }
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, active: false) }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:beyond_identity_integration).and_return(integration)
|
||||
|
|
@ -1927,7 +1927,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
end
|
||||
|
||||
context 'when a GPG key failed external validation and one GPC key is externally validated' do
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:integration) { create(:beyond_identity_integration) }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:beyond_identity_integration).and_return(integration)
|
||||
|
|
@ -1939,7 +1939,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
end
|
||||
|
||||
context 'when there are no GPG keys externally validated' do
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:integration) { create(:beyond_identity_integration) }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:beyond_identity_integration).and_return(integration)
|
||||
|
|
@ -1951,7 +1951,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
end
|
||||
|
||||
context 'when GPG keys are missing' do
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:integration) { create(:beyond_identity_integration) }
|
||||
|
||||
before do
|
||||
allow(project).to receive(:beyond_identity_integration).and_return(integration)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::EncryptMissedCiRunnerTokens,
|
||||
migration: :gitlab_ci, feature_category: :fleet_visibility do
|
||||
let(:connection) { Ci::ApplicationRecord.connection }
|
||||
|
||||
describe '#perform' do
|
||||
let(:runners) { table(:ci_runners, primary_key: :id) }
|
||||
|
||||
let(:args) do
|
||||
min, max = runners.pick('MIN(id)', 'MAX(id)')
|
||||
{
|
||||
start_id: min,
|
||||
end_id: max,
|
||||
batch_table: 'ci_runners',
|
||||
batch_column: 'id',
|
||||
sub_batch_size: 100,
|
||||
pause_ms: 0,
|
||||
connection: connection
|
||||
}
|
||||
end
|
||||
|
||||
let!(:runner_with_plain_token) { runners.create!(runner_type: 1, token: 'plain_token') }
|
||||
let!(:runner_without_token) { runners.create!(runner_type: 1, token: nil, token_encrypted: nil) }
|
||||
let!(:runner_with_encrypted_token) do
|
||||
runners.create!(runner_type: 1, token: nil,
|
||||
token_encrypted: Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(32)))
|
||||
end
|
||||
|
||||
let!(:runner_with_plain_and_encrypted_token) do
|
||||
runners.create!(runner_type: 1, token: 'plain_token2',
|
||||
token_encrypted: Gitlab::CryptoHelper.aes256_gcm_encrypt('encrypted_token2'))
|
||||
end
|
||||
|
||||
subject(:perform_migration) { described_class.new(**args).perform }
|
||||
|
||||
it 'encrypts plain tokens', :aggregate_failures do
|
||||
expect { perform_migration }
|
||||
.to change { runner_with_plain_token.reload.token_encrypted }
|
||||
.from(nil).to(Authn::TokenField::EncryptionHelper.encrypt_token('plain_token'))
|
||||
.and change { runner_with_plain_token.reload.token }.to(nil)
|
||||
.and not_change { runner_with_encrypted_token.reload.token }
|
||||
.and not_change { runner_with_encrypted_token.reload.token_encrypted }
|
||||
.and not_change { runner_with_plain_and_encrypted_token.reload.token }
|
||||
.and not_change { runner_with_plain_and_encrypted_token.reload.token_encrypted }
|
||||
.and not_change { runner_without_token.reload.token }
|
||||
.and not_change { runner_without_token.reload.token_encrypted }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,17 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do
|
||||
let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') }
|
||||
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:group1) { namespaces_table.create!(name: 'group1', path: 'group1', organization_id: organization.id) }
|
||||
let(:group2) { namespaces_table.create!(name: 'group2', path: 'group2', organization_id: organization.id) }
|
||||
let(:group3) { namespaces_table.create!(name: 'group3', path: 'group3', organization_id: organization.id) }
|
||||
|
||||
let(:integrations_table) { table(:integrations) }
|
||||
let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService', group_id: group1.id) }
|
||||
let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService', group_id: group2.id) }
|
||||
let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService', group_id: group3.id) }
|
||||
let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') }
|
||||
let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') }
|
||||
let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService') }
|
||||
|
||||
let(:table_name) { :jira_tracker_data }
|
||||
let(:batch_column) { :id }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::BeyondIdentity::Client, feature_category: :source_code_management do
|
||||
let_it_be_with_reload(:integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be_with_reload(:integration) { create(:beyond_identity_integration) }
|
||||
|
||||
let(:stubbed_response) do
|
||||
{ 'authorized' => true }.to_json
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_category: :source_code_management do
|
||||
include_context 'changes access checks context'
|
||||
let(:integration_check) { Gitlab::Checks::IntegrationsCheck.new(changes_access) }
|
||||
let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:beyond_identity_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
subject(:check) { described_class.new(integration_check) }
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_catego
|
|||
|
||||
context 'when service accounts are excluded' do
|
||||
let!(:beyond_identity_integration) do
|
||||
create(:beyond_identity_integration, :instance, exclude_service_accounts: true)
|
||||
create(:beyond_identity_integration, exclude_service_accounts: true)
|
||||
end
|
||||
|
||||
it 'does not raise an error' do
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
|
||||
RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis, feature_category: :database do
|
||||
let(:load_balancer) { ActiveRecord::Base.load_balancer }
|
||||
let(:primary_write_location) { 'the-primary-lsn' }
|
||||
let(:last_write_location) { 'the-last-write-lsn' }
|
||||
let(:namespace) { 'user' }
|
||||
let(:location) { '0/1234ABED' }
|
||||
|
||||
let(:sticking) do
|
||||
described_class.new(load_balancer)
|
||||
|
|
@ -141,4 +143,88 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
|
|||
subject { sticking.bulk_stick(:user, ids) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_database_sticking_operations_enabled?' do
|
||||
it 'returns true when the feature flag is enabled' do
|
||||
stub_feature_flags(log_database_sticking_operations: true)
|
||||
expect(sticking.send(:log_database_sticking_operations_enabled?)).to be true
|
||||
end
|
||||
|
||||
it 'returns false when the feature flag is disabled' do
|
||||
stub_feature_flags(log_database_sticking_operations: false)
|
||||
expect(sticking.send(:log_database_sticking_operations_enabled?)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#capture_stick_logs' do
|
||||
let(:id) { 123 }
|
||||
let(:ids) { [124, 125, 16] }
|
||||
|
||||
context 'when logging is enabled and namespace is user' do
|
||||
before do
|
||||
stub_feature_flags(log_database_sticking_operations: true)
|
||||
|
||||
allow(sticking).to receive(:with_primary_write_location).and_yield(location)
|
||||
allow(sticking).to receive(:set_write_location_for)
|
||||
allow(sticking).to receive(:use_primary!)
|
||||
end
|
||||
|
||||
it 'logs the sticking operation with correct parameters' do
|
||||
expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:info).with(
|
||||
event: :load_balancer_stick_logging,
|
||||
client_id: "#{namespace}/#{id}",
|
||||
stick_id: id,
|
||||
stick_type: namespace,
|
||||
current_lsn: location
|
||||
)
|
||||
sticking.stick(namespace, id)
|
||||
end
|
||||
|
||||
it 'logs only the first ID for bulk sticking operations with correct parameters' do
|
||||
expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:info).with(
|
||||
event: :load_balancer_stick_logging,
|
||||
client_id: "#{namespace}/#{ids.first}",
|
||||
stick_id: ids.first,
|
||||
stick_type: namespace,
|
||||
current_lsn: location
|
||||
)
|
||||
|
||||
sticking.bulk_stick(namespace, ids)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logging is disabled' do
|
||||
before do
|
||||
stub_feature_flags(log_database_sticking_operations: false)
|
||||
|
||||
allow(sticking).to receive(:with_primary_write_location).and_yield(location)
|
||||
allow(sticking).to receive(:set_write_location_for)
|
||||
allow(sticking).to receive(:use_primary!)
|
||||
end
|
||||
|
||||
it 'does not log anything' do
|
||||
expect(::Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
|
||||
|
||||
sticking.stick(namespace, id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logging is enabled and namespace is not user' do
|
||||
let(:namespace) { 'project' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(log_database_sticking_operations: true)
|
||||
|
||||
allow(sticking).to receive(:with_primary_write_location).and_yield(location)
|
||||
allow(sticking).to receive(:set_write_location_for)
|
||||
allow(sticking).to receive(:use_primary!)
|
||||
end
|
||||
|
||||
it 'does not log anything' do
|
||||
expect(::Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
|
||||
|
||||
sticking.bulk_stick(namespace, ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueEncryptMissedCiRunnerTokens, migration: :gitlab_ci, feature_category: :fleet_visibility do
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_ci,
|
||||
table_name: :ci_runners,
|
||||
column_name: :id
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe CleanupUntetheredIntegrations, feature_category: :integrations do
|
||||
let(:organizations) { table(:organizations) }
|
||||
let(:integrations) { table(:integrations) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
|
||||
let!(:default_organization) { organizations.create!(id: 1, name: 'Default', path: 'default') }
|
||||
let!(:other_organization) { organizations.create!(id: 2, name: 'Other', path: 'other') }
|
||||
let!(:group) { namespaces.create!(name: 'bar', path: 'bar', type: 'Group', organization_id: 1) }
|
||||
|
||||
let!(:project) do
|
||||
projects.create!(
|
||||
name: 'project',
|
||||
path: 'project',
|
||||
namespace_id: group.id,
|
||||
organization_id: 1,
|
||||
project_namespace_id: group.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:untethered_integration) do
|
||||
integrations.create!(
|
||||
instance: false,
|
||||
group_id: nil,
|
||||
project_id: nil,
|
||||
type_new: 'Integrations::MockMonitoring'
|
||||
)
|
||||
end
|
||||
|
||||
let!(:another_untethered_integration) do
|
||||
integrations.create!(
|
||||
instance: false,
|
||||
group_id: nil,
|
||||
project_id: nil,
|
||||
type_new: 'Integrations::MockCi'
|
||||
)
|
||||
end
|
||||
|
||||
let!(:tethered_integration) do
|
||||
integrations.create!(
|
||||
instance: false,
|
||||
group_id: group.id,
|
||||
type_new: 'Integrations::MockCi'
|
||||
)
|
||||
end
|
||||
|
||||
let!(:another_tethered_integration) do
|
||||
integrations.create!(
|
||||
instance: false,
|
||||
project_id: project.id,
|
||||
group_id: nil,
|
||||
type_new: 'Integrations::Asana'
|
||||
)
|
||||
end
|
||||
|
||||
describe "#up" do
|
||||
it 'removes untethered integrations' do
|
||||
expect(integrations.count).to eq(4)
|
||||
expect(untethered_integration).to be_present
|
||||
expect(another_untethered_integration).to be_present
|
||||
|
||||
migrate!
|
||||
|
||||
expect(integrations.count).to eq(2)
|
||||
expect(tethered_integration.reload).to be_present
|
||||
expect(another_tethered_integration.reload).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RemoveNotNullIntegrationsConstraint, feature_category: :integrations do
|
||||
include Database::TableSchemaHelpers
|
||||
|
||||
describe '#up' do
|
||||
before do
|
||||
ApplicationRecord
|
||||
.connection
|
||||
.execute(
|
||||
'ALTER TABLE integrations ADD CONSTRAINT check_2aae034509 ' \
|
||||
'CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID;'
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes the check constraint' do
|
||||
expect(check_constraint_definition(:integrations, 'check_2aae034509'))
|
||||
.to eq('CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID')
|
||||
|
||||
migrate!
|
||||
|
||||
expect(check_constraint_definition(:integrations, 'check_2aae034509')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -53,28 +53,21 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
it { is_expected.to validate_presence_of(:type) }
|
||||
it { is_expected.to validate_exclusion_of(:type).in_array(described_class::BASE_CLASSES) }
|
||||
|
||||
where(:project_id, :group_id, :instance, :organization_id, :valid) do
|
||||
1 | nil | false | nil | true
|
||||
nil | 1 | false | nil | true
|
||||
nil | nil | true | 1 | true
|
||||
nil | nil | false | nil | false
|
||||
1 | 1 | false | 1 | false
|
||||
1 | nil | true | nil | false
|
||||
nil | 1 | false | nil | true
|
||||
nil | 1 | true | 1 | false
|
||||
where(:project_id, :group_id, :instance, :valid) do
|
||||
1 | nil | false | true
|
||||
nil | 1 | false | true
|
||||
nil | nil | true | true
|
||||
nil | nil | false | false
|
||||
1 | 1 | false | false
|
||||
1 | nil | false | true
|
||||
1 | nil | true | false
|
||||
nil | 1 | false | true
|
||||
nil | 1 | true | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'validates the integration' do
|
||||
expect(
|
||||
build(
|
||||
:integration,
|
||||
project_id: project_id,
|
||||
group_id: group_id,
|
||||
organization_id: organization_id,
|
||||
instance: instance
|
||||
).valid?
|
||||
).to eq(valid)
|
||||
expect(build(:integration, project_id: project_id, group_id: group_id, instance: instance).valid?).to eq(valid)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -363,95 +356,57 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
end
|
||||
|
||||
describe '.find_or_initialize_all_non_project_specific' do
|
||||
shared_examples 'integration instances' do |include_instance_specific|
|
||||
it 'returns the available integration instances' do
|
||||
integrations = described_class.find_or_initialize_all_non_project_specific(
|
||||
described_class.for_instance, include_instance_specific: include_instance_specific
|
||||
).map(&:to_param)
|
||||
shared_examples 'integration instances' do
|
||||
[false, true].each do |include_instance_specific|
|
||||
context "with include_instance_specific value equal to #{include_instance_specific}" do
|
||||
it 'returns the available integration instances' do
|
||||
integrations = described_class.find_or_initialize_all_non_project_specific(
|
||||
described_class.for_instance, include_instance_specific: include_instance_specific
|
||||
).map(&:to_param)
|
||||
|
||||
expect(integrations).to match_array(
|
||||
described_class.available_integration_names(
|
||||
include_project_specific: false,
|
||||
include_instance_specific: include_instance_specific
|
||||
)
|
||||
)
|
||||
end
|
||||
expect(integrations).to match_array(
|
||||
described_class.available_integration_names(
|
||||
include_project_specific: false,
|
||||
include_instance_specific: include_instance_specific)
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not create integration instances' do
|
||||
expect do
|
||||
described_class.find_or_initialize_all_non_project_specific(
|
||||
described_class.for_instance,
|
||||
include_instance_specific: include_instance_specific
|
||||
)
|
||||
end.not_to change { described_class.count }
|
||||
it 'does not create integration instances' do
|
||||
expect do
|
||||
described_class.find_or_initialize_all_non_project_specific(
|
||||
described_class.for_instance,
|
||||
include_instance_specific: include_instance_specific
|
||||
)
|
||||
end.not_to change { described_class.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without existing instance integrations' do
|
||||
it_behaves_like 'integration instances', false
|
||||
it_behaves_like 'integration instances', true
|
||||
end
|
||||
it_behaves_like 'integration instances'
|
||||
|
||||
context 'with existing instances' do
|
||||
context 'with all existing instances' do
|
||||
def integration_hash(type)
|
||||
integration_hash = Integration.new(type: type).to_database_hash
|
||||
integration_hash[:organization_id] = create(:organization).id
|
||||
integration_hash[:instance] = true
|
||||
integration_hash
|
||||
Integration.new(instance: true, type: type).to_database_hash
|
||||
end
|
||||
|
||||
context 'when all instance integrations are present' do
|
||||
context 'when include_instance_specific is true' do
|
||||
before do
|
||||
attrs = described_class
|
||||
.available_integration_types(
|
||||
include_project_specific: false,
|
||||
include_instance_specific: true
|
||||
)
|
||||
.map do |integration_type|
|
||||
integration_hash(integration_type)
|
||||
end
|
||||
|
||||
described_class.insert_all(attrs)
|
||||
end
|
||||
|
||||
it_behaves_like 'integration instances', true
|
||||
|
||||
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
|
||||
before do
|
||||
described_class.insert(integration_hash(:mock_ci))
|
||||
described_class.delete_by(**integration_hash(:asana))
|
||||
end
|
||||
|
||||
it_behaves_like 'integration instances', true
|
||||
end
|
||||
before do
|
||||
attrs = described_class.available_integration_types(include_project_specific: false).map do |integration_type|
|
||||
integration_hash(integration_type)
|
||||
end
|
||||
|
||||
context 'when include_instance_specific is false' do
|
||||
before do
|
||||
attrs = described_class
|
||||
.available_integration_types(
|
||||
include_project_specific: false,
|
||||
include_instance_specific: false
|
||||
)
|
||||
.map do |integration_type|
|
||||
integration_hash(integration_type)
|
||||
end
|
||||
described_class.insert_all(attrs)
|
||||
end
|
||||
|
||||
described_class.insert_all(attrs)
|
||||
end
|
||||
it_behaves_like 'integration instances'
|
||||
|
||||
it_behaves_like 'integration instances', false
|
||||
|
||||
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
|
||||
before do
|
||||
described_class.insert(integration_hash(:mock_ci))
|
||||
described_class.delete_by(**integration_hash(:asana))
|
||||
end
|
||||
|
||||
it_behaves_like 'integration instances', false
|
||||
end
|
||||
context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do
|
||||
before do
|
||||
described_class.insert(integration_hash(:mock_ci))
|
||||
described_class.delete_by(**integration_hash(:asana))
|
||||
end
|
||||
|
||||
it_behaves_like 'integration instances'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -460,8 +415,7 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
create(:jira_integration, :instance)
|
||||
end
|
||||
|
||||
it_behaves_like 'integration instances', true
|
||||
it_behaves_like 'integration instances', false
|
||||
it_behaves_like 'integration instances'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -648,8 +602,8 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
end
|
||||
|
||||
describe '.create_from_default_integrations' do
|
||||
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/', organization: create(:organization)) }
|
||||
let!(:instance_level_instance_specific_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
|
||||
let!(:instance_level_instance_specific_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'creates integrations from default integrations' do
|
||||
expect(described_class).to receive(:create_from_active_default_integrations)
|
||||
|
|
@ -783,7 +737,7 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
end
|
||||
|
||||
context 'when the integration is instance specific' do
|
||||
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'does not create an integration from the instance level instance specific integration' do
|
||||
described_class.create_from_active_default_integrations(project, :project_id)
|
||||
|
|
@ -796,7 +750,7 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
|
||||
describe '.create_from_default_instance_specific_integrations' do
|
||||
context 'with an active instance-level integration' do
|
||||
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'creates an integration from the instance-level integration' do
|
||||
described_class.create_from_default_instance_specific_integrations(project, :project_id)
|
||||
|
|
@ -1514,7 +1468,7 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
|
||||
it 'saves correctly using insert_all' do
|
||||
hash = record.to_database_hash
|
||||
hash[:project_id] = project.id
|
||||
hash[:project_id] = project
|
||||
|
||||
expect do
|
||||
described_class.insert_all([hash])
|
||||
|
|
@ -1522,9 +1476,6 @@ RSpec.describe Integration, feature_category: :integrations do
|
|||
|
||||
expect(described_class.last).not_to eq record
|
||||
expect(described_class.last).to have_attributes(properties: db_props)
|
||||
expect(described_class.last.project_id).to eq(project.id)
|
||||
expect(described_class.last.group_id).to be_nil
|
||||
expect(described_class.last.organization_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Integrations::BeyondIdentity, feature_category: :integrations do
|
||||
subject(:integration) { create(:beyond_identity_integration, :instance) }
|
||||
subject(:integration) { create(:beyond_identity_integration) }
|
||||
|
||||
describe 'validations' do
|
||||
context 'when inactive' do
|
||||
|
|
@ -65,28 +65,22 @@ RSpec.describe Integrations::BeyondIdentity, feature_category: :integrations do
|
|||
end
|
||||
|
||||
describe '.activated_for_instance?' do
|
||||
let!(:integration) do
|
||||
create(:beyond_identity_integration, instance: instance, active: active, group: group, organization: organization)
|
||||
end
|
||||
|
||||
let!(:integration) { create(:beyond_identity_integration, instance: instance, active: active, group: group) }
|
||||
let_it_be(:group_for_integration) { create(:group) }
|
||||
let_it_be(:organization_for_integration) { create(:organization) }
|
||||
|
||||
subject(:activated) { described_class.activated_for_instance? }
|
||||
subject { described_class.activated_for_instance? }
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:instance, :group, :active, :expected, :organization) do
|
||||
true | nil | true | true | lazy { organization_for_integration }
|
||||
false | lazy { group_for_integration } | true | false | nil
|
||||
true | nil | false | false | lazy { organization_for_integration }
|
||||
false | lazy { group_for_integration } | false | false | nil
|
||||
where(:instance, :group, :active, :expected) do
|
||||
true | nil | true | true
|
||||
false | lazy { group_for_integration } | true | false
|
||||
true | nil | false | false
|
||||
false | lazy { group_for_integration } | false | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns true if integration is activated' do
|
||||
expect(activated).to eq(expected)
|
||||
end
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching, fea
|
|||
it 'does not create a hook if project is not present' do
|
||||
integration.project = nil
|
||||
integration.instance = true
|
||||
integration.organization = create(:organization)
|
||||
|
||||
expect { integration.save! }.not_to change(ServiceHook, :count)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::SlacksController, :enable_admin_mode, :with_current_organization, feature_category: :integrations do
|
||||
RSpec.describe Admin::SlacksController, :enable_admin_mode, feature_category: :integrations do
|
||||
let_it_be(:user) { create(:admin) }
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ RSpec.describe 'Querying for integration exclusions', feature_category: :integra
|
|||
end
|
||||
|
||||
context 'when the user is authorized' do
|
||||
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_integration) { create(:beyond_identity_integration) }
|
||||
let!(:integration_exclusion) do
|
||||
create(:beyond_identity_integration, active: false, instance: false, project: project2, inherit_from_id: nil)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ RSpec.describe Mutations::Integrations::Exclusions::Create, feature_category: :i
|
|||
end
|
||||
|
||||
context 'when integrations exist for the projects' do
|
||||
let!(:instance_exclusion) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_exclusion) { create(:beyond_identity_integration) }
|
||||
let!(:existing_exclusion) do
|
||||
create(:beyond_identity_integration, project: project2, active: false, inherit_from_id: instance_exclusion.id,
|
||||
instance: false)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ RSpec.describe Mutations::Integrations::Exclusions::Delete, feature_category: :i
|
|||
end
|
||||
|
||||
context 'and the integration is active for the instance' do
|
||||
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
context 'and there is a group exclusion', :sidekiq_inline do
|
||||
let!(:group_exclusion) do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'NamespacesRegenerateNewWorkItemEmailAddress', feature_category: :team_planning do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :private) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
let(:mutation) do
|
||||
graphql_mutation(
|
||||
:namespaces_regenerate_new_work_item_email_address,
|
||||
{ full_path: namespace.full_path },
|
||||
<<~FIELDS
|
||||
namespace {
|
||||
id
|
||||
fullPath
|
||||
}
|
||||
errors
|
||||
FIELDS
|
||||
)
|
||||
end
|
||||
|
||||
shared_examples 'successful regeneration' do
|
||||
it 'regenerates the new work item email address' do
|
||||
expect { post_graphql_mutation(mutation, current_user: user) }
|
||||
.to change { user.reload.incoming_email_token }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']['fullPath'])
|
||||
.to eq(namespace.full_path)
|
||||
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors']).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'permission denied' do
|
||||
it 'returns an error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_errors).to include(
|
||||
a_hash_including(
|
||||
'message' => 'The resource that you are attempting to access does not exist or you don\'t have ' \
|
||||
'permission to perform this action'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for group namespace' do
|
||||
let(:namespace) { group }
|
||||
|
||||
context 'when work item creation via email is supported' do
|
||||
before do
|
||||
stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com')
|
||||
user.ensure_incoming_email_token!
|
||||
end
|
||||
|
||||
context 'when user has access to namespace' do
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns an error about work item creation only being supported for projects' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors'])
|
||||
.to include('Work item creation via email is only supported for projects')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to namespace' do
|
||||
it_behaves_like 'permission denied'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for project namespace' do
|
||||
let(:namespace) { project.project_namespace }
|
||||
|
||||
context 'when work item creation via email is supported' do
|
||||
before do
|
||||
stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com')
|
||||
user.ensure_incoming_email_token!
|
||||
end
|
||||
|
||||
context 'when user has access to namespace' do
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'successful regeneration'
|
||||
end
|
||||
|
||||
context 'when user does not have access to namespace' do
|
||||
it_behaves_like 'permission denied'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when work item creation via email is not supported' do
|
||||
before do
|
||||
stub_incoming_email_setting(enabled: false)
|
||||
end
|
||||
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns an error about work item creation not being supported' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors'])
|
||||
.to include('Work item creation via email is not supported')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token reset fails' do
|
||||
before do
|
||||
stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com')
|
||||
user.ensure_incoming_email_token!
|
||||
allow_next_found_instance_of(User) do |instance|
|
||||
allow(instance).to receive(:reset_incoming_email_token!).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns an error about failed regeneration' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil
|
||||
expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors'])
|
||||
.to include('Failed to regenerate new work item email address')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,7 +18,7 @@ RSpec.describe GpgKeys::ValidateIntegrationsService, feature_category: :source_c
|
|||
end
|
||||
|
||||
context 'when BeyondIdentity integration is not activated' do
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) }
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, active: false) }
|
||||
|
||||
it 'return false' do
|
||||
expect(::Gitlab::BeyondIdentity::Client).not_to receive(:new)
|
||||
|
|
@ -28,7 +28,7 @@ RSpec.describe GpgKeys::ValidateIntegrationsService, feature_category: :source_c
|
|||
end
|
||||
|
||||
context 'when BeyondIdentity integration is activated' do
|
||||
let_it_be(:integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'returns true on successful check' do
|
||||
expect_next_instance_of(::Gitlab::BeyondIdentity::Client) do |instance|
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
|
|||
end
|
||||
|
||||
context 'when an instance-level instance specific integration' do
|
||||
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'creates integration inheriting from the instance level integration' do
|
||||
expect(created_group.integrations.count).to eq(1)
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :grou
|
|||
end
|
||||
|
||||
context 'with instance specific integration' do
|
||||
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:instance_specific_integration) { create(:beyond_identity_integration) }
|
||||
let_it_be(:group_instance_specific_integration) do
|
||||
create(
|
||||
:beyond_identity_integration,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ RSpec.describe Integrations::Exclusions::CreateService, feature_category: :sourc
|
|||
end
|
||||
|
||||
context 'when there are existing custom settings' do
|
||||
let_it_be(:instance_level_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let_it_be(:instance_level_integration) { create(:beyond_identity_integration) }
|
||||
let_it_be(:project_level_integration) do
|
||||
create(
|
||||
:beyond_identity_integration,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ RSpec.describe Integrations::Exclusions::DestroyService, feature_category: :sour
|
|||
end
|
||||
|
||||
context 'and the integration is active for the instance' do
|
||||
let!(:instance_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'updates the exclusion integration to be active' do
|
||||
expect { execute }.to change { exclusion.reload.active }.from(false).to(true)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_mode, :with_current_organization, feature_category: :integrations do
|
||||
RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_mode, feature_category: :integrations do
|
||||
let_it_be(:user) { create(:admin) }
|
||||
let(:params) { {} }
|
||||
|
||||
|
|
@ -13,10 +13,9 @@ RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_m
|
|||
let(:integration) { Integrations::GitlabSlackApplication.for_instance.first }
|
||||
let(:redirect_url) { Gitlab::Routing.url_helpers.slack_auth_admin_application_settings_slack_url }
|
||||
let(:enqueues_propagation_worker) { true }
|
||||
let(:params) { { code: oauth_code, organization_id: current_organization.id } }
|
||||
|
||||
def create_gitlab_slack_application_integration!
|
||||
Integrations::GitlabSlackApplication.create!(instance: true, organization: current_organization)
|
||||
Integrations::GitlabSlackApplication.create!(instance: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -935,7 +935,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
|
|||
subject(:project) { create_project(user, opts) }
|
||||
|
||||
context 'when an instance-level instance specific integration' do
|
||||
let!(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_specific_integration) { create(:beyond_identity_integration) }
|
||||
|
||||
it 'creates integration inheriting from the instance level integration' do
|
||||
expect(project.integrations.count).to eq(1)
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
|
|||
end
|
||||
|
||||
context 'when the new default integration is instance specific and deactivated' do
|
||||
let!(:instance_specific_integration) { create(:beyond_identity_integration, :instance) }
|
||||
let!(:instance_specific_integration) { create(:beyond_identity_integration) }
|
||||
let!(:project_instance_specific_integration) do
|
||||
create(
|
||||
:beyond_identity_integration,
|
||||
|
|
|
|||
|
|
@ -288,9 +288,6 @@ RSpec.configure do |config|
|
|||
# (ie. ApplicationSetting#auto_devops_enabled)
|
||||
stub_feature_flags(force_autodevops_on_by_default: false)
|
||||
|
||||
# The survey popover can block the diffs causing specs to fail
|
||||
stub_feature_flags(mr_experience_survey: false)
|
||||
|
||||
# Using FortiAuthenticator as OTP provider is disabled by default in
|
||||
# tests, until we introduce it in user settings
|
||||
stub_feature_flags(forti_authenticator: false)
|
||||
|
|
|
|||
|
|
@ -75,13 +75,7 @@ RSpec.describe Projects::PostCreationWorker, feature_category: :source_code_mana
|
|||
end
|
||||
|
||||
it 'cleans invalid record and logs warning', :aggregate_failures do
|
||||
invalid_integration_record = build(
|
||||
:prometheus_integration,
|
||||
properties: { api_url: nil, manual_configuration: true },
|
||||
project: build(:project),
|
||||
group: build(:group)
|
||||
)
|
||||
|
||||
invalid_integration_record = build(:prometheus_integration, properties: { api_url: nil, manual_configuration: true })
|
||||
allow(::Integrations::Prometheus).to receive(:new).and_return(invalid_integration_record)
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice
|
||||
|
|
|
|||
Loading…
Reference in New Issue