Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c1436508fa
commit
f9e0126cad
|
@ -2606,7 +2606,6 @@ Style/OpenStructUse:
|
||||||
- 'spec/models/design_management/design_at_version_spec.rb'
|
- 'spec/models/design_management/design_at_version_spec.rb'
|
||||||
- 'spec/models/user_spec.rb'
|
- 'spec/models/user_spec.rb'
|
||||||
- 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
|
- 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
|
||||||
- 'spec/requests/api/graphql/mutations/design_management/delete_spec.rb'
|
|
||||||
- 'spec/requests/api/import_github_spec.rb'
|
- 'spec/requests/api/import_github_spec.rb'
|
||||||
- 'spec/services/packages/nuget/metadata_extraction_service_spec.rb'
|
- 'spec/services/packages/nuget/metadata_extraction_service_spec.rb'
|
||||||
- 'spec/services/projects/import_service_spec.rb'
|
- 'spec/services/projects/import_service_spec.rb'
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
|
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
|
||||||
|
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
|
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
|
||||||
import {
|
import {
|
||||||
|
@ -177,16 +178,16 @@ export default {
|
||||||
'saveDiffDiscussion',
|
'saveDiffDiscussion',
|
||||||
'setSuggestPopoverDismissed',
|
'setSuggestPopoverDismissed',
|
||||||
]),
|
]),
|
||||||
handleCancelCommentForm(shouldConfirm, isDirty) {
|
async handleCancelCommentForm(shouldConfirm, isDirty) {
|
||||||
if (shouldConfirm && isDirty) {
|
if (shouldConfirm && isDirty) {
|
||||||
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
|
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
|
||||||
|
|
||||||
// eslint-disable-next-line no-alert
|
const confirmed = await confirmAction(msg);
|
||||||
if (!window.confirm(msg)) {
|
|
||||||
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cancelCommentForm({
|
this.cancelCommentForm({
|
||||||
lineCode: this.line.line_code,
|
lineCode: this.line.line_code,
|
||||||
fileHash: this.diffFileHash,
|
fileHash: this.diffFileHash,
|
||||||
|
|
|
@ -1,25 +1,9 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export function confirmViaGlModal(message, element) {
|
export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = {}) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
|
|
||||||
const props = {};
|
|
||||||
|
|
||||||
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
|
|
||||||
|
|
||||||
if (confirmBtnVariant) {
|
|
||||||
props.primaryVariant = confirmBtnVariant;
|
|
||||||
}
|
|
||||||
const screenReaderText =
|
|
||||||
element.querySelector('.gl-sr-only')?.textContent ||
|
|
||||||
element.querySelector('.sr-only')?.textContent ||
|
|
||||||
element.getAttribute('aria-label');
|
|
||||||
|
|
||||||
if (screenReaderText) {
|
|
||||||
props.primaryText = screenReaderText;
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = new Vue({
|
const component = new Vue({
|
||||||
components: {
|
components: {
|
||||||
ConfirmModal: () => import('./confirm_modal.vue'),
|
ConfirmModal: () => import('./confirm_modal.vue'),
|
||||||
|
@ -28,7 +12,10 @@ export function confirmViaGlModal(message, element) {
|
||||||
return h(
|
return h(
|
||||||
'confirm-modal',
|
'confirm-modal',
|
||||||
{
|
{
|
||||||
props,
|
props: {
|
||||||
|
primaryVariant: primaryBtnVariant,
|
||||||
|
primaryText: primaryBtnText,
|
||||||
|
},
|
||||||
on: {
|
on: {
|
||||||
confirmed() {
|
confirmed() {
|
||||||
confirmed = true;
|
confirmed = true;
|
||||||
|
@ -45,3 +32,24 @@ export function confirmViaGlModal(message, element) {
|
||||||
}).$mount();
|
}).$mount();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function confirmViaGlModal(message, element) {
|
||||||
|
const primaryBtnConfig = {};
|
||||||
|
|
||||||
|
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
|
||||||
|
|
||||||
|
if (confirmBtnVariant) {
|
||||||
|
primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const screenReaderText =
|
||||||
|
element.querySelector('.gl-sr-only')?.textContent ||
|
||||||
|
element.querySelector('.sr-only')?.textContent ||
|
||||||
|
element.getAttribute('aria-label');
|
||||||
|
|
||||||
|
if (screenReaderText) {
|
||||||
|
primaryBtnConfig.primaryBtnText = screenReaderText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmAction(message, primaryBtnConfig);
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
scrollToCommitForm: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -52,6 +57,13 @@ export default {
|
||||||
return !(this.message && this.targetBranch);
|
return !(this.message && this.targetBranch);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
scrollToCommitForm(flag) {
|
||||||
|
if (flag) {
|
||||||
|
this.scrollIntoView();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.$emit('submit', {
|
this.$emit('submit', {
|
||||||
|
@ -63,6 +75,10 @@ export default {
|
||||||
onReset() {
|
onReset() {
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
},
|
},
|
||||||
|
scrollIntoView() {
|
||||||
|
this.$el.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
this.$emit('scrolled-to-commit-form');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
commitMessage: __('Commit message'),
|
commitMessage: __('Commit message'),
|
||||||
|
|
|
@ -45,6 +45,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
scrollToCommitForm: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -146,6 +151,8 @@ export default {
|
||||||
:current-branch="currentBranch"
|
:current-branch="currentBranch"
|
||||||
:default-message="defaultCommitMessage"
|
:default-message="defaultCommitMessage"
|
||||||
:is-saving="isSaving"
|
:is-saving="isSaving"
|
||||||
|
:scroll-to-commit-form="scrollToCommitForm"
|
||||||
|
v-on="$listeners"
|
||||||
@cancel="onCommitCancel"
|
@cancel="onCommitCancel"
|
||||||
@submit="onCommitSubmit"
|
@submit="onCommitSubmit"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { GlButton, GlIcon } from '@gitlab/ui';
|
import { GlButton, GlIcon } from '@gitlab/ui';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||||
|
import { experiment } from '~/experimentation/utils';
|
||||||
import { DRAWER_EXPANDED_KEY } from '../../constants';
|
import { DRAWER_EXPANDED_KEY } from '../../constants';
|
||||||
import FirstPipelineCard from './cards/first_pipeline_card.vue';
|
import FirstPipelineCard from './cards/first_pipeline_card.vue';
|
||||||
import GettingStartedCard from './cards/getting_started_card.vue';
|
import GettingStartedCard from './cards/getting_started_card.vue';
|
||||||
|
@ -53,12 +54,23 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setInitialExpandState() {
|
setInitialExpandState() {
|
||||||
|
let isExpanded;
|
||||||
|
|
||||||
|
experiment('pipeline_editor_walkthrough', {
|
||||||
|
control: () => {
|
||||||
|
isExpanded = true;
|
||||||
|
},
|
||||||
|
candidate: () => {
|
||||||
|
isExpanded = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// We check in the local storage and if no value is defined, we want the default
|
// We check in the local storage and if no value is defined, we want the default
|
||||||
// to be true. We want to explicitly set it to true here so that the drawer
|
// to be true. We want to explicitly set it to true here so that the drawer
|
||||||
// animates to open on load.
|
// animates to open on load.
|
||||||
const localValue = localStorage.getItem(this.$options.localDrawerKey);
|
const localValue = localStorage.getItem(this.$options.localDrawerKey);
|
||||||
if (localValue === null) {
|
if (localValue === null) {
|
||||||
this.isExpanded = true;
|
this.isExpanded = isExpanded;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setTopPosition() {
|
setTopPosition() {
|
||||||
|
|
|
@ -112,7 +112,7 @@ export default {
|
||||||
isBranchesLoading() {
|
isBranchesLoading() {
|
||||||
return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
|
return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
|
||||||
},
|
},
|
||||||
showBranchSwitcher() {
|
enableBranchSwitcher() {
|
||||||
return this.branches.length > 0 || this.searchTerm.length > 0;
|
return this.branches.length > 0 || this.searchTerm.length > 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -230,11 +230,11 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<gl-dropdown
|
<gl-dropdown
|
||||||
v-if="showBranchSwitcher"
|
|
||||||
v-gl-tooltip.hover
|
v-gl-tooltip.hover
|
||||||
:title="$options.i18n.dropdownHeader"
|
:title="$options.i18n.dropdownHeader"
|
||||||
:header-text="$options.i18n.dropdownHeader"
|
:header-text="$options.i18n.dropdownHeader"
|
||||||
:text="currentBranch"
|
:text="currentBranch"
|
||||||
|
:disabled="!enableBranchSwitcher"
|
||||||
icon="branch"
|
icon="branch"
|
||||||
data-qa-selector="branch_selector_button"
|
data-qa-selector="branch_selector_button"
|
||||||
data-testid="branch-selector"
|
data-testid="branch-selector"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { s__ } from '~/locale';
|
||||||
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
||||||
|
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
|
||||||
import {
|
import {
|
||||||
CREATE_TAB,
|
CREATE_TAB,
|
||||||
EDITOR_APP_STATUS_EMPTY,
|
EDITOR_APP_STATUS_EMPTY,
|
||||||
|
@ -22,6 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
|
||||||
import TextEditor from './editor/text_editor.vue';
|
import TextEditor from './editor/text_editor.vue';
|
||||||
import CiLint from './lint/ci_lint.vue';
|
import CiLint from './lint/ci_lint.vue';
|
||||||
import EditorTab from './ui/editor_tab.vue';
|
import EditorTab from './ui/editor_tab.vue';
|
||||||
|
import WalkthroughPopover from './walkthrough_popover.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
i18n: {
|
i18n: {
|
||||||
|
@ -63,6 +65,8 @@ export default {
|
||||||
GlTabs,
|
GlTabs,
|
||||||
PipelineGraph,
|
PipelineGraph,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
|
GitlabExperiment,
|
||||||
|
WalkthroughPopover,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagsMixin()],
|
mixins: [glFeatureFlagsMixin()],
|
||||||
props: {
|
props: {
|
||||||
|
@ -79,6 +83,10 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
isNewCiConfigFile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
appStatus: {
|
appStatus: {
|
||||||
|
@ -136,11 +144,17 @@ export default {
|
||||||
>
|
>
|
||||||
<editor-tab
|
<editor-tab
|
||||||
class="gl-mb-3"
|
class="gl-mb-3"
|
||||||
|
title-link-class="js-walkthrough-popover-target"
|
||||||
:title="$options.i18n.tabEdit"
|
:title="$options.i18n.tabEdit"
|
||||||
lazy
|
lazy
|
||||||
data-testid="editor-tab"
|
data-testid="editor-tab"
|
||||||
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
|
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
|
||||||
>
|
>
|
||||||
|
<gitlab-experiment name="pipeline_editor_walkthrough">
|
||||||
|
<template #candidate>
|
||||||
|
<walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
|
||||||
|
</template>
|
||||||
|
</gitlab-experiment>
|
||||||
<ci-editor-header />
|
<ci-editor-header />
|
||||||
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
|
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
|
||||||
</editor-tab>
|
</editor-tab>
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script>
|
||||||
|
import { GlButton, GlPopover, GlSprintf, GlOutsideDirective as Outside } from '@gitlab/ui';
|
||||||
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
directives: { Outside },
|
||||||
|
i18n: {
|
||||||
|
title: s__('pipelineEditorWalkthrough|See how GitLab pipelines work'),
|
||||||
|
description: s__(
|
||||||
|
'pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline.',
|
||||||
|
),
|
||||||
|
instruction: s__(
|
||||||
|
'pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline.',
|
||||||
|
),
|
||||||
|
ctaText: s__("pipelineEditorWalkthrough|Let's do this!"),
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
GlButton,
|
||||||
|
GlPopover,
|
||||||
|
GlSprintf,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
targetElement() {
|
||||||
|
return document.querySelector('.js-walkthrough-popover-target');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.show = false;
|
||||||
|
},
|
||||||
|
handleClickCta() {
|
||||||
|
this.close();
|
||||||
|
this.$emit('walkthrough-popover-cta-clicked');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-popover
|
||||||
|
:show.sync="show"
|
||||||
|
:title="$options.i18n.title"
|
||||||
|
:target="targetElement"
|
||||||
|
placement="right"
|
||||||
|
triggers="focus"
|
||||||
|
>
|
||||||
|
<div v-outside="close" class="gl-display-flex gl-flex-direction-column">
|
||||||
|
<p>
|
||||||
|
<gl-sprintf :message="$options.i18n.description">
|
||||||
|
<template #code="{ content }">
|
||||||
|
<code>{{ content }}</code>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<gl-sprintf :message="$options.i18n.instruction">
|
||||||
|
<template #bold="{ content }">
|
||||||
|
<strong>
|
||||||
|
{{ content }}
|
||||||
|
</strong>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<gl-button
|
||||||
|
class="gl-align-self-end"
|
||||||
|
category="tertiary"
|
||||||
|
data-testid="ctaBtn"
|
||||||
|
variant="confirm"
|
||||||
|
@click="handleClickCta"
|
||||||
|
>
|
||||||
|
<gl-emoji data-name="rocket" />
|
||||||
|
{{ this.$options.i18n.ctaText }}
|
||||||
|
</gl-button>
|
||||||
|
</div>
|
||||||
|
</gl-popover>
|
||||||
|
</template>
|
|
@ -58,6 +58,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentTab: CREATE_TAB,
|
currentTab: CREATE_TAB,
|
||||||
|
scrollToCommitForm: false,
|
||||||
shouldLoadNewBranch: false,
|
shouldLoadNewBranch: false,
|
||||||
showSwitchBranchModal: false,
|
showSwitchBranchModal: false,
|
||||||
};
|
};
|
||||||
|
@ -81,6 +82,9 @@ export default {
|
||||||
setCurrentTab(tabName) {
|
setCurrentTab(tabName) {
|
||||||
this.currentTab = tabName;
|
this.currentTab = tabName;
|
||||||
},
|
},
|
||||||
|
setScrollToCommitForm(newValue = true) {
|
||||||
|
this.scrollToCommitForm = newValue;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -117,8 +121,10 @@ export default {
|
||||||
:ci-config-data="ciConfigData"
|
:ci-config-data="ciConfigData"
|
||||||
:ci-file-content="ciFileContent"
|
:ci-file-content="ciFileContent"
|
||||||
:commit-sha="commitSha"
|
:commit-sha="commitSha"
|
||||||
|
:is-new-ci-config-file="isNewCiConfigFile"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
@set-current-tab="setCurrentTab"
|
@set-current-tab="setCurrentTab"
|
||||||
|
@walkthrough-popover-cta-clicked="setScrollToCommitForm"
|
||||||
/>
|
/>
|
||||||
<commit-section
|
<commit-section
|
||||||
v-if="showCommitForm"
|
v-if="showCommitForm"
|
||||||
|
@ -126,6 +132,8 @@ export default {
|
||||||
:ci-file-content="ciFileContent"
|
:ci-file-content="ciFileContent"
|
||||||
:commit-sha="commitSha"
|
:commit-sha="commitSha"
|
||||||
:is-new-ci-config-file="isNewCiConfigFile"
|
:is-new-ci-config-file="isNewCiConfigFile"
|
||||||
|
:scroll-to-commit-form="scrollToCommitForm"
|
||||||
|
@scrolled-to-commit-form="setScrollToCommitForm(false)"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
/>
|
/>
|
||||||
<pipeline-editor-drawer />
|
<pipeline-editor-drawer />
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlLink } from '@gitlab/ui';
|
import { GlBadge, GlLink } from '@gitlab/ui';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { fetchPolicies } from '~/lib/graphql';
|
import { fetchPolicies } from '~/lib/graphql';
|
||||||
import { updateHistory } from '~/lib/utils/url_utility';
|
import { updateHistory } from '~/lib/utils/url_utility';
|
||||||
import { formatNumber, sprintf, __ } from '~/locale';
|
import { sprintf, __ } from '~/locale';
|
||||||
|
|
||||||
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
||||||
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||||
|
@ -14,7 +14,13 @@ import RunnerTypeTabs from '../components/runner_type_tabs.vue';
|
||||||
|
|
||||||
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
|
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
|
||||||
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
|
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
|
||||||
import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
|
import {
|
||||||
|
ADMIN_FILTERED_SEARCH_NAMESPACE,
|
||||||
|
INSTANCE_TYPE,
|
||||||
|
GROUP_TYPE,
|
||||||
|
PROJECT_TYPE,
|
||||||
|
I18N_FETCH_ERROR,
|
||||||
|
} from '../constants';
|
||||||
import getRunnersQuery from '../graphql/get_runners.query.graphql';
|
import getRunnersQuery from '../graphql/get_runners.query.graphql';
|
||||||
import {
|
import {
|
||||||
fromUrlQueryToSearch,
|
fromUrlQueryToSearch,
|
||||||
|
@ -26,6 +32,7 @@ import { captureException } from '../sentry_utils';
|
||||||
export default {
|
export default {
|
||||||
name: 'AdminRunnersApp',
|
name: 'AdminRunnersApp',
|
||||||
components: {
|
components: {
|
||||||
|
GlBadge,
|
||||||
GlLink,
|
GlLink,
|
||||||
RegistrationDropdown,
|
RegistrationDropdown,
|
||||||
RunnerFilteredSearchBar,
|
RunnerFilteredSearchBar,
|
||||||
|
@ -35,11 +42,27 @@ export default {
|
||||||
RunnerTypeTabs,
|
RunnerTypeTabs,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
activeRunnersCount: {
|
registrationToken: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
registrationToken: {
|
activeRunnersCount: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
allRunnersCount: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
instanceRunnersCount: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
groupRunnersCount: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
projectRunnersCount: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
@ -89,7 +112,7 @@ export default {
|
||||||
},
|
},
|
||||||
activeRunnersMessage() {
|
activeRunnersMessage() {
|
||||||
return sprintf(__('Runners currently online: %{active_runners_count}'), {
|
return sprintf(__('Runners currently online: %{active_runners_count}'), {
|
||||||
active_runners_count: formatNumber(this.activeRunnersCount),
|
active_runners_count: this.activeRunnersCount,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
searchTokens() {
|
searchTokens() {
|
||||||
|
@ -118,6 +141,20 @@ export default {
|
||||||
this.reportToSentry(error);
|
this.reportToSentry(error);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
tabCount({ runnerType }) {
|
||||||
|
switch (runnerType) {
|
||||||
|
case null:
|
||||||
|
return this.allRunnersCount;
|
||||||
|
case INSTANCE_TYPE:
|
||||||
|
return this.instanceRunnersCount;
|
||||||
|
case GROUP_TYPE:
|
||||||
|
return this.groupRunnersCount;
|
||||||
|
case PROJECT_TYPE:
|
||||||
|
return this.projectRunnersCount;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
reportToSentry(error) {
|
reportToSentry(error) {
|
||||||
captureException({ error, component: this.$options.name });
|
captureException({ error, component: this.$options.name });
|
||||||
},
|
},
|
||||||
|
@ -128,15 +165,25 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="gl-display-flex gl-align-items-center">
|
<div
|
||||||
|
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
|
||||||
|
>
|
||||||
<runner-type-tabs
|
<runner-type-tabs
|
||||||
v-model="search"
|
v-model="search"
|
||||||
|
class="gl-w-full"
|
||||||
content-class="gl-display-none"
|
content-class="gl-display-none"
|
||||||
nav-class="gl-border-none!"
|
nav-class="gl-border-none!"
|
||||||
/>
|
>
|
||||||
|
<template #title="{ tab }">
|
||||||
|
{{ tab.title }}
|
||||||
|
<gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
|
||||||
|
{{ tabCount(tab) }}
|
||||||
|
</gl-badge>
|
||||||
|
</template>
|
||||||
|
</runner-type-tabs>
|
||||||
|
|
||||||
<registration-dropdown
|
<registration-dropdown
|
||||||
class="gl-ml-auto"
|
class="gl-w-full gl-sm-w-auto gl-mr-auto"
|
||||||
:registration-token="registrationToken"
|
:registration-token="registrationToken"
|
||||||
:type="$options.INSTANCE_TYPE"
|
:type="$options.INSTANCE_TYPE"
|
||||||
right
|
right
|
||||||
|
|
|
@ -16,7 +16,16 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
||||||
|
|
||||||
// TODO `activeRunnersCount` should be implemented using a GraphQL API
|
// TODO `activeRunnersCount` should be implemented using a GraphQL API
|
||||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806
|
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806
|
||||||
const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset;
|
const {
|
||||||
|
runnerInstallHelpPage,
|
||||||
|
registrationToken,
|
||||||
|
|
||||||
|
activeRunnersCount,
|
||||||
|
allRunnersCount,
|
||||||
|
instanceRunnersCount,
|
||||||
|
groupRunnersCount,
|
||||||
|
projectRunnersCount,
|
||||||
|
} = el.dataset;
|
||||||
|
|
||||||
const apolloProvider = new VueApollo({
|
const apolloProvider = new VueApollo({
|
||||||
defaultClient: createDefaultClient(),
|
defaultClient: createDefaultClient(),
|
||||||
|
@ -31,8 +40,15 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
||||||
render(h) {
|
render(h) {
|
||||||
return h(AdminRunnersApp, {
|
return h(AdminRunnersApp, {
|
||||||
props: {
|
props: {
|
||||||
activeRunnersCount: parseInt(activeRunnersCount, 10),
|
|
||||||
registrationToken,
|
registrationToken,
|
||||||
|
|
||||||
|
// All runner counts are returned as formatted
|
||||||
|
// strings, we do not use `parseInt`.
|
||||||
|
activeRunnersCount,
|
||||||
|
allRunnersCount,
|
||||||
|
instanceRunnersCount,
|
||||||
|
groupRunnersCount,
|
||||||
|
projectRunnersCount,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -51,13 +51,16 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<gl-tabs v-bind="$attrs">
|
<gl-tabs v-bind="$attrs" data-testid="runner-type-tabs">
|
||||||
<gl-tab
|
<gl-tab
|
||||||
v-for="tab in $options.tabs"
|
v-for="tab in $options.tabs"
|
||||||
:key="`${tab.runnerType}`"
|
:key="`${tab.runnerType}`"
|
||||||
:active="isTabActive(tab)"
|
:active="isTabActive(tab)"
|
||||||
:title="tab.title"
|
|
||||||
@click="onTabSelected(tab)"
|
@click="onTabSelected(tab)"
|
||||||
/>
|
>
|
||||||
|
<template #title>
|
||||||
|
<slot name="title" :tab="tab">{{ tab.title }}</slot>
|
||||||
|
</template>
|
||||||
|
</gl-tab>
|
||||||
</gl-tabs>
|
</gl-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,7 +8,6 @@ class Admin::RunnersController < Admin::ApplicationController
|
||||||
feature_category :runner
|
feature_category :runner
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@active_runners_count = Ci::Runner.online.count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -116,7 +116,9 @@ class Import::BitbucketController < Import::BaseController
|
||||||
redirect_to oauth_client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
|
redirect_to oauth_client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def bitbucket_unauthorized
|
def bitbucket_unauthorized(exception)
|
||||||
|
log_exception(exception)
|
||||||
|
|
||||||
go_to_bitbucket_for_permissions
|
go_to_bitbucket_for_permissions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
||||||
before_action :check_can_collaborate!
|
before_action :check_can_collaborate!
|
||||||
|
before_action :setup_walkthrough_experiment, only: :show
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
|
||||||
end
|
end
|
||||||
|
@ -16,4 +17,10 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
||||||
def check_can_collaborate!
|
def check_can_collaborate!
|
||||||
render_404 unless can_collaborate_with_project?(@project)
|
render_404 unless can_collaborate_with_project?(@project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setup_walkthrough_experiment
|
||||||
|
experiment(:pipeline_editor_walkthrough, actor: current_user) do |e|
|
||||||
|
e.candidate {}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,13 +23,14 @@ git push -uf origin <%= @project.default_branch_or_main %>
|
||||||
|
|
||||||
## Integrate with your tools
|
## Integrate with your tools
|
||||||
|
|
||||||
- [ ] [Set up project integrations](<%= redirect("https://docs.gitlab.com/ee/user/project/integrations/") %>)
|
- [ ] [Set up project integrations](<%= redirect(project_settings_integrations_url(@project)) %>)
|
||||||
|
|
||||||
## Collaborate with your team
|
## Collaborate with your team
|
||||||
|
|
||||||
- [ ] [Invite team members and collaborators](<%= redirect("https://docs.gitlab.com/ee/user/project/members/") %>)
|
- [ ] [Invite team members and collaborators](<%= redirect("https://docs.gitlab.com/ee/user/project/members/") %>)
|
||||||
- [ ] [Create a new merge request](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html") %>)
|
- [ ] [Create a new merge request](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html") %>)
|
||||||
- [ ] [Automatically close issues from merge requests](<%= redirect("https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically") %>)
|
- [ ] [Automatically close issues from merge requests](<%= redirect("https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically") %>)
|
||||||
|
- [ ] [Enable merge request approvals](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/approvals/") %>)
|
||||||
- [ ] [Automatically merge when pipeline succeeds](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html") %>)
|
- [ ] [Automatically merge when pipeline succeeds](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html") %>)
|
||||||
|
|
||||||
## Test and Deploy
|
## Test and Deploy
|
||||||
|
@ -40,6 +41,7 @@ Use the built-in continuous integration in GitLab.
|
||||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](<%= redirect("https://docs.gitlab.com/ee/user/application_security/sast/") %>)
|
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](<%= redirect("https://docs.gitlab.com/ee/user/application_security/sast/") %>)
|
||||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](<%= redirect("https://docs.gitlab.com/ee/topics/autodevops/requirements.html") %>)
|
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](<%= redirect("https://docs.gitlab.com/ee/topics/autodevops/requirements.html") %>)
|
||||||
- [ ] [Use pull-based deployments for improved Kubernetes management](<%= redirect("https://docs.gitlab.com/ee/user/clusters/agent/") %>)
|
- [ ] [Use pull-based deployments for improved Kubernetes management](<%= redirect("https://docs.gitlab.com/ee/user/clusters/agent/") %>)
|
||||||
|
- [ ] [Set up protected environments](<%= redirect("https://docs.gitlab.com/ee/ci/environments/protected_environments.html") %>)
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,22 @@ module Ci
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def admin_runners_data_attributes
|
||||||
|
{
|
||||||
|
# Runner install help page is external, located at
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab-runner
|
||||||
|
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||||
|
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||||
|
|
||||||
|
# All runner counts are returned as formatted strings
|
||||||
|
active_runners_count: Ci::Runner.online.count.to_s,
|
||||||
|
all_runners_count: limited_counter_with_delimiter(Ci::Runner),
|
||||||
|
instance_runners_count: limited_counter_with_delimiter(Ci::Runner.instance_type),
|
||||||
|
group_runners_count: limited_counter_with_delimiter(Ci::Runner.group_type),
|
||||||
|
project_runners_count: limited_counter_with_delimiter(Ci::Runner.project_type)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def group_shared_runners_settings_data(group)
|
def group_shared_runners_settings_data(group)
|
||||||
{
|
{
|
||||||
update_path: api_v4_groups_path(id: group.id),
|
update_path: api_v4_groups_path(id: group.id),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- breadcrumb_title _('Runners')
|
- breadcrumb_title _('Runners')
|
||||||
- page_title _('Runners')
|
- page_title _('Runners')
|
||||||
|
|
||||||
#js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
|
#js-admin-runners{ data: admin_runners_data_attributes }
|
||||||
|
|
|
@ -18,6 +18,36 @@ module Gitlab
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
config.load_defaults 6.1
|
config.load_defaults 6.1
|
||||||
|
|
||||||
|
# This section contains configuration from Rails upgrades to override the new defaults so that we
|
||||||
|
# keep existing behavior.
|
||||||
|
#
|
||||||
|
# For boolean values, the new default is the opposite of the value being set in this section.
|
||||||
|
# For other types, the new default is noted in the comments. These are also documented in
|
||||||
|
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
|
||||||
|
#
|
||||||
|
# To switch a setting to the new default value, we just need to delete the specific line here.
|
||||||
|
|
||||||
|
# Rails 6.1
|
||||||
|
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
|
||||||
|
ActiveSupport.utc_to_local_returns_utc_offset_times = false
|
||||||
|
config.action_controller.urlsafe_csrf_tokens = false
|
||||||
|
config.action_view.preload_links_header = false
|
||||||
|
|
||||||
|
# Rails 5.2
|
||||||
|
config.action_dispatch.use_authenticated_cookie_encryption = false
|
||||||
|
config.active_support.use_authenticated_message_encryption = false
|
||||||
|
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
|
||||||
|
config.action_controller.default_protect_from_forgery = false
|
||||||
|
config.action_view.form_with_generates_ids = false
|
||||||
|
|
||||||
|
# Rails 5.1
|
||||||
|
config.assets.unknown_asset_fallback = true
|
||||||
|
|
||||||
|
# Rails 5.0
|
||||||
|
config.action_controller.per_form_csrf_tokens = false
|
||||||
|
config.action_controller.forgery_protection_origin_check = false
|
||||||
|
ActiveSupport.to_time_preserves_timezone = false
|
||||||
|
|
||||||
require_dependency Rails.root.join('lib/gitlab')
|
require_dependency Rails.root.join('lib/gitlab')
|
||||||
require_dependency Rails.root.join('lib/gitlab/utils')
|
require_dependency Rails.root.join('lib/gitlab/utils')
|
||||||
require_dependency Rails.root.join('lib/gitlab/action_cable/config')
|
require_dependency Rails.root.join('lib/gitlab/action_cable/config')
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: pipeline_editor_walkthrough
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73050
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345558
|
||||||
|
milestone: '14.5'
|
||||||
|
type: experiment
|
||||||
|
group: group::activation
|
||||||
|
default_enabled: false
|
|
@ -1,33 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# This contains configuration from Rails upgrades to override the new defaults so that we
|
|
||||||
# keep existing behavior.
|
|
||||||
#
|
|
||||||
# For boolean values, the new default is the opposite of the value being set in this file.
|
|
||||||
# For other types, the new default is noted in the comments. These are also documented in
|
|
||||||
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
|
|
||||||
#
|
|
||||||
# To switch a setting to the new default value, we just need to delete the specific line here.
|
|
||||||
|
|
||||||
Rails.application.configure do
|
|
||||||
# Rails 6.1
|
|
||||||
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
|
|
||||||
ActiveSupport.utc_to_local_returns_utc_offset_times = false
|
|
||||||
config.action_controller.urlsafe_csrf_tokens = false
|
|
||||||
config.action_view.preload_links_header = false
|
|
||||||
|
|
||||||
# Rails 5.2
|
|
||||||
config.action_dispatch.use_authenticated_cookie_encryption = false
|
|
||||||
config.active_support.use_authenticated_message_encryption = false
|
|
||||||
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
|
|
||||||
config.action_controller.default_protect_from_forgery = false
|
|
||||||
config.action_view.form_with_generates_ids = false
|
|
||||||
|
|
||||||
# Rails 5.1
|
|
||||||
config.assets.unknown_asset_fallback = true
|
|
||||||
|
|
||||||
# Rails 5.0
|
|
||||||
config.action_controller.per_form_csrf_tokens = false
|
|
||||||
config.action_controller.forgery_protection_origin_check = false
|
|
||||||
ActiveSupport.to_time_preserves_timezone = false
|
|
||||||
end
|
|
|
@ -48,10 +48,18 @@ Please consider creating a merge request to
|
||||||
for them.
|
for them.
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
|
|
||||||
|
def group_not_available_template(slack_channel, gitlab_group)
|
||||||
|
<<~TEMPLATE
|
||||||
|
No engineer is available for automated assignment, please reach out to `#{slack_channel}` slack channel or mention `#{gitlab_group}` for assistance.
|
||||||
|
TEMPLATE
|
||||||
|
end
|
||||||
|
|
||||||
OPTIONAL_REVIEW_TEMPLATE = '%{role} review is optional for %{category}'
|
OPTIONAL_REVIEW_TEMPLATE = '%{role} review is optional for %{category}'
|
||||||
NOT_AVAILABLE_TEMPLATES = {
|
NOT_AVAILABLE_TEMPLATES = {
|
||||||
default: 'No %{role} available',
|
default: 'No %{role} available',
|
||||||
product_intelligence: "No engineer is available for automated assignment, please reach out to `#g_product_intelligence` slack channel or mention `@gitlab-org/growth/product-intelligence/engineers` for assistance."
|
product_intelligence: group_not_available_template('#g_product_intelligence', '@gitlab-org/growth/product-intelligence/engineers'),
|
||||||
|
integrations_be: group_not_available_template('#g_ecosystem_integrations', '@gitlab-org/ecosystem-stage/integrations'),
|
||||||
|
integrations_fe: group_not_available_template('#g_ecosystem_integrations', '@gitlab-org/ecosystem-stage/integrations')
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def note_for_spins_role(spins, role, category)
|
def note_for_spins_role(spins, role, category)
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
|
@ -115,32 +115,28 @@ your chosen [provider](#supported-providers).
|
||||||
|
|
||||||
## Enable OmniAuth for an existing user
|
## Enable OmniAuth for an existing user
|
||||||
|
|
||||||
Existing users can enable OmniAuth for specific providers after the account is
|
If you're an existing user, after your GitLab account is
|
||||||
created. For example, if the user originally signed in with LDAP, an OmniAuth
|
created, you can activate an OmniAuth provider. For example, if you originally signed in with LDAP, you can enable an OmniAuth
|
||||||
provider such as Twitter can be enabled. Follow the steps below to enable an
|
provider like Twitter.
|
||||||
OmniAuth provider for an existing user.
|
|
||||||
|
|
||||||
1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider.
|
1. Sign in to GitLab with your GitLab credentials, LDAP, or another OmniAuth provider.
|
||||||
1. In the top-right corner, select your avatar.
|
1. On the top bar, in the top right corner, select your avatar.
|
||||||
1. Select **Edit profile**.
|
1. Select **Edit profile**.
|
||||||
1. On the left sidebar, select **Account**.
|
1. On the left sidebar, select **Account**.
|
||||||
1. In the **Connected Accounts** section, select the desired OmniAuth provider, such as Twitter.
|
1. In the **Connected Accounts** section, select the OmniAuth provider, such as Twitter.
|
||||||
1. The user is redirected to the provider. After the user authorizes GitLab,
|
1. You are redirected to the provider. After you authorize GitLab,
|
||||||
they are redirected back to GitLab.
|
you are redirected back to GitLab.
|
||||||
|
|
||||||
The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on.
|
You can now use your chosen OmniAuth provider to sign in to GitLab.
|
||||||
|
|
||||||
## Link existing users to OmniAuth users
|
## Link existing users to OmniAuth users
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36664) in GitLab 13.4.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36664) in GitLab 13.4.
|
||||||
|
|
||||||
You can automatically link OmniAuth users with existing GitLab users if their email addresses match.
|
You can automatically link OmniAuth users with existing GitLab users if their email addresses match.
|
||||||
Automatic linking using this method works for all providers
|
|
||||||
[except the SAML provider](https://gitlab.com/gitlab-org/gitlab/-/issues/338293). For automatic
|
|
||||||
linking using the SAML provider, see [SAML-specific](saml.md#general-setup) instructions.
|
|
||||||
|
|
||||||
As an example, the following configuration is used to enable the auto link
|
The following example enables automatic linking
|
||||||
feature for both an **OpenID Connect provider** and a **Twitter OAuth provider**.
|
for the OpenID Connect provider and the Twitter OAuth provider.
|
||||||
|
|
||||||
- **For Omnibus installations**
|
- **For Omnibus installations**
|
||||||
|
|
||||||
|
@ -155,18 +151,23 @@ feature for both an **OpenID Connect provider** and a **Twitter OAuth provider**
|
||||||
auto_link_user: ["openid_connect", "twitter"]
|
auto_link_user: ["openid_connect", "twitter"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configure OmniAuth providers as external
|
This method of enabling automatic linking works for all providers
|
||||||
|
[except SAML](https://gitlab.com/gitlab-org/gitlab/-/issues/338293).
|
||||||
|
To enable automatic linking for SAML, see the [SAML setup instructions](saml.md#general-setup).
|
||||||
|
|
||||||
You can define which OmniAuth providers you want to be `external`. Users
|
## Create an external providers list
|
||||||
creating accounts, or logging in by using these `external` providers cannot have
|
|
||||||
access to internal projects. You must use the full name of the provider,
|
You can define a list of external OmniAuth providers.
|
||||||
like `google_oauth2` for Google. Refer to the examples for the full names of the
|
Users who create accounts or sign in to GitLab through the listed providers do not get access to [internal projects](../public_access/public_access.md#internal-projects-and-groups).
|
||||||
supported providers.
|
|
||||||
|
To define the external providers list, use the full name of the provider,
|
||||||
|
for example, `google_oauth2` for Google. For provider names, see the
|
||||||
|
**OmniAuth provider name** column in the [supported providers table](#supported-providers).
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
If you decide to remove an OmniAuth provider from the external providers list,
|
If you remove an OmniAuth provider from the external providers list,
|
||||||
you must manually update the users that use this method to sign in if you want
|
you must manually update the users that use this sign-in method so their
|
||||||
their accounts to be upgraded to full internal accounts.
|
accounts are upgraded to full internal accounts.
|
||||||
|
|
||||||
- **For Omnibus installations**
|
- **For Omnibus installations**
|
||||||
|
|
||||||
|
@ -184,70 +185,67 @@ their accounts to be upgraded to full internal accounts.
|
||||||
## Use a custom OmniAuth provider
|
## Use a custom OmniAuth provider
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
The following information only applies for installations from source.
|
The following information only applies to installations from source.
|
||||||
|
|
||||||
GitLab uses [OmniAuth](https://github.com/omniauth/omniauth) for authentication and already ships
|
If you have to integrate with an authentication solution other than the [OmniAuth](https://github.com/omniauth/omniauth) providers included with GitLab,
|
||||||
with a few providers pre-installed, such as LDAP, GitHub, and Twitter. You may also
|
you can use a custom OmniAuth provider.
|
||||||
have to integrate with other authentication solutions. For
|
|
||||||
these cases, you can use the OmniAuth provider.
|
|
||||||
|
|
||||||
These steps are fairly general and you must figure out the exact details
|
These steps are general. Read the OmniAuth provider's documentation for the exact
|
||||||
from the OmniAuth provider's documentation.
|
implementation details.
|
||||||
|
|
||||||
- Stop GitLab:
|
1. Stop GitLab:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo service gitlab stop
|
sudo service gitlab stop
|
||||||
```
|
```
|
||||||
|
|
||||||
- Add the gem to your [`Gemfile`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile):
|
1. Add the gem to your [`Gemfile`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
gem "omniauth-your-auth-provider"
|
gem "omniauth-your-auth-provider"
|
||||||
```
|
```
|
||||||
|
|
||||||
- Install the new OmniAuth provider gem by running the following command:
|
1. Install the new OmniAuth provider gem:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo -u git -H bundle install --without development test mysql --path vendor/bundle --no-deployment
|
sudo -u git -H bundle install --without development test mysql --path vendor/bundle --no-deployment
|
||||||
```
|
```
|
||||||
|
|
||||||
> These are the same commands you used during initial installation in the [Install Gems section](../install/installation.md#install-gems) with `--path vendor/bundle --no-deployment` instead of `--deployment`.
|
These commands are the same as the commands for [installing gems](../install/installation.md#install-gems)
|
||||||
|
during initial installation, with `--path vendor/bundle --no-deployment` instead of `--deployment`.
|
||||||
|
|
||||||
- Start GitLab:
|
1. Start GitLab:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo service gitlab start
|
sudo service gitlab start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom OmniAuth provider examples
|
### Custom OmniAuth provider examples
|
||||||
|
|
||||||
If you have successfully set up a provider that is not shipped with GitLab itself,
|
If you have successfully set up a provider that is not already integrated with GitLab,
|
||||||
please let us know.
|
let us know.
|
||||||
|
|
||||||
While we can't officially support every possible authentication mechanism out there,
|
We can't officially support every possible authentication mechanism available,
|
||||||
we'd like to at least help those with specific needs.
|
but we'd like to at least help those with specific needs.
|
||||||
|
|
||||||
## Enable or disable sign-in with an OmniAuth provider without disabling import sources
|
## Enable or disable sign-in with an OmniAuth provider without disabling import sources
|
||||||
|
|
||||||
Administrators are able to enable or disable **Sign In** by using some OmniAuth providers.
|
Administrators can enable or disable sign-in for some OmniAuth providers.
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
By default, **Sign In** is enabled by using all the OAuth Providers that have been configured in `config/gitlab.yml`.
|
By default, sign-in is enabled for all the OAuth providers configured in `config/gitlab.yml`.
|
||||||
|
|
||||||
To enable/disable an OmniAuth provider:
|
To enable or disable an OmniAuth provider:
|
||||||
|
|
||||||
1. On the top bar, select **Menu > Admin**.
|
1. On the top bar, select **Menu > Admin**.
|
||||||
1. On the left sidebar, go to **Settings**.
|
1. On the left sidebar, select **Settings**.
|
||||||
1. Scroll to the **Sign-in Restrictions** section, and click **Expand**.
|
1. Expand **Sign-in restrictions**.
|
||||||
1. Below **Enabled OAuth Sign-In sources**, select the checkbox for each provider you want to enable or disable.
|
1. In the **Enabled OAuth authentication sources** section, select or clear the checkbox for each provider you want to enable or disable.
|
||||||
|
|
||||||
![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources_v13_10.png)
|
|
||||||
|
|
||||||
## Disable OmniAuth
|
## Disable OmniAuth
|
||||||
|
|
||||||
Starting from version 11.4 of GitLab, OmniAuth is enabled by default. This only
|
In GitLab 11.4 and later, OmniAuth is enabled by default. However, OmniAuth only works
|
||||||
has an effect if providers are configured and [enabled](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
|
if providers are configured and [enabled](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
|
||||||
|
|
||||||
If OmniAuth providers are causing problems even when individually disabled, you
|
If OmniAuth providers are causing problems even when individually disabled, you
|
||||||
can disable the entire OmniAuth subsystem by modifying the configuration file:
|
can disable the entire OmniAuth subsystem by modifying the configuration file:
|
||||||
|
@ -267,7 +265,8 @@ can disable the entire OmniAuth subsystem by modifying the configuration file:
|
||||||
|
|
||||||
## Keep OmniAuth user profiles up to date
|
## Keep OmniAuth user profiles up to date
|
||||||
|
|
||||||
You can enable profile syncing from selected OmniAuth providers and for all or for specific user information.
|
You can enable profile syncing from selected OmniAuth providers. You can sync
|
||||||
|
all or specific user information.
|
||||||
|
|
||||||
When authenticating using LDAP, the user's name and email are always synced.
|
When authenticating using LDAP, the user's name and email are always synced.
|
||||||
|
|
||||||
|
@ -288,13 +287,20 @@ When authenticating using LDAP, the user's name and email are always synced.
|
||||||
|
|
||||||
## Bypass two-factor authentication
|
## Bypass two-factor authentication
|
||||||
|
|
||||||
In GitLab 12.3 or later, users can sign in with specified providers _without_
|
> Introduced in GitLab 12.3.
|
||||||
using two factor authentication.
|
|
||||||
|
|
||||||
Define the allowed providers using an array (for example, `["twitter", 'google_oauth2']`),
|
With certain OmniAuth providers, users can sign in without
|
||||||
or as `true` or `false` to allow all providers (or none). This option should be
|
using two-factor authentication.
|
||||||
configured only for providers which already have two factor authentication
|
|
||||||
(default: false). This configuration doesn't apply to SAML.
|
To bypass two-factor authentication, you can either:
|
||||||
|
|
||||||
|
- Define the allowed providers using an array (for example, `['twitter', 'google_oauth2']`).
|
||||||
|
- Specify `true` to allow all providers, or `false` to allow none.
|
||||||
|
|
||||||
|
This option should be configured only for providers that already have
|
||||||
|
two-factor authentication. The default is `false`.
|
||||||
|
|
||||||
|
This configuration doesn't apply to SAML.
|
||||||
|
|
||||||
- **For Omnibus package**
|
- **For Omnibus package**
|
||||||
|
|
||||||
|
@ -313,10 +319,10 @@ configured only for providers which already have two factor authentication
|
||||||
|
|
||||||
You can add the `auto_sign_in_with_provider` setting to your GitLab
|
You can add the `auto_sign_in_with_provider` setting to your GitLab
|
||||||
configuration to redirect login requests to your OmniAuth provider for
|
configuration to redirect login requests to your OmniAuth provider for
|
||||||
authentication. This removes the need to click a button before actually signing in.
|
authentication. This removes the need to select the provider before signing in.
|
||||||
|
|
||||||
For example, when using the [Azure v2 integration](azure.md#microsoft-azure-oauth-20-omniauth-provider-v2), set the following to enable auto
|
For example, to enable automatic sign-in for the
|
||||||
sign-in:
|
[Azure v2 integration](azure.md#microsoft-azure-oauth-20-omniauth-provider-v2):
|
||||||
|
|
||||||
- **For Omnibus package**
|
- **For Omnibus package**
|
||||||
|
|
||||||
|
@ -332,10 +338,10 @@ sign-in:
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep in mind that every sign-in attempt is redirected to the OmniAuth
|
Keep in mind that every sign-in attempt is redirected to the OmniAuth
|
||||||
provider; you can't sign in using local credentials. Ensure at least
|
provider, so you can't sign in using local credentials. Ensure at least
|
||||||
one of the OmniAuth users has an administrator role.
|
one of the OmniAuth users is an administrator.
|
||||||
|
|
||||||
You may also bypass the auto sign in feature by browsing to
|
You can also bypass automatic sign-in by browsing to
|
||||||
`https://gitlab.example.com/users/sign_in?auto_sign_in=false`.
|
`https://gitlab.example.com/users/sign_in?auto_sign_in=false`.
|
||||||
|
|
||||||
## Passwords for users created via OmniAuth
|
## Passwords for users created via OmniAuth
|
||||||
|
@ -344,11 +350,12 @@ The [Generated passwords for users created through integrated authentication](..
|
||||||
guide provides an overview about how GitLab generates and sets passwords for
|
guide provides an overview about how GitLab generates and sets passwords for
|
||||||
users created with OmniAuth.
|
users created with OmniAuth.
|
||||||
|
|
||||||
## Custom OmniAuth provider icon
|
## Use a custom OmniAuth provider icon
|
||||||
|
|
||||||
Most supported providers include a built-in icon for the rendered sign-in button.
|
Most supported providers include a built-in icon for the rendered sign-in button.
|
||||||
After you ensure your image is optimized for rendering at 64 x 64 pixels,
|
|
||||||
you can override this icon in one of two ways:
|
To use your own icon, ensure your image is optimized for rendering at 64 x 64 pixels,
|
||||||
|
then override the icon in one of two ways:
|
||||||
|
|
||||||
- **Provide a custom image path**:
|
- **Provide a custom image path**:
|
||||||
|
|
||||||
|
@ -359,11 +366,11 @@ you can override this icon in one of two ways:
|
||||||
to your GitLab configuration file. Read [OpenID Connect OmniAuth provider](../administration/auth/oidc.md)
|
to your GitLab configuration file. Read [OpenID Connect OmniAuth provider](../administration/auth/oidc.md)
|
||||||
for an example for the OpenID Connect provider.
|
for an example for the OpenID Connect provider.
|
||||||
|
|
||||||
- **Directly embed an image in a configuration file**: This example creates a Base64-encoded
|
- **Embed an image directly in a configuration file**: This example creates a Base64-encoded
|
||||||
version of your image you can serve through a
|
version of your image you can serve through a
|
||||||
[Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs):
|
[Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs):
|
||||||
|
|
||||||
1. Encode your image file with GNU `base64` command (such as `base64 -w 0 <logo.png>`)
|
1. Encode your image file with a GNU `base64` command (such as `base64 -w 0 <logo.png>`)
|
||||||
which returns a single-line `<base64-data>` string.
|
which returns a single-line `<base64-data>` string.
|
||||||
1. Add the Base64-encoded data to a custom `icon` parameter in your GitLab
|
1. Add the Base64-encoded data to a custom `icon` parameter in your GitLab
|
||||||
configuration file:
|
configuration file:
|
||||||
|
|
|
@ -68,7 +68,7 @@ You can set a global prefix for all generated Personal Access Tokens.
|
||||||
A prefix can help you identify PATs visually, as well as with automation tools.
|
A prefix can help you identify PATs visually, as well as with automation tools.
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
For GitLab.com and new self-managed instances, the default prefix is `glpat-`.
|
For GitLab.com and self-managed instances, the default prefix is `glpat-`.
|
||||||
|
|
||||||
### Set a prefix
|
### Set a prefix
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,10 @@ The Agent can be configured to enable access to the CI/CD Tunnel to other projec
|
||||||
|
|
||||||
You can read more on how to [authorize access in the Agent configuration reference](repository.md#authorize-projects-and-groups-to-use-an-agent).
|
You can read more on how to [authorize access in the Agent configuration reference](repository.md#authorize-projects-and-groups-to-use-an-agent).
|
||||||
|
|
||||||
|
## Restrict access of authorized projects and groups **(PREMIUM)**
|
||||||
|
|
||||||
|
You can [configure various impersonations](repository.md#use-impersonation-to-restrict-project-and-group-access) to restrict the permissions of a shared CI/CD Tunnel.
|
||||||
|
|
||||||
## Example for a `kubectl` command using the CI/CD Tunnel
|
## Example for a `kubectl` command using the CI/CD Tunnel
|
||||||
|
|
||||||
The following example shows a CI/CD job that runs a `kubectl` command using the CI/CD Tunnel.
|
The following example shows a CI/CD job that runs a `kubectl` command using the CI/CD Tunnel.
|
||||||
|
|
|
@ -198,6 +198,87 @@ To grant access to all projects within a group:
|
||||||
- id: path/to/group/subgroup
|
- id: path/to/group/subgroup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use impersonation to restrict project and group access **(PREMIUM)**
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345014) in GitLab 14.5.
|
||||||
|
|
||||||
|
By default, the [CI/CD Tunnel](ci_cd_tunnel.md) inherits all the permissions from the service account used to install the
|
||||||
|
Agent in the cluster.
|
||||||
|
To restrict access to your cluster, you can use [impersonation](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation).
|
||||||
|
|
||||||
|
To specify impersonations, use the `access_as` attribute in your Agent's configuration file and use Kubernetes RBAC rules to manage impersonated account permissions.
|
||||||
|
|
||||||
|
You can impersonate:
|
||||||
|
|
||||||
|
- The Agent itself (default).
|
||||||
|
- The CI job that accesses the cluster.
|
||||||
|
- A specific user or system account defined within the cluster.
|
||||||
|
|
||||||
|
#### Impersonate the Agent
|
||||||
|
|
||||||
|
The Agent is impersonated by default. You don't need to do anything to impersonate it.
|
||||||
|
|
||||||
|
#### Impersonate the CI job that accesses the cluster
|
||||||
|
|
||||||
|
To impersonate the CI job that accesses the cluster, add the `ci_job: {}` key-value
|
||||||
|
under the `access_as` key.
|
||||||
|
|
||||||
|
When the agent makes the request to the actual Kubernetes API, it sets the
|
||||||
|
impersonation credentials in the following way:
|
||||||
|
|
||||||
|
- `UserName` is set to `gitlab:ci_job:<job id>`. Example: `gitlab:ci_job:1074499489`.
|
||||||
|
- `Groups` is set to:
|
||||||
|
- `gitlab:ci_job` to identify all requests coming from CI jobs.
|
||||||
|
- The list of IDs of groups the project is in.
|
||||||
|
- The project ID.
|
||||||
|
- The slug of the environment this job belongs to.
|
||||||
|
|
||||||
|
Example: for a CI job in `group1/group1-1/project1` where:
|
||||||
|
|
||||||
|
- Group `group1` has ID 23.
|
||||||
|
- Group `group1/group1-1` has ID 25.
|
||||||
|
- Project `group1/group1-1/project1` has ID 150.
|
||||||
|
- Job running in a prod environment.
|
||||||
|
|
||||||
|
Group list would be `[gitlab:ci_job, gitlab:group:23, gitlab:group:25, gitlab:project:150, gitlab:project_env:150:prod]`.
|
||||||
|
|
||||||
|
- `Extra` carries extra information about the request. The following properties are set on the impersonated identity:
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
| -------- | ----------- |
|
||||||
|
| `agent.gitlab.com/id` | Contains the agent ID. |
|
||||||
|
| `agent.gitlab.com/config_project_id` | Contains the agent's configuration project ID. |
|
||||||
|
| `agent.gitlab.com/project_id` | Contains the CI project ID. |
|
||||||
|
| `agent.gitlab.com/ci_pipeline_id` | Contains the CI pipeline ID. |
|
||||||
|
| `agent.gitlab.com/ci_job_id` | Contains the CI job ID. |
|
||||||
|
| `agent.gitlab.com/username` | Contains the username of the user the CI job is running as. |
|
||||||
|
| `agent.gitlab.com/environment_slug` | Contains the slug of the environment. Only set if running in an environment. |
|
||||||
|
|
||||||
|
Example to restrict access by the CI job's identity:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ci_access:
|
||||||
|
projects:
|
||||||
|
- id: path/to/project
|
||||||
|
access_as:
|
||||||
|
ci_job: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Impersonate a static identity
|
||||||
|
|
||||||
|
For the given CI/CD Tunnel connection, you can use a static identity for the impersonation.
|
||||||
|
|
||||||
|
Add the `impersonate` key under the `access_as` key to make the request using the provided identity.
|
||||||
|
|
||||||
|
The identity can be specified with the following keys:
|
||||||
|
|
||||||
|
- `username` (required)
|
||||||
|
- `uid`
|
||||||
|
- `groups`
|
||||||
|
- `extra`
|
||||||
|
|
||||||
|
See the [official Kubernetes documentation for more details](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation) on the usage of these keys.
|
||||||
|
|
||||||
## Surface network security alerts from cluster to GitLab **(ULTIMATE)**
|
## Surface network security alerts from cluster to GitLab **(ULTIMATE)**
|
||||||
|
|
||||||
The GitLab Agent provides an [integration with Cilium](index.md#kubernetes-network-security-alerts).
|
The GitLab Agent provides an [integration with Cilium](index.md#kubernetes-network-security-alerts).
|
||||||
|
|
|
@ -6,6 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Container Host Security **(FREE)**
|
# Container Host Security **(FREE)**
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
|
||||||
|
You can continue using Container Host Security, even though it relies on this certificate-based
|
||||||
|
method. The work to allow all aspects of Container Host Security to function through the [GitLab Kubernetes Agent](../../../../clusters/agent/index.md)
|
||||||
|
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350).
|
||||||
|
|
||||||
Container Host Security in GitLab provides Intrusion Detection and Prevention capabilities that can
|
Container Host Security in GitLab provides Intrusion Detection and Prevention capabilities that can
|
||||||
monitor and (optionally) block activity inside the containers themselves. This is done by leveraging
|
monitor and (optionally) block activity inside the containers themselves. This is done by leveraging
|
||||||
an integration with Falco to provide the monitoring capabilities and an integration with Pod
|
an integration with Falco to provide the monitoring capabilities and an integration with Pod
|
||||||
|
|
|
@ -6,6 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Container Network Security **(FREE)**
|
# Container Network Security **(FREE)**
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
|
||||||
|
You can continue using Container Network Security, even though it relies on this certificate-based
|
||||||
|
method. The work to allow all aspects of Container Network Security to function through the [GitLab Kubernetes Agent](../../../../clusters/agent/index.md)
|
||||||
|
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350) and [this GitLab Epic](https://gitlab.com/groups/gitlab-org/-/epics/7057).
|
||||||
|
|
||||||
Container Network Security in GitLab provides basic firewall functionality by leveraging Cilium
|
Container Network Security in GitLab provides basic firewall functionality by leveraging Cilium
|
||||||
NetworkPolicies to filter traffic going in and out of the cluster as well as traffic between pods
|
NetworkPolicies to filter traffic going in and out of the cluster as well as traffic between pods
|
||||||
inside the cluster. Container Network Security can be used to enforce L3, L4, and L7 policies and
|
inside the cluster. Container Network Security can be used to enforce L3, L4, and L7 policies and
|
||||||
|
|
|
@ -34,7 +34,7 @@ For examples of how you can use a project access token to authenticate with the
|
||||||
[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
|
[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
For GitLab.com and new self-managed instances, the default prefix is `glpat-`.
|
For GitLab.com and self-managed instances, the default prefix is `glpat-`.
|
||||||
|
|
||||||
## Creating a project access token
|
## Creating a project access token
|
||||||
|
|
||||||
|
|
|
@ -41698,6 +41698,18 @@ msgstr ""
|
||||||
msgid "pipeline schedules documentation"
|
msgid "pipeline schedules documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "pipelineEditorWalkthrough|Let's do this!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "pipelineEditorWalkthrough|See how GitLab pipelines work"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
|
msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,11 @@ RSpec.describe Admin::RunnersController do
|
||||||
describe '#index' do
|
describe '#index' do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
it 'lists all runners' do
|
before do
|
||||||
get :index
|
get :index
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders index template' do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(response).to render_template(:index)
|
expect(response).to render_template(:index)
|
||||||
end
|
end
|
||||||
|
|
|
@ -252,6 +252,30 @@ RSpec.describe Import::BitbucketController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when exceptions occur" do
|
||||||
|
shared_examples "handles exceptions" do
|
||||||
|
it "logs an exception" do
|
||||||
|
expect(Bitbucket::Client).to receive(:new).and_raise(error)
|
||||||
|
expect(controller).to receive(:log_exception)
|
||||||
|
|
||||||
|
post :create, format: :json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for OAuth2 errors" do
|
||||||
|
let(:fake_response) { double('Faraday::Response', headers: {}, body: '', status: 403) }
|
||||||
|
let(:error) { OAuth2::Error.new(OAuth2::Response.new(fake_response)) }
|
||||||
|
|
||||||
|
it_behaves_like "handles exceptions"
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for Bitbucket errors" do
|
||||||
|
let(:error) { Bitbucket::Error::Unauthorized.new("error") }
|
||||||
|
|
||||||
|
it_behaves_like "handles exceptions"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'user has chosen an existing nested namespace and name for the project' do
|
context 'user has chosen an existing nested namespace and name for the project' do
|
||||||
|
|
|
@ -6,6 +6,8 @@ RSpec.describe Projects::Ci::PipelineEditorController do
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
subject(:show_request) { get :show, params: { namespace_id: project.namespace, project_id: project } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
@ -14,8 +16,7 @@ RSpec.describe Projects::Ci::PipelineEditorController do
|
||||||
context 'with enough privileges' do
|
context 'with enough privileges' do
|
||||||
before do
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
|
show_request
|
||||||
get :show, params: { namespace_id: project.namespace, project_id: project }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(response).to have_gitlab_http_status(:ok) }
|
it { expect(response).to have_gitlab_http_status(:ok) }
|
||||||
|
@ -28,13 +29,27 @@ RSpec.describe Projects::Ci::PipelineEditorController do
|
||||||
context 'without enough privileges' do
|
context 'without enough privileges' do
|
||||||
before do
|
before do
|
||||||
project.add_reporter(user)
|
project.add_reporter(user)
|
||||||
|
show_request
|
||||||
get :show, params: { namespace_id: project.namespace, project_id: project }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'responds with 404' do
|
it 'responds with 404' do
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'pipeline_editor_walkthrough experiment' do
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tracks the assignment', :experiment do
|
||||||
|
expect(experiment(:pipeline_editor_walkthrough))
|
||||||
|
.to track(:assignment)
|
||||||
|
.with_context(actor: user)
|
||||||
|
.on_next_instance
|
||||||
|
|
||||||
|
show_request
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,11 +66,11 @@ RSpec.describe "Admin Runners" do
|
||||||
visit admin_runners_path
|
visit admin_runners_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'runner type can be selected' do
|
it 'runner types tabs have total counts and can be selected' do
|
||||||
expect(page).to have_link('All')
|
expect(page).to have_link('All 2')
|
||||||
expect(page).to have_link('Instance')
|
expect(page).to have_link('Instance 2')
|
||||||
expect(page).to have_link('Group')
|
expect(page).to have_link('Group 0')
|
||||||
expect(page).to have_link('Project')
|
expect(page).to have_link('Project 0')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows runners' do
|
it 'shows runners' do
|
||||||
|
@ -162,10 +162,12 @@ RSpec.describe "Admin Runners" do
|
||||||
create(:ci_runner, :group, description: 'runner-group', groups: [group])
|
create(:ci_runner, :group, description: 'runner-group', groups: [group])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows correct runner when type matches' do
|
it '"All" tab is selected by default' do
|
||||||
visit admin_runners_path
|
visit admin_runners_path
|
||||||
|
|
||||||
expect(page).to have_link('All', class: 'active')
|
page.within('[data-testid="runner-type-tabs"]') do
|
||||||
|
expect(page).to have_link('All', class: 'active')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows correct runner when type matches' do
|
it 'shows correct runner when type matches' do
|
||||||
|
@ -174,9 +176,11 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).to have_content 'runner-project'
|
expect(page).to have_content 'runner-project'
|
||||||
expect(page).to have_content 'runner-group'
|
expect(page).to have_content 'runner-group'
|
||||||
|
|
||||||
click_on 'Project'
|
page.within('[data-testid="runner-type-tabs"]') do
|
||||||
|
click_on('Project')
|
||||||
|
|
||||||
expect(page).to have_link('Project', class: 'active')
|
expect(page).to have_link('Project', class: 'active')
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_content 'runner-project'
|
expect(page).to have_content 'runner-project'
|
||||||
expect(page).not_to have_content 'runner-group'
|
expect(page).not_to have_content 'runner-group'
|
||||||
|
@ -185,9 +189,11 @@ RSpec.describe "Admin Runners" do
|
||||||
it 'shows no runner when type does not match' do
|
it 'shows no runner when type does not match' do
|
||||||
visit admin_runners_path
|
visit admin_runners_path
|
||||||
|
|
||||||
click_on 'Instance'
|
page.within('[data-testid="runner-type-tabs"]') do
|
||||||
|
click_on 'Instance'
|
||||||
|
|
||||||
expect(page).to have_link('Instance', class: 'active')
|
expect(page).to have_link('Instance', class: 'active')
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).not_to have_content 'runner-project'
|
expect(page).not_to have_content 'runner-project'
|
||||||
expect(page).not_to have_content 'runner-group'
|
expect(page).not_to have_content 'runner-group'
|
||||||
|
@ -200,7 +206,9 @@ RSpec.describe "Admin Runners" do
|
||||||
|
|
||||||
visit admin_runners_path
|
visit admin_runners_path
|
||||||
|
|
||||||
click_on 'Project'
|
page.within('[data-testid="runner-type-tabs"]') do
|
||||||
|
click_on 'Project'
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_content 'runner-project'
|
expect(page).to have_content 'runner-project'
|
||||||
expect(page).to have_content 'runner-2-project'
|
expect(page).to have_content 'runner-2-project'
|
||||||
|
@ -224,7 +232,9 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).to have_content 'runner-group'
|
expect(page).to have_content 'runner-group'
|
||||||
expect(page).not_to have_content 'runner-paused-project'
|
expect(page).not_to have_content 'runner-paused-project'
|
||||||
|
|
||||||
click_on 'Project'
|
page.within('[data-testid="runner-type-tabs"]') do
|
||||||
|
click_on 'Project'
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_content 'runner-project'
|
expect(page).to have_content 'runner-project'
|
||||||
expect(page).not_to have_content 'runner-group'
|
expect(page).not_to have_content 'runner-group'
|
||||||
|
|
|
@ -238,8 +238,10 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
||||||
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
|
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
|
||||||
write_comment_on_line(line_holder, diff_side)
|
write_comment_on_line(line_holder, diff_side)
|
||||||
|
|
||||||
accept_confirm do
|
find('.js-close-discussion-note-form').click
|
||||||
find('.js-close-discussion-note-form').click
|
|
||||||
|
page.within('.modal') do
|
||||||
|
click_button 'OK'
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_comment_dismissal(line_holder)
|
assert_comment_dismissal(line_holder)
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
|
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
|
||||||
import { createStore } from '~/mr_notes/stores';
|
import { createStore } from '~/mr_notes/stores';
|
||||||
import NoteForm from '~/notes/components/note_form.vue';
|
import NoteForm from '~/notes/components/note_form.vue';
|
||||||
|
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||||
import { noteableDataMock } from '../../notes/mock_data';
|
import { noteableDataMock } from '../../notes/mock_data';
|
||||||
import diffFileMockData from '../mock_data/diff_file';
|
import diffFileMockData from '../mock_data/diff_file';
|
||||||
|
|
||||||
|
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
|
||||||
|
return {
|
||||||
|
confirmAction: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('DiffLineNoteForm', () => {
|
describe('DiffLineNoteForm', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let diffFile;
|
let diffFile;
|
||||||
|
@ -36,49 +44,56 @@ describe('DiffLineNoteForm', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findNoteForm = () => wrapper.findComponent(NoteForm);
|
||||||
|
|
||||||
describe('methods', () => {
|
describe('methods', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = createComponent();
|
wrapper = createComponent();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleCancelCommentForm', () => {
|
describe('handleCancelCommentForm', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
confirmAction.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
|
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
|
||||||
jest.spyOn(window, 'confirm').mockReturnValue(false);
|
confirmAction.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
wrapper.vm.handleCancelCommentForm(true, true);
|
findNoteForm().vm.$emit('cancelForm', true, true);
|
||||||
|
|
||||||
expect(window.confirm).toHaveBeenCalled();
|
expect(confirmAction).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ask for confirmation when one of the params false', () => {
|
it('should not ask for confirmation when one of the params false', () => {
|
||||||
jest.spyOn(window, 'confirm').mockReturnValue(false);
|
confirmAction.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
wrapper.vm.handleCancelCommentForm(true, false);
|
findNoteForm().vm.$emit('cancelForm', true, false);
|
||||||
|
|
||||||
expect(window.confirm).not.toHaveBeenCalled();
|
expect(confirmAction).not.toHaveBeenCalled();
|
||||||
|
|
||||||
wrapper.vm.handleCancelCommentForm(false, true);
|
findNoteForm().vm.$emit('cancelForm', false, true);
|
||||||
|
|
||||||
expect(window.confirm).not.toHaveBeenCalled();
|
expect(confirmAction).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call cancelCommentForm with lineCode', (done) => {
|
it('should call cancelCommentForm with lineCode', async () => {
|
||||||
jest.spyOn(window, 'confirm').mockImplementation(() => {});
|
confirmAction.mockResolvedValueOnce(true);
|
||||||
jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {});
|
jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {});
|
||||||
jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {});
|
jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {});
|
||||||
wrapper.vm.handleCancelCommentForm();
|
|
||||||
|
|
||||||
expect(window.confirm).not.toHaveBeenCalled();
|
findNoteForm().vm.$emit('cancelForm', true, true);
|
||||||
wrapper.vm.$nextTick(() => {
|
|
||||||
expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
|
|
||||||
lineCode: diffLines[1].line_code,
|
|
||||||
fileHash: wrapper.vm.diffFileHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
|
await nextTick();
|
||||||
|
|
||||||
done();
|
expect(confirmAction).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
|
||||||
|
lineCode: diffLines[1].line_code,
|
||||||
|
fileHash: wrapper.vm.diffFileHash,
|
||||||
});
|
});
|
||||||
|
expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,9 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
|
||||||
|
|
||||||
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
|
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
|
||||||
|
|
||||||
|
const scrollIntoViewMock = jest.fn();
|
||||||
|
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||||
|
|
||||||
describe('Pipeline Editor | Commit Form', () => {
|
describe('Pipeline Editor | Commit Form', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
@ -113,4 +116,20 @@ describe('Pipeline Editor | Commit Form', () => {
|
||||||
expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
|
expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when scrollToCommitForm becomes true', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
createComponent();
|
||||||
|
wrapper.setProps({ scrollToCommitForm: true });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls into view', () => {
|
||||||
|
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits "scrolled-to-commit-form"', () => {
|
||||||
|
expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -277,4 +277,16 @@ describe('Pipeline Editor | Commit section', () => {
|
||||||
expect(wrapper.emitted('resetContent')).toHaveLength(1);
|
expect(wrapper.emitted('resetContent')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets listeners on commit form', () => {
|
||||||
|
const handler = jest.fn();
|
||||||
|
createComponent({ options: { listeners: { event: handler } } });
|
||||||
|
findCommitForm().vm.$emit('event');
|
||||||
|
expect(handler).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes down scroll-to-commit-form prop to commit form', () => {
|
||||||
|
createComponent({ props: { 'scroll-to-commit-form': true } });
|
||||||
|
expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { GlButton } from '@gitlab/ui';
|
import { GlButton } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
import { stubExperiments } from 'helpers/experimentation_helper';
|
||||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||||
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
|
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
|
||||||
import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
|
import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
|
||||||
|
@ -33,19 +34,41 @@ describe('Pipeline editor drawer', () => {
|
||||||
|
|
||||||
const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
|
const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
|
||||||
|
|
||||||
|
const originalObjects = [];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalObjects.push(window.gon, window.gl);
|
||||||
|
stubExperiments({ pipeline_editor_walkthrough: 'control' });
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
[window.gon, window.gl] = originalObjects;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it sets the drawer to be opened by default', async () => {
|
describe('default expanded state', () => {
|
||||||
createComponent();
|
describe('when experiment control', () => {
|
||||||
|
it('sets the drawer to be opened by default', async () => {
|
||||||
|
createComponent();
|
||||||
|
expect(findDrawerContent().exists()).toBe(false);
|
||||||
|
await nextTick();
|
||||||
|
expect(findDrawerContent().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(findDrawerContent().exists()).toBe(false);
|
describe('when experiment candidate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
|
||||||
|
});
|
||||||
|
|
||||||
await nextTick();
|
it('sets the drawer to be closed by default', async () => {
|
||||||
|
createComponent();
|
||||||
expect(findDrawerContent().exists()).toBe(true);
|
expect(findDrawerContent().exists()).toBe(false);
|
||||||
|
await nextTick();
|
||||||
|
expect(findDrawerContent().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the drawer is collapsed', () => {
|
describe('when the drawer is collapsed', () => {
|
||||||
|
|
|
@ -141,8 +141,8 @@ describe('Pipeline editor branch switcher', () => {
|
||||||
createComponentWithApollo();
|
createComponentWithApollo();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render dropdown', () => {
|
it('disables the dropdown', () => {
|
||||||
expect(findDropdown().exists()).toBe(false);
|
expect(findDropdown().props('disabled')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ describe('Pipeline editor branch switcher', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render dropdown', () => {
|
it('does not render dropdown', () => {
|
||||||
expect(findDropdown().exists()).toBe(false);
|
expect(findDropdown().props('disabled')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows an error message', () => {
|
it('shows an error message', () => {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
|
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
|
||||||
import { shallowMount, mount } from '@vue/test-utils';
|
import { shallowMount, mount } from '@vue/test-utils';
|
||||||
import { nextTick } from 'vue';
|
import Vue, { nextTick } from 'vue';
|
||||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||||
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
|
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
|
||||||
|
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
|
||||||
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
|
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
|
||||||
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
|
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
|
||||||
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
|
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
|
||||||
|
import { stubExperiments } from 'helpers/experimentation_helper';
|
||||||
import {
|
import {
|
||||||
CREATE_TAB,
|
CREATE_TAB,
|
||||||
EDITOR_APP_STATUS_EMPTY,
|
EDITOR_APP_STATUS_EMPTY,
|
||||||
|
@ -19,6 +21,8 @@ import {
|
||||||
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
||||||
import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
|
import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
|
||||||
|
|
||||||
|
Vue.config.ignoredElements = ['gl-emoji'];
|
||||||
|
|
||||||
describe('Pipeline editor tabs component', () => {
|
describe('Pipeline editor tabs component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const MockTextEditor = {
|
const MockTextEditor = {
|
||||||
|
@ -26,6 +30,7 @@ describe('Pipeline editor tabs component', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = ({
|
const createComponent = ({
|
||||||
|
listeners = {},
|
||||||
props = {},
|
props = {},
|
||||||
provide = {},
|
provide = {},
|
||||||
appStatus = EDITOR_APP_STATUS_VALID,
|
appStatus = EDITOR_APP_STATUS_VALID,
|
||||||
|
@ -35,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
|
||||||
propsData: {
|
propsData: {
|
||||||
ciConfigData: mockLintResponse,
|
ciConfigData: mockLintResponse,
|
||||||
ciFileContent: mockCiYml,
|
ciFileContent: mockCiYml,
|
||||||
|
isNewCiConfigFile: true,
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -47,6 +53,7 @@ describe('Pipeline editor tabs component', () => {
|
||||||
TextEditor: MockTextEditor,
|
TextEditor: MockTextEditor,
|
||||||
EditorTab,
|
EditorTab,
|
||||||
},
|
},
|
||||||
|
listeners,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,6 +69,7 @@ describe('Pipeline editor tabs component', () => {
|
||||||
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
|
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
|
||||||
const findTextEditor = () => wrapper.findComponent(MockTextEditor);
|
const findTextEditor = () => wrapper.findComponent(MockTextEditor);
|
||||||
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
|
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
|
||||||
|
const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
@ -236,4 +244,63 @@ describe('Pipeline editor tabs component', () => {
|
||||||
expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
|
expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pipeline_editor_walkthrough experiment', () => {
|
||||||
|
describe('when in control path', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
stubExperiments({ pipeline_editor_walkthrough: 'control' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show walkthrough popover', async () => {
|
||||||
|
createComponent({ mountFn: mount });
|
||||||
|
await nextTick();
|
||||||
|
expect(findWalkthroughPopover().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when in candidate path', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when isNewCiConfigFile prop is true (default)', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
createComponent({
|
||||||
|
mountFn: mount,
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows walkthrough popover', async () => {
|
||||||
|
expect(findWalkthroughPopover().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when isNewCiConfigFile prop is false', () => {
|
||||||
|
it('does not show walkthrough popover', async () => {
|
||||||
|
createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount });
|
||||||
|
await nextTick();
|
||||||
|
expect(findWalkthroughPopover().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets listeners on walkthrough popover', async () => {
|
||||||
|
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
|
||||||
|
|
||||||
|
const handler = jest.fn();
|
||||||
|
|
||||||
|
createComponent({
|
||||||
|
mountFn: mount,
|
||||||
|
listeners: {
|
||||||
|
event: handler,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
findWalkthroughPopover().vm.$emit('event');
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { mount, shallowMount } from '@vue/test-utils';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
|
||||||
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
|
|
||||||
|
Vue.config.ignoredElements = ['gl-emoji'];
|
||||||
|
|
||||||
|
describe('WalkthroughPopover component', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const createComponent = (mountFn = shallowMount) => {
|
||||||
|
return extendedWrapper(mountFn(WalkthroughPopover));
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CTA button clicked', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = createComponent(mount);
|
||||||
|
await wrapper.findByTestId('ctaBtn').trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits "walkthrough-popover-cta-clicked" event', async () => {
|
||||||
|
expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -152,4 +152,27 @@ describe('Pipeline editor home wrapper', () => {
|
||||||
expect(findCommitSection().exists()).toBe(true);
|
expect(findCommitSection().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('WalkthroughPopover events', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => {
|
||||||
|
it('passes down `scrollToCommitForm=true` to commit section', async () => {
|
||||||
|
expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
|
||||||
|
await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
|
||||||
|
expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when "scrolled-to-commit-form" is emitted from commit section', () => {
|
||||||
|
it('passes down `scrollToCommitForm=false` to commit section', async () => {
|
||||||
|
await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
|
||||||
|
expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
|
||||||
|
await findCommitSection().vm.$emit('scrolled-to-commit-form');
|
||||||
|
expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import { updateHistory } from '~/lib/utils/url_utility';
|
import { updateHistory } from '~/lib/utils/url_utility';
|
||||||
|
|
||||||
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
|
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
|
||||||
|
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
|
||||||
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
||||||
import RunnerList from '~/runner/components/runner_list.vue';
|
import RunnerList from '~/runner/components/runner_list.vue';
|
||||||
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
||||||
|
@ -33,7 +34,11 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
|
||||||
import { runnersData, runnersDataPaginated } from '../mock_data';
|
import { runnersData, runnersDataPaginated } from '../mock_data';
|
||||||
|
|
||||||
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
|
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
|
||||||
const mockActiveRunnersCount = 2;
|
const mockActiveRunnersCount = '2';
|
||||||
|
const mockAllRunnersCount = '6';
|
||||||
|
const mockInstanceRunnersCount = '3';
|
||||||
|
const mockGroupRunnersCount = '2';
|
||||||
|
const mockProjectRunnersCount = '1';
|
||||||
|
|
||||||
jest.mock('~/flash');
|
jest.mock('~/flash');
|
||||||
jest.mock('~/runner/sentry_utils');
|
jest.mock('~/runner/sentry_utils');
|
||||||
|
@ -50,6 +55,7 @@ describe('AdminRunnersApp', () => {
|
||||||
let mockRunnersQuery;
|
let mockRunnersQuery;
|
||||||
|
|
||||||
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
||||||
|
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
||||||
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
||||||
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
||||||
const findRunnerPaginationPrev = () =>
|
const findRunnerPaginationPrev = () =>
|
||||||
|
@ -65,8 +71,12 @@ describe('AdminRunnersApp', () => {
|
||||||
localVue,
|
localVue,
|
||||||
apolloProvider: createMockApollo(handlers),
|
apolloProvider: createMockApollo(handlers),
|
||||||
propsData: {
|
propsData: {
|
||||||
activeRunnersCount: mockActiveRunnersCount,
|
|
||||||
registrationToken: mockRegistrationToken,
|
registrationToken: mockRegistrationToken,
|
||||||
|
activeRunnersCount: mockActiveRunnersCount,
|
||||||
|
allRunnersCount: mockAllRunnersCount,
|
||||||
|
instanceRunnersCount: mockInstanceRunnersCount,
|
||||||
|
groupRunnersCount: mockGroupRunnersCount,
|
||||||
|
projectRunnersCount: mockProjectRunnersCount,
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -85,6 +95,16 @@ describe('AdminRunnersApp', () => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the runner tabs with a runner count', async () => {
|
||||||
|
createComponent({ mountFn: mount });
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
|
||||||
|
`All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows the runner setup instructions', () => {
|
it('shows the runner setup instructions', () => {
|
||||||
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
|
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
|
||||||
expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
|
expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
|
||||||
|
|
|
@ -14,11 +14,16 @@ describe('RunnerTypeTabs', () => {
|
||||||
.filter((tab) => tab.attributes('active') === 'true')
|
.filter((tab) => tab.attributes('active') === 'true')
|
||||||
.at(0);
|
.at(0);
|
||||||
|
|
||||||
const createComponent = ({ value = mockSearch } = {}) => {
|
const createComponent = ({ props, ...options } = {}) => {
|
||||||
wrapper = shallowMount(RunnerTypeTabs, {
|
wrapper = shallowMount(RunnerTypeTabs, {
|
||||||
propsData: {
|
propsData: {
|
||||||
value,
|
value: mockSearch,
|
||||||
|
...props,
|
||||||
},
|
},
|
||||||
|
stubs: {
|
||||||
|
GlTab,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,7 +36,7 @@ describe('RunnerTypeTabs', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders options to filter runners', () => {
|
it('Renders options to filter runners', () => {
|
||||||
expect(findTabs().wrappers.map((tab) => tab.attributes('title'))).toEqual([
|
expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
|
||||||
'All',
|
'All',
|
||||||
'Instance',
|
'Instance',
|
||||||
'Group',
|
'Group',
|
||||||
|
@ -40,18 +45,20 @@ describe('RunnerTypeTabs', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"All" is selected by default', () => {
|
it('"All" is selected by default', () => {
|
||||||
expect(findActiveTab().attributes('title')).toBe('All');
|
expect(findActiveTab().text()).toBe('All');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Another tab can be preselected by the user', () => {
|
it('Another tab can be preselected by the user', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
value: {
|
props: {
|
||||||
...mockSearch,
|
value: {
|
||||||
runnerType: INSTANCE_TYPE,
|
...mockSearch,
|
||||||
|
runnerType: INSTANCE_TYPE,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findActiveTab().attributes('title')).toBe('Instance');
|
expect(findActiveTab().text()).toBe('Instance');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When the user selects a tab', () => {
|
describe('When the user selects a tab', () => {
|
||||||
|
@ -72,7 +79,31 @@ describe('RunnerTypeTabs', () => {
|
||||||
const newValue = emittedValue();
|
const newValue = emittedValue();
|
||||||
await wrapper.setProps({ value: newValue });
|
await wrapper.setProps({ value: newValue });
|
||||||
|
|
||||||
expect(findActiveTab().attributes('title')).toBe('Group');
|
expect(findActiveTab().text()).toBe('Group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When using a custom slot', () => {
|
||||||
|
const mockContent = 'content';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({
|
||||||
|
scopedSlots: {
|
||||||
|
title: `
|
||||||
|
<span>
|
||||||
|
{{props.tab.title}} ${mockContent}
|
||||||
|
</span>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders tabs with additional information', () => {
|
||||||
|
expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
|
||||||
|
`All ${mockContent}`,
|
||||||
|
`Instance ${mockContent}`,
|
||||||
|
`Group ${mockContent}`,
|
||||||
|
`Project ${mockContent}`,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Ci::RunnersHelper do
|
RSpec.describe Ci::RunnersHelper do
|
||||||
let_it_be(:user, refind: true) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(helper).to receive(:current_user).and_return(user)
|
allow(helper).to receive(:current_user).and_return(user)
|
||||||
|
@ -12,22 +12,22 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
describe '#runner_status_icon', :clean_gitlab_redis_cache do
|
describe '#runner_status_icon', :clean_gitlab_redis_cache do
|
||||||
it "returns - not contacted yet" do
|
it "returns - not contacted yet" do
|
||||||
runner = create(:ci_runner)
|
runner = create(:ci_runner)
|
||||||
expect(runner_status_icon(runner)).to include("not connected yet")
|
expect(helper.runner_status_icon(runner)).to include("not connected yet")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns offline text" do
|
it "returns offline text" do
|
||||||
runner = create(:ci_runner, contacted_at: 1.day.ago, active: true)
|
runner = create(:ci_runner, contacted_at: 1.day.ago, active: true)
|
||||||
expect(runner_status_icon(runner)).to include("Runner is offline")
|
expect(helper.runner_status_icon(runner)).to include("Runner is offline")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns online text" do
|
it "returns online text" do
|
||||||
runner = create(:ci_runner, contacted_at: 1.second.ago, active: true)
|
runner = create(:ci_runner, contacted_at: 1.second.ago, active: true)
|
||||||
expect(runner_status_icon(runner)).to include("Runner is online")
|
expect(helper.runner_status_icon(runner)).to include("Runner is online")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns paused text" do
|
it "returns paused text" do
|
||||||
runner = create(:ci_runner, contacted_at: 1.second.ago, active: false)
|
runner = create(:ci_runner, contacted_at: 1.second.ago, active: false)
|
||||||
expect(runner_status_icon(runner)).to include("Runner is paused")
|
expect(helper.runner_status_icon(runner)).to include("Runner is paused")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
|
|
||||||
context 'without sorting' do
|
context 'without sorting' do
|
||||||
it 'returns cached value' do
|
it 'returns cached value' do
|
||||||
expect(runner_contacted_at(runner)).to eq(contacted_at_cached)
|
expect(helper.runner_contacted_at(runner)).to eq(contacted_at_cached)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns cached value' do
|
it 'returns cached value' do
|
||||||
expect(runner_contacted_at(runner)).to eq(contacted_at_cached)
|
expect(helper.runner_contacted_at(runner)).to eq(contacted_at_cached)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,11 +62,33 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns stored value' do
|
it 'returns stored value' do
|
||||||
expect(runner_contacted_at(runner)).to eq(contacted_at_stored)
|
expect(helper.runner_contacted_at(runner)).to eq(contacted_at_stored)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#admin_runners_data_attributes' do
|
||||||
|
let_it_be(:admin) { create(:user, :admin) }
|
||||||
|
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
|
||||||
|
let_it_be(:project_runner) { create(:ci_runner, :project ) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:current_user).and_return(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the data in format' do
|
||||||
|
expect(helper.admin_runners_data_attributes).to eq({
|
||||||
|
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||||
|
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||||
|
active_runners_count: '0',
|
||||||
|
all_runners_count: '2',
|
||||||
|
instance_runners_count: '1',
|
||||||
|
group_runners_count: '0',
|
||||||
|
project_runners_count: '1'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#group_shared_runners_settings_data' do
|
describe '#group_shared_runners_settings_data' do
|
||||||
let_it_be(:parent) { create(:group) }
|
let_it_be(:parent) { create(:group) }
|
||||||
let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
|
let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
|
||||||
|
@ -86,7 +108,7 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
parent_shared_runners_availability: nil
|
parent_shared_runners_availability: nil
|
||||||
}.merge(runner_constants)
|
}.merge(runner_constants)
|
||||||
|
|
||||||
expect(group_shared_runners_settings_data(parent)).to eq result
|
expect(helper.group_shared_runners_settings_data(parent)).to eq result
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns group data for child group' do
|
it 'returns group data for child group' do
|
||||||
|
@ -96,7 +118,7 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
parent_shared_runners_availability: Namespace::SR_ENABLED
|
parent_shared_runners_availability: Namespace::SR_ENABLED
|
||||||
}.merge(runner_constants)
|
}.merge(runner_constants)
|
||||||
|
|
||||||
expect(group_shared_runners_settings_data(group)).to eq result
|
expect(helper.group_shared_runners_settings_data(group)).to eq result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -104,7 +126,7 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
let(:group) { create(:group) }
|
let(:group) { create(:group) }
|
||||||
|
|
||||||
it 'returns group data to render a runner list' do
|
it 'returns group data to render a runner list' do
|
||||||
data = group_runners_data_attributes(group)
|
data = helper.group_runners_data_attributes(group)
|
||||||
|
|
||||||
expect(data[:registration_token]).to eq(group.runners_token)
|
expect(data[:registration_token]).to eq(group.runners_token)
|
||||||
expect(data[:group_id]).to eq(group.id)
|
expect(data[:group_id]).to eq(group.id)
|
||||||
|
|
|
@ -53,7 +53,7 @@ RSpec.describe "deleting designs" do
|
||||||
|
|
||||||
context 'the designs list contains filenames we cannot find' do
|
context 'the designs list contains filenames we cannot find' do
|
||||||
it_behaves_like 'a failed request' do
|
it_behaves_like 'a failed request' do
|
||||||
let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } }
|
let(:designs) { %w/foo bar baz/.map { |fn| instance_double('file', filename: fn) } }
|
||||||
let(:the_error) { a_string_matching %r/filenames were not found/ }
|
let(:the_error) { a_string_matching %r/filenames were not found/ }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -201,8 +201,8 @@ RSpec.configure do |config|
|
||||||
# Do not retry controller tests because rspec-retry cannot properly
|
# Do not retry controller tests because rspec-retry cannot properly
|
||||||
# reset the controller which may contain data from last attempt. See
|
# reset the controller which may contain data from last attempt. See
|
||||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360
|
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360
|
||||||
config.around(:each, type: :controller) do |example|
|
config.prepend_before(:each, type: :controller) do |example|
|
||||||
example.run_with_retry(retry: 1)
|
example.metadata[:retry] = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
|
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
|
||||||
|
|
|
@ -193,6 +193,54 @@ RSpec.describe Tooling::Danger::ProjectHelper do
|
||||||
'config/metrics/schema.json' | [:product_intelligence]
|
'config/metrics/schema.json' | [:product_intelligence]
|
||||||
'doc/api/usage_data.md' | [:product_intelligence]
|
'doc/api/usage_data.md' | [:product_intelligence]
|
||||||
'spec/lib/gitlab/usage_data_spec.rb' | [:product_intelligence]
|
'spec/lib/gitlab/usage_data_spec.rb' | [:product_intelligence]
|
||||||
|
|
||||||
|
'app/models/integration.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/models/integrations/github.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/models/ee/integrations/jira.rb' | [:integrations_be, :backend]
|
||||||
|
'app/models/integrations/chat_message/pipeline_message.rb' | [:integrations_be, :backend]
|
||||||
|
'app/models/jira_connect_subscription.rb' | [:integrations_be, :backend]
|
||||||
|
'app/models/hooks/service_hook.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/models/ee/hooks/system_hook.rb' | [:integrations_be, :backend]
|
||||||
|
'app/services/concerns/integrations/project_test_data.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/services/ee/integrations/test/project_service.rb' | [:integrations_be, :backend]
|
||||||
|
'app/controllers/concerns/integrations/actions.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/controllers/concerns/ee/integrations/params.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/controllers/projects/integrations/jira/issues_controller.rb' | [:integrations_be, :backend]
|
||||||
|
'app/controllers/projects/hooks_controller.rb' | [:integrations_be, :backend]
|
||||||
|
'app/controllers/admin/hook_logs_controller.rb' | [:integrations_be, :backend]
|
||||||
|
'app/controllers/groups/settings/integrations_controller.rb' | [:integrations_be, :backend]
|
||||||
|
'app/controllers/jira_connect/branches_controller.rb' | [:integrations_be, :backend]
|
||||||
|
'app/controllers/oauth/jira/authorizations_controller.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/finders/projects/integrations/jira/by_ids_finder.rb' | [:integrations_be, :database, :backend]
|
||||||
|
'app/workers/jira_connect/sync_merge_request_worker.rb' | [:integrations_be, :backend]
|
||||||
|
'app/workers/propagate_integration_inherit_worker.rb' | [:integrations_be, :backend]
|
||||||
|
'app/workers/web_hooks/log_execution_worker.rb' | [:integrations_be, :backend]
|
||||||
|
'app/workers/web_hook_worker.rb' | [:integrations_be, :backend]
|
||||||
|
'app/workers/project_service_worker.rb' | [:integrations_be, :backend]
|
||||||
|
'lib/atlassian/jira_connect/serializers/commit_entity.rb' | [:integrations_be, :backend]
|
||||||
|
'lib/api/entities/project_integration.rb' | [:integrations_be, :backend]
|
||||||
|
'lib/gitlab/hook_data/note_builder.rb' | [:integrations_be, :backend]
|
||||||
|
'lib/gitlab/data_builder/note.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/lib/ee/gitlab/integrations/sti_type.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/lib/ee/api/helpers/integrations_helpers.rb' | [:integrations_be, :backend]
|
||||||
|
'ee/app/serializers/integrations/jira_serializers/issue_entity.rb' | [:integrations_be, :backend]
|
||||||
|
'lib/api/github/entities.rb' | [:integrations_be, :backend]
|
||||||
|
'lib/api/v3/github.rb' | [:integrations_be, :backend]
|
||||||
|
'app/models/clusters/integrations/elastic_stack.rb' | [:backend]
|
||||||
|
'app/controllers/clusters/integrations_controller.rb' | [:backend]
|
||||||
|
'app/services/clusters/integrations/prometheus_health_check_service.rb' | [:backend]
|
||||||
|
'app/graphql/types/alert_management/integration_type.rb' | [:backend]
|
||||||
|
|
||||||
|
'app/views/jira_connect/branches/new.html.haml' | [:integrations_fe, :frontend]
|
||||||
|
'app/views/layouts/jira_connect.html.haml' | [:integrations_fe, :frontend]
|
||||||
|
'app/assets/javascripts/jira_connect/branches/pages/index.vue' | [:integrations_fe, :frontend]
|
||||||
|
'ee/app/views/projects/integrations/jira/issues/show.html.haml' | [:integrations_fe, :frontend]
|
||||||
|
'ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql' | [:integrations_fe, :frontend]
|
||||||
|
'app/assets/javascripts/pages/projects/settings/integrations/show/index.js' | [:integrations_fe, :frontend]
|
||||||
|
'ee/app/assets/javascripts/pages/groups/hooks/index.js' | [:integrations_fe, :frontend]
|
||||||
|
'app/views/clusters/clusters/_integrations_tab.html.haml' | [:frontend]
|
||||||
|
'app/assets/javascripts/alerts_settings/graphql/fragments/integration_item.fragment.graphql' | [:frontend]
|
||||||
|
'app/assets/javascripts/filtered_search/droplab/hook_input.js' | [:frontend]
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
@ -212,6 +260,11 @@ RSpec.describe Tooling::Danger::ProjectHelper do
|
||||||
[:backend, :product_intelligence] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb']
|
[:backend, :product_intelligence] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb']
|
||||||
[:backend, :product_intelligence] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb']
|
[:backend, :product_intelligence] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb']
|
||||||
[:backend] | '+ count(User.active)' | ['user.rb']
|
[:backend] | '+ count(User.active)' | ['user.rb']
|
||||||
|
[:integrations_be, :database, :migration] | '+ add_column :integrations, :foo, :text' | ['db/migrate/foo.rb']
|
||||||
|
[:integrations_be, :database, :migration] | '+ create_table :zentao_tracker_data do |t|' | ['ee/db/post_migrate/foo.rb']
|
||||||
|
[:integrations_be, :backend] | '+ Integrations::Foo' | ['app/foo/bar.rb']
|
||||||
|
[:integrations_be, :backend] | '+ project.execute_hooks(foo, :bar)' | ['ee/lib/ee/foo.rb']
|
||||||
|
[:integrations_be, :backend] | '+ project.execute_integrations(foo, :bar)' | ['app/foo.rb']
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
|
|
@ -44,6 +44,28 @@ module Tooling
|
||||||
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
|
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
|
||||||
%r{\Adata/whats_new/} => :docs,
|
%r{\Adata/whats_new/} => :docs,
|
||||||
|
|
||||||
|
%r{\A((ee|jh)/)?app/finders/(.+/)?integrations/} => [:integrations_be, :database, :backend],
|
||||||
|
[%r{\A((ee|jh)/)?db/(geo/)?(migrate|post_migrate)/}, %r{(:integrations|:\w+_tracker_data)\b}] => [:integrations_be, :database, :migration],
|
||||||
|
[%r{\A((ee|jh)/)?(app|lib)/.+\.rb}, %r{\b(Integrations::|\.execute_(integrations|hooks))\b}] => [:integrations_be, :backend],
|
||||||
|
%r{\A(
|
||||||
|
((ee|jh)/)?app/((?!.*clusters)(?!.*alert_management)(?!.*views)(?!.*assets).+/)?integration.+ |
|
||||||
|
((ee|jh)/)?app/((?!.*search).+/)?project_service.+ |
|
||||||
|
((ee|jh)/)?app/(models|helpers|workers|services|controllers)/(.+/)?(jira_connect.+|.*hook.+) |
|
||||||
|
((ee|jh)/)?app/controllers/(.+/)?oauth/jira/.+ |
|
||||||
|
((ee|jh)/)?app/services/(.+/)?jira.+ |
|
||||||
|
((ee|jh)/)?app/workers/(.+/)?(propagate_integration.+|irker_worker\.rb) |
|
||||||
|
((ee|jh)/)?lib/(.+/)?(atlassian|data_builder|hook_data)/.+ |
|
||||||
|
((ee|jh)/)?lib/(.+/)?.*integration.+ |
|
||||||
|
((ee|jh)/)?lib/(.+/)?api/v3/github\.rb |
|
||||||
|
((ee|jh)/)?lib/(.+/)?api/github/entities\.rb
|
||||||
|
)\z}x => [:integrations_be, :backend],
|
||||||
|
|
||||||
|
%r{\A(
|
||||||
|
((ee|jh)/)?app/(views|assets)/((?!.*clusters)(?!.*alerts_settings).+/)?integration.+ |
|
||||||
|
((ee|jh)/)?app/(views|assets)/(.+/)?jira_connect.+ |
|
||||||
|
((ee|jh)/)?app/(views|assets)/((?!.*filtered_search).+/)?hooks?.+
|
||||||
|
)\z}x => [:integrations_fe, :frontend],
|
||||||
|
|
||||||
%r{\A(
|
%r{\A(
|
||||||
app/assets/javascripts/tracking/.*\.js |
|
app/assets/javascripts/tracking/.*\.js |
|
||||||
spec/frontend/tracking/.*\.js |
|
spec/frontend/tracking/.*\.js |
|
||||||
|
|
Loading…
Reference in New Issue