Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
31f5ca749f
commit
0985362048
|
|
@ -288,14 +288,6 @@ export default {
|
|||
'app/assets/javascripts/webhooks/components/form_custom_header_item.vue',
|
||||
'app/assets/javascripts/work_items/components/create_work_item.vue',
|
||||
'app/assets/javascripts/work_items/components/design_management/design_notes/design_discussion.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/system_note.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_add_note.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_comment_locked.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_note.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue',
|
||||
'app/assets/javascripts/work_items/components/notes/work_item_note_body.vue',
|
||||
'app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue',
|
||||
'app/assets/javascripts/work_items/components/shared/work_item_token_input.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_assignees.vue',
|
||||
|
|
@ -318,7 +310,6 @@ export default {
|
|||
'app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_data.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_milestone.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_notes.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_notifications_widget.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_state_toggle.vue',
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ stages:
|
|||
- .docker-in-docker
|
||||
- .qa-install
|
||||
- .e2e-test-variables
|
||||
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}-rust-${RUST_VERSION}:git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-kubectl-${KUBECTL_VERSION}-helm-${HELM_VERSION}-kind-${KIND_VERSION}"
|
||||
variables:
|
||||
# variables related to failure issue reporting
|
||||
# default values from /ci/qa-report.gitlab-ci.yml will work with gitlab-qa orchestrator but not with cng and gdk tests
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ workflow:
|
|||
- export QA_GITLAB_URL="http://gitlab.${GITLAB_DOMAIN}"
|
||||
|
||||
.cng-test:
|
||||
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ci/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-kubectl-${KUBECTL_VERSION}-helm-${HELM_VERSION}-kind-${KIND_VERSION}"
|
||||
stage: test
|
||||
extends:
|
||||
- .e2e-test-base
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ workflow:
|
|||
- mv $CI_BUILDS_DIR/*.log $CI_PROJECT_DIR/
|
||||
|
||||
.gdk-qa-base:
|
||||
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ci/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}"
|
||||
extends:
|
||||
- .e2e-test-base
|
||||
# ignore runtime data from gdk because it's significantly slower than cng and runtime data for
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
5ddaf4012bca2e5291f21503c5df3ccd7bafe62f
|
||||
cdfcfafe0ef01938ccb97298d64245e6a5f0fbf1
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { __, s__ } from '~/locale';
|
||||
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
|
||||
|
||||
export default {
|
||||
name: 'InputsAdoptionBanner',
|
||||
components: { GlAlert, GlSprintf, UserCalloutDismisser },
|
||||
components: { GlAlert, GlButton, GlSprintf, UserCalloutDismisser },
|
||||
inputsDocsPath: helpPagePath('ci/yaml/inputs'),
|
||||
inject: ['canViewPipelineEditor', 'pipelineEditorPath'],
|
||||
props: {
|
||||
|
|
@ -19,16 +18,6 @@ export default {
|
|||
showPipelineEditorButton() {
|
||||
return this.canViewPipelineEditor && this.pipelineEditorPath;
|
||||
},
|
||||
alertProps() {
|
||||
return {
|
||||
secondaryButtonText: __('Learn more'),
|
||||
secondaryButtonLink: this.$options.inputsDocsPath,
|
||||
...(this.showPipelineEditorButton && {
|
||||
primaryButtonText: s__('Pipelines|Go to the pipeline editor'),
|
||||
primaryButtonLink: this.pipelineEditorPath,
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -36,13 +25,7 @@ export default {
|
|||
<template>
|
||||
<user-callout-dismisser :feature-name="featureName">
|
||||
<template #default="{ dismiss, shouldShowCallout }">
|
||||
<gl-alert
|
||||
v-if="shouldShowCallout"
|
||||
variant="tip"
|
||||
class="gl-my-4"
|
||||
v-bind="alertProps"
|
||||
@dismiss="dismiss"
|
||||
>
|
||||
<gl-alert v-if="shouldShowCallout" variant="tip" class="gl-my-4" @dismiss="dismiss">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
|
|
@ -54,6 +37,19 @@ export default {
|
|||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<div class="gl-mt-4 gl-flex gl-gap-3">
|
||||
<gl-button
|
||||
v-if="showPipelineEditorButton"
|
||||
:href="pipelineEditorPath"
|
||||
category="secondary"
|
||||
variant="confirm"
|
||||
>
|
||||
{{ __('Go to the pipeline editor') }}
|
||||
</gl-button>
|
||||
<gl-button :href="$options.inputsDocsPath" category="secondary" target="_blank">
|
||||
{{ __('Learn more') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-alert>
|
||||
</template>
|
||||
</user-callout-dismisser>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import InputsTableSkeletonLoader from './pipeline_inputs_table/inputs_table_skel
|
|||
import PipelineInputsTable from './pipeline_inputs_table/pipeline_inputs_table.vue';
|
||||
import getPipelineInputsQuery from './graphql/queries/pipeline_creation_inputs.query.graphql';
|
||||
|
||||
const ARRAY_TYPE = 'ARRAY';
|
||||
|
||||
export default {
|
||||
name: 'PipelineInputsForm',
|
||||
components: {
|
||||
|
|
@ -78,10 +80,21 @@ export default {
|
|||
input.name === updatedInput.name ? updatedInput : input,
|
||||
);
|
||||
|
||||
const nameValuePairs = this.inputs.map((input) => ({
|
||||
name: input.name,
|
||||
value: input.default,
|
||||
}));
|
||||
const nameValuePairs = this.inputs.map((input) => {
|
||||
let value = input.default;
|
||||
|
||||
// Convert string to array for ARRAY type inputs
|
||||
if (input.type === ARRAY_TYPE && typeof value === 'string' && value) {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
if (!Array.isArray(value)) value = [value];
|
||||
} catch (e) {
|
||||
value = value.split(',').map((item) => item.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return { name: input.name, value };
|
||||
});
|
||||
|
||||
this.$emit('update-inputs', nameValuePairs);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,15 +24,28 @@ const INPUT_TYPES = {
|
|||
STRING: 'STRING',
|
||||
};
|
||||
|
||||
const VALIDATION_MESSAGES = {
|
||||
ARRAY_FORMAT_MISMATCH: __(
|
||||
'The value must be a valid JSON array format: [1,2,3] or [{"key": "value"}]',
|
||||
),
|
||||
GENERAL_FORMAT_MISMATCH: __('Please match the requested format.'),
|
||||
NUMBER_TYPE_MISMATCH: __('The value must contain only numbers.'),
|
||||
REGEX_MISMATCH: __('The value must match the defined regular expression.'),
|
||||
VALUE_MISSING: __('This is required and must be defined.'),
|
||||
};
|
||||
|
||||
const feedbackMap = {
|
||||
arrayFormatMismatch: {
|
||||
isInvalid: (el) => {
|
||||
if (el.dataset.jsonArray !== 'true' || !el.value) return false;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(el.value);
|
||||
return !Array.isArray(parsed);
|
||||
const isValid = Array.isArray(JSON.parse(el.value));
|
||||
// we use setCustomValidity to set the message that appears when the user clicks submit
|
||||
el.setCustomValidity(isValid ? '' : VALIDATION_MESSAGES.GENERAL_FORMAT_MISMATCH);
|
||||
return !isValid;
|
||||
} catch {
|
||||
el.setCustomValidity(VALIDATION_MESSAGES.GENERAL_FORMAT_MISMATCH);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
|
@ -40,21 +53,25 @@ const feedbackMap = {
|
|||
},
|
||||
numberTypeMismatch: {
|
||||
isInvalid: (el) => {
|
||||
return (
|
||||
const isInvalid =
|
||||
el.dataset.fieldType === INPUT_TYPES.NUMBER &&
|
||||
el.value &&
|
||||
!Number.isFinite(Number(el.value))
|
||||
);
|
||||
!Number.isFinite(Number(el.value));
|
||||
|
||||
// we use setCustomValidity to set the message that appears when the user clicks submit
|
||||
el.setCustomValidity(isInvalid ? VALIDATION_MESSAGES.GENERAL_FORMAT_MISMATCH : '');
|
||||
|
||||
return isInvalid;
|
||||
},
|
||||
message: __('The value must contain only numbers.'),
|
||||
message: VALIDATION_MESSAGES.NUMBER_TYPE_MISMATCH,
|
||||
},
|
||||
regexMismatch: {
|
||||
isInvalid: (el) => el.validity?.patternMismatch,
|
||||
message: __('The value must match the defined regular expression.'),
|
||||
message: VALIDATION_MESSAGES.REGEX_MISMATCH,
|
||||
},
|
||||
valueMissing: {
|
||||
isInvalid: (el) => el.validity?.valueMissing,
|
||||
message: __('This is required and must be defined.'),
|
||||
message: VALIDATION_MESSAGES.VALUE_MISSING,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -116,6 +133,13 @@ export default {
|
|||
const field = this.form.fields[this.item.name];
|
||||
return this.isArrayType && field?.feedback === feedbackMap.arrayFormatMismatch.message;
|
||||
},
|
||||
hasNumberTypeError() {
|
||||
const field = this.form.fields[this.item.name];
|
||||
return (
|
||||
this.item.type === INPUT_TYPES.NUMBER &&
|
||||
field?.feedback === feedbackMap.numberTypeMismatch.message
|
||||
);
|
||||
},
|
||||
hasValidationFeedback() {
|
||||
return Boolean(this.validationFeedback);
|
||||
},
|
||||
|
|
@ -137,9 +161,9 @@ export default {
|
|||
: feedback;
|
||||
},
|
||||
validationState() {
|
||||
// Override validation state for array format errors
|
||||
// This handles cases where checkValidity() returns true but our custom array validation fails
|
||||
if (this.hasArrayFormatError) {
|
||||
// Override validation state for array format errors and number type errors for our custom validation
|
||||
// This is also responsible for turning the border red when the input is invalid
|
||||
if (this.hasArrayFormatError || this.hasNumberTypeError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import {
|
||||
GlButton,
|
||||
GlDisclosureDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlLoadingIcon,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
|
|
@ -23,7 +23,7 @@ export default {
|
|||
CiIcon,
|
||||
GlButton,
|
||||
GlDisclosureDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlLoadingIcon,
|
||||
JobDropdownItem,
|
||||
},
|
||||
|
|
@ -161,22 +161,27 @@ export default {
|
|||
data-testid="pipeline-mini-graph-dropdown-menu-list"
|
||||
@click.stop
|
||||
>
|
||||
<span v-if="hasFailedJobs" class="gl-flex gl-px-4 gl-py-3 gl-text-sm gl-font-bold">
|
||||
{{ s__('Pipelines|Failed jobs') }}
|
||||
</span>
|
||||
<job-dropdown-item
|
||||
v-for="job in failedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
<gl-dropdown-divider v-if="hasPassedJobs && hasFailedJobs" />
|
||||
<job-dropdown-item
|
||||
v-for="job in passedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
<gl-disclosure-dropdown-group v-if="hasFailedJobs">
|
||||
<template #group-label>{{ s__('Pipelines|Failed jobs') }}</template>
|
||||
<job-dropdown-item
|
||||
v-for="job in failedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
<gl-disclosure-dropdown-group
|
||||
v-if="hasPassedJobs"
|
||||
:bordered="hasFailedJobs"
|
||||
data-testid="passed-jobs"
|
||||
>
|
||||
<job-dropdown-item
|
||||
v-for="job in passedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</ul>
|
||||
|
||||
<template #footer>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import { reportToSentry } from '~/ci/utils';
|
||||
import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
|
||||
import filterVariables from '../utils/filter_variables';
|
||||
import {
|
||||
CI_VARIABLE_TYPE_FILE,
|
||||
|
|
@ -46,6 +47,7 @@ export default {
|
|||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
InputsAdoptionBanner,
|
||||
Markdown,
|
||||
VariableValuesListbox,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
|
|
@ -361,9 +363,11 @@ export default {
|
|||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="descriptions[variable.key]" class="gl-text-subtle">
|
||||
{{ descriptions[variable.key] }}
|
||||
</div>
|
||||
<markdown
|
||||
v-if="descriptions[variable.key]"
|
||||
class="gl-text-subtle"
|
||||
:markdown="descriptions[variable.key]"
|
||||
/>
|
||||
</div>
|
||||
<template #description>
|
||||
<gl-sprintf
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ export default {
|
|||
<template>
|
||||
<div class="col-lg-8 gl-pl-0">
|
||||
<gl-loading-icon v-if="loading && editing" size="lg" />
|
||||
<gl-form v-else>
|
||||
<gl-form v-else @submit.prevent="scheduleHandler">
|
||||
<!--Description-->
|
||||
<gl-form-group :label="$options.i18n.description" label-for="schedule-description">
|
||||
<gl-form-input
|
||||
|
|
@ -344,10 +344,10 @@ export default {
|
|||
</gl-form-checkbox>
|
||||
<div class="gl-flex gl-flex-wrap gl-gap-3">
|
||||
<gl-button
|
||||
type="submit"
|
||||
variant="confirm"
|
||||
data-testid="schedule-submit-button"
|
||||
class="gl-w-full sm:gl-w-auto"
|
||||
@click="scheduleHandler"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapMutations } from 'vuex';
|
||||
import { debounce } from 'lodash';
|
||||
import { mapActions } from 'pinia';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { getCookie, setCookie } from '~/lib/utils/common_utils';
|
||||
import * as types from '~/diffs/store/mutation_types';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import {
|
||||
INITIAL_TREE_WIDTH,
|
||||
MIN_TREE_WIDTH,
|
||||
|
|
@ -65,7 +65,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('diffs', {
|
||||
...mapActions(useLegacyDiffs, {
|
||||
setCurrentDiffFile: types.SET_CURRENT_DIFF_FILE,
|
||||
}),
|
||||
onFileClick(file) {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
GlButton,
|
||||
GlSearchBoxByType,
|
||||
} from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { mapActions, mapState } from 'pinia';
|
||||
import micromatch from 'micromatch';
|
||||
import { getModifierKey } from '~/constants';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
|
||||
import { isElementClipped } from '~/lib/utils/common_utils';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import DiffFileRow from './diff_file_row.vue';
|
||||
import TreeListHeight from './tree_list_height.vue';
|
||||
|
||||
|
|
@ -48,8 +48,15 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds', 'realSize']),
|
||||
...mapGetters('diffs', ['fileTree', 'allBlobs', 'linkedFile']),
|
||||
...mapState(useLegacyDiffs, [
|
||||
'renderTreeList',
|
||||
'currentDiffFileId',
|
||||
'viewedDiffFileIds',
|
||||
'realSize',
|
||||
'fileTree',
|
||||
'allBlobs',
|
||||
'linkedFile',
|
||||
]),
|
||||
filteredTreeList() {
|
||||
let search = this.search.toLowerCase().trim();
|
||||
|
||||
|
|
@ -153,8 +160,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['toggleTreeOpen', 'setRenderTreeList', 'setTreeOpen']),
|
||||
|
||||
...mapActions(useLegacyDiffs, ['toggleTreeOpen', 'setRenderTreeList', 'setTreeOpen']),
|
||||
scrollVirtualScrollerToFileHash(hash) {
|
||||
const item = document.querySelector(`[data-file-row="${hash}"]`);
|
||||
if (item && !isElementClipped(item, this.$refs.scroller.$el)) return;
|
||||
|
|
|
|||
|
|
@ -335,6 +335,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
class="detail-page-header-actions gl-mt-1 gl-flex gl-w-full gl-self-start sm:gl-gap-3 md:gl-w-auto"
|
||||
data-testid="issue-header"
|
||||
>
|
||||
<div class="gl-w-full md:!gl-hidden">
|
||||
<gl-disclosure-dropdown
|
||||
|
|
|
|||
|
|
@ -592,11 +592,20 @@ export function insertMarkdownText({
|
|||
}
|
||||
}
|
||||
|
||||
export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) {
|
||||
export function updateText({
|
||||
textArea,
|
||||
tag,
|
||||
cursorOffset,
|
||||
blockTag,
|
||||
wrap,
|
||||
select,
|
||||
tagContent,
|
||||
replaceText = false,
|
||||
}) {
|
||||
const $textArea = $(textArea);
|
||||
textArea = $textArea.get(0);
|
||||
const text = $textArea.val();
|
||||
const selected = selectedText(text, textArea) || tagContent;
|
||||
const selected = replaceText ? '' : selectedText(text, textArea) || tagContent;
|
||||
textArea.focus();
|
||||
insertMarkdownText({
|
||||
textArea,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import CommitInfo from './commit_info.vue';
|
|||
import CollapsibleCommitInfo from './collapsible_commit_info.vue';
|
||||
|
||||
const trackingMixin = InternalEvents.mixin();
|
||||
const POLL_INTERVAL = 30000;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CiIcon,
|
||||
|
|
@ -49,7 +51,7 @@ export default {
|
|||
};
|
||||
},
|
||||
update: (data) => {
|
||||
const lastCommit = data.project?.repository?.paginatedTree?.nodes[0]?.lastCommit;
|
||||
const lastCommit = data.project?.repository?.lastCommit ?? {};
|
||||
const pipelines = lastCommit?.pipelines?.edges;
|
||||
|
||||
return {
|
||||
|
|
@ -60,6 +62,7 @@ export default {
|
|||
error(error) {
|
||||
throw error;
|
||||
},
|
||||
pollInterval: POLL_INTERVAL,
|
||||
subscribeToMore: {
|
||||
document() {
|
||||
return pipelineCiStatusUpdatedSubscription;
|
||||
|
|
|
|||
|
|
@ -26,10 +26,13 @@ export const i18n = {
|
|||
false,
|
||||
),
|
||||
improvementAndDegradationCopy: (improvement, degradation) =>
|
||||
sprintf(__('Code Quality scans found %{degradation} and %{improvement}.'), {
|
||||
improvement,
|
||||
degradation,
|
||||
}),
|
||||
singularCopy: (findings) =>
|
||||
sprintf(__('Code Quality scans found %{findings}.'), { findings }, false),
|
||||
sprintf(
|
||||
__('Code Quality scans found %{degradation} and %{improvement}.'),
|
||||
{
|
||||
improvement,
|
||||
degradation,
|
||||
},
|
||||
false,
|
||||
),
|
||||
singularCopy: (findings) => sprintf(__('Code Quality scans found %{findings}.'), { findings }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export default {
|
|||
editorAiActions: { default: () => [] },
|
||||
mrGeneratedContent: { default: null },
|
||||
canSummarizeChanges: { default: false },
|
||||
canUseComposer: { default: false },
|
||||
},
|
||||
props: {
|
||||
previewMarkdown: {
|
||||
|
|
@ -677,7 +678,7 @@ export default {
|
|||
:new-comment-template-paths="commentTemplatePaths"
|
||||
@select="insertSavedReply"
|
||||
/>
|
||||
<template v-if="!previewMarkdown && canSummarizeChanges">
|
||||
<template v-if="!previewMarkdown && canSummarizeChanges && !canUseComposer">
|
||||
<header-divider />
|
||||
<summarize-code-changes />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import Autosize from 'autosize';
|
||||
import MarkdownComposer from 'ee_component/vue_shared/components/markdown/composer.vue';
|
||||
import { __ } from '~/locale';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { updateDraft, clearDraft, getDraft } from '~/lib/utils/autosave';
|
||||
|
|
@ -36,11 +37,13 @@ export default {
|
|||
GlAlert,
|
||||
MarkdownField,
|
||||
LocalStorageSync,
|
||||
MarkdownComposer,
|
||||
ContentEditor: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
|
||||
),
|
||||
},
|
||||
inject: { canUseComposer: { default: false } },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
|
@ -180,6 +183,9 @@ export default {
|
|||
isDefaultEditorEnabled() {
|
||||
return ['plain_text_editor', 'rich_text_editor'].includes(window.gon?.text_editor);
|
||||
},
|
||||
composerComponent() {
|
||||
return this.canUseComposer ? 'markdown-composer' : 'div';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: 'updateValue',
|
||||
|
|
@ -375,6 +381,7 @@ export default {
|
|||
>
|
||||
{{ alert.message }}
|
||||
</gl-alert>
|
||||
<!-- <markdown-composer v-if="!isContentEditorActive && canUseComposer" /> -->
|
||||
<markdown-field
|
||||
v-if="!isContentEditorActive"
|
||||
ref="markdownField"
|
||||
|
|
@ -402,20 +409,23 @@ export default {
|
|||
<template #header-buttons><slot name="header-buttons"></slot></template>
|
||||
<template #toolbar><slot name="toolbar"></slot></template>
|
||||
<template #textarea>
|
||||
<textarea
|
||||
v-bind="formFieldProps"
|
||||
ref="textarea"
|
||||
:value="markdown"
|
||||
class="note-textarea js-gfm-input markdown-area"
|
||||
dir="auto"
|
||||
:data-can-suggest="codeSuggestionsConfig.canSuggest"
|
||||
:data-noteable-type="noteableType"
|
||||
:data-supports-quick-actions="supportsQuickActions"
|
||||
:data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
|
||||
:disabled="disabled"
|
||||
@input="updateMarkdownFromMarkdownField"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
></textarea>
|
||||
<component :is="composerComponent" :markdown="canUseComposer ? markdown : null">
|
||||
<textarea
|
||||
v-bind="formFieldProps"
|
||||
ref="textarea"
|
||||
:value="markdown"
|
||||
class="note-textarea js-gfm-input markdown-area"
|
||||
:class="[{ 'gl-relative gl-z-3 !gl-pl-7': canUseComposer }, formFieldProps.class || '']"
|
||||
dir="auto"
|
||||
:data-can-suggest="codeSuggestionsConfig.canSuggest"
|
||||
:data-noteable-type="noteableType"
|
||||
:data-supports-quick-actions="supportsQuickActions"
|
||||
:data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
|
||||
:disabled="disabled"
|
||||
@input="updateMarkdownFromMarkdownField"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
></textarea>
|
||||
</component>
|
||||
</template>
|
||||
</markdown-field>
|
||||
<div v-else>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export function mountMarkdownEditor(options = {}) {
|
|||
const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true);
|
||||
const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true);
|
||||
const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false);
|
||||
const canUseComposer = parseBoolean(el.dataset.canUseComposer ?? false);
|
||||
const autofocus = parseBoolean(el.dataset.autofocus ?? true);
|
||||
const hiddenInput = el.querySelector('input[type="hidden"]');
|
||||
const formFieldName = hiddenInput.getAttribute('name');
|
||||
|
|
@ -98,6 +99,8 @@ export function mountMarkdownEditor(options = {}) {
|
|||
componentConfiguration.apolloProvider =
|
||||
options.apolloProvider || new VueApollo({ defaultClient: createApolloClient() });
|
||||
|
||||
componentConfiguration.provide.canUseComposer = canUseComposer;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import PromoPageLink from './promo_page_link.vue';
|
|||
|
||||
export default {
|
||||
component: PromoPageLink,
|
||||
title: 'vue_shared/help_page_link',
|
||||
title: 'vue_shared/promo_page_link',
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
|
|
|
|||
|
|
@ -56,10 +56,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
lines: [],
|
||||
showLines: false,
|
||||
loadingDiff: false,
|
||||
isLoadingDescriptionVersion: false,
|
||||
descriptionVersions: {},
|
||||
};
|
||||
|
|
@ -80,9 +76,6 @@ export default {
|
|||
isTargetNote() {
|
||||
return this.targetNoteHash === this.noteAnchorId;
|
||||
},
|
||||
toggleIcon() {
|
||||
return this.expanded ? 'chevron-up' : 'chevron-down';
|
||||
},
|
||||
actionTextHtml() {
|
||||
return $(this.note.bodyHtml).unwrap().html();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sortFilterProp: {
|
||||
sortFilter: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -36,11 +36,7 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
filterEvent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultSortFilterProp: {
|
||||
defaultSortFilter: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -50,6 +46,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
|
|
@ -57,28 +54,20 @@ export default {
|
|||
property: `type_${this.workItemType}`,
|
||||
};
|
||||
},
|
||||
getDropdownSelectedText() {
|
||||
return this.selectedSortOption.text;
|
||||
dropdownText() {
|
||||
return this.selectedItem.text;
|
||||
},
|
||||
selectedSortOption() {
|
||||
return (
|
||||
this.items.find(({ key }) => this.sortFilterProp === key) || this.defaultSortFilterProp
|
||||
);
|
||||
selectedItem() {
|
||||
return this.items.find(({ key }) => this.sortFilter === key) || this.defaultSortFilter;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setDiscussionFilterOption(filterValue) {
|
||||
this.$emit(this.filterEvent, filterValue);
|
||||
},
|
||||
fetchFilteredDiscussions(filterValue) {
|
||||
if (this.isSortDropdownItemActive(filterValue)) {
|
||||
handleSelect(sortFilter) {
|
||||
if (sortFilter === this.sortFilter) {
|
||||
return;
|
||||
}
|
||||
this.track(this.trackingAction);
|
||||
this.$emit(this.filterEvent, filterValue);
|
||||
},
|
||||
isSortDropdownItemActive(value) {
|
||||
return value === this.sortFilterProp;
|
||||
this.$emit('select', sortFilter);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -87,18 +76,19 @@ export default {
|
|||
<template>
|
||||
<div class="gl-inline-block gl-align-bottom">
|
||||
<local-storage-sync
|
||||
:value="sortFilterProp"
|
||||
:value="sortFilter"
|
||||
:storage-key="storageKey"
|
||||
as-string
|
||||
@input="setDiscussionFilterOption"
|
||||
@input="$emit('select', $event)"
|
||||
/>
|
||||
<gl-collapsible-listbox
|
||||
:toggle-text="getDropdownSelectedText"
|
||||
:disabled="loading"
|
||||
:toggle-text="dropdownText"
|
||||
:items="items"
|
||||
:selected="sortFilterProp"
|
||||
:selected="sortFilter"
|
||||
placement="bottom-end"
|
||||
size="small"
|
||||
@select="fetchFilteredDiscussions"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { uniqueId } from 'lodash';
|
|||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import Tracking from '~/tracking';
|
||||
import { ASC } from '~/notes/constants';
|
||||
import { __ } from '~/locale';
|
||||
import { clearDraft } from '~/lib/utils/autosave';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
|
|
@ -64,11 +63,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sortOrder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ASC,
|
||||
},
|
||||
markdownPreviewPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -147,7 +141,6 @@ export default {
|
|||
workItem: {},
|
||||
isEditing: this.isNewDiscussion,
|
||||
isSubmitting: false,
|
||||
isSubmittingWithKeydown: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -181,6 +174,7 @@ export default {
|
|||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return this.discussionId ? `${this.discussionId}-comment` : `${this.workItemId}-comment`;
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
|
|
|
|||
|
|
@ -32,9 +32,6 @@ export default {
|
|||
projectArchivedWarning: __('This project is archived and cannot be commented on.'),
|
||||
},
|
||||
computed: {
|
||||
issuableDisplayName() {
|
||||
return this.workItemType.replace(/_/g, ' ');
|
||||
},
|
||||
lockedIssueWarning() {
|
||||
return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
|
||||
noteableTypeText: this.noteableTypeText,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
|
|
@ -389,7 +390,6 @@ export default {
|
|||
:note-url="noteUrl"
|
||||
:show-reply="showReply"
|
||||
:show-edit="hasAdminPermission"
|
||||
:note-id="note.id"
|
||||
:is-author-an-assignee="isAuthorAnAssignee"
|
||||
:show-assign-unassign="canSetWorkItemMetadata && hasAuthor"
|
||||
:can-report-abuse="!isCurrentUserAuthorOfNote"
|
||||
|
|
@ -399,7 +399,6 @@ export default {
|
|||
:max-access-level-of-author="note.maxAccessLevelOfAuthor"
|
||||
:project-name="projectName"
|
||||
:can-resolve="canResolve"
|
||||
:resolvable="isDiscussionResolvable"
|
||||
:is-resolved="isDiscussionResolved"
|
||||
:is-resolving="isResolving"
|
||||
:resolved-by="discussionResolvedBy"
|
||||
|
|
@ -444,7 +443,6 @@ export default {
|
|||
ref="noteBody"
|
||||
:note="note"
|
||||
:has-admin-note-permission="hasAdminPermission"
|
||||
:has-replies="hasReplies"
|
||||
:is-updating="isUpdating"
|
||||
@updateNote="updateNote"
|
||||
/>
|
||||
|
|
@ -463,7 +461,6 @@ export default {
|
|||
:full-path="fullPath"
|
||||
:note="note"
|
||||
:work-item-iid="workItemIid"
|
||||
:is-modal="isModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,10 +57,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
noteId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showAwardEmoji: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -115,11 +111,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
resolvable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isResolved: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isModal: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
awards() {
|
||||
|
|
|
|||
|
|
@ -20,11 +20,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasReplies: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
|||
|
|
@ -59,14 +59,6 @@ export default {
|
|||
return this.smallHeaderStyle ? 'gl-text-base gl-m-0' : 'gl-text-size-h1 gl-m-0';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeNotesSortOrder(direction) {
|
||||
this.$emit('changeSort', direction);
|
||||
},
|
||||
filterDiscussions(filterValue) {
|
||||
this.$emit('changeFilter', filterValue);
|
||||
},
|
||||
},
|
||||
WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
|
||||
WORK_ITEM_NOTES_FILTER_KEY,
|
||||
WORK_ITEM_NOTES_FILTER_ALL_NOTES,
|
||||
|
|
@ -93,28 +85,26 @@ export default {
|
|||
<work-item-activity-sort-filter
|
||||
:work-item-type="workItemType"
|
||||
:loading="disableActivityFilterSort"
|
||||
:sort-filter-prop="discussionFilter"
|
||||
:sort-filter="discussionFilter"
|
||||
:items="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
|
||||
:storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
|
||||
:default-sort-filter-prop="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
|
||||
:default-sort-filter="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
|
||||
tracking-action="work_item_notes_filter_changed"
|
||||
tracking-label="item_track_notes_filtering"
|
||||
filter-event="changeFilter"
|
||||
data-testid="work-item-filter"
|
||||
@changeFilter="filterDiscussions"
|
||||
@select="$emit('changeFilter', $event)"
|
||||
/>
|
||||
<work-item-activity-sort-filter
|
||||
:work-item-type="workItemType"
|
||||
:loading="disableActivityFilterSort"
|
||||
:sort-filter-prop="sortOrder"
|
||||
:sort-filter="sortOrder"
|
||||
:items="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
|
||||
:storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
|
||||
:default-sort-filter-prop="$options.ASC"
|
||||
:default-sort-filter="$options.ASC"
|
||||
tracking-action="work_item_notes_sort_order_changed"
|
||||
tracking-label="item_track_notes_sorting"
|
||||
filter-event="changeSort"
|
||||
data-testid="work-item-sort"
|
||||
@changeSort="changeNotesSortOrder"
|
||||
@select="$emit('changeSort', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -139,13 +139,7 @@ export default {
|
|||
WorkItemCreateBranchMergeRequestSplitButton,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin(), trackingMixin],
|
||||
inject: [
|
||||
'fullPath',
|
||||
'reportAbusePath',
|
||||
'groupPath',
|
||||
'hasSubepicsFeature',
|
||||
'hasLinkedItemsEpicsFeature',
|
||||
],
|
||||
inject: ['fullPath', 'groupPath', 'hasSubepicsFeature', 'hasLinkedItemsEpicsFeature'],
|
||||
props: {
|
||||
isModal: {
|
||||
type: Boolean,
|
||||
|
|
@ -1235,7 +1229,6 @@ export default {
|
|||
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
|
||||
:can-set-work-item-metadata="canAssignUnassignUser"
|
||||
:can-summarize-comments="canSummarizeComments"
|
||||
:report-abuse-path="reportAbusePath"
|
||||
:is-discussion-locked="isDiscussionLocked"
|
||||
:is-work-item-confidential="workItem.confidential"
|
||||
:new-comment-template-paths="newCommentTemplatePaths"
|
||||
|
|
|
|||
|
|
@ -92,10 +92,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
reportAbusePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDiscussionLocked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -153,9 +149,6 @@ export default {
|
|||
someNotesLoaded() {
|
||||
return !this.initialLoading || this.previewNote;
|
||||
},
|
||||
avatarUrl() {
|
||||
return window.gon.current_user_avatar_url;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.workItemNotes?.pageInfo;
|
||||
},
|
||||
|
|
@ -379,10 +372,10 @@ export default {
|
|||
isSystemNote(note) {
|
||||
return note.notes.nodes[0].system;
|
||||
},
|
||||
changeNotesSortOrder(direction) {
|
||||
setSort(direction) {
|
||||
this.sortOrder = direction;
|
||||
},
|
||||
filterDiscussions(filterValue) {
|
||||
setFilter(filterValue) {
|
||||
this.discussionFilter = filterValue;
|
||||
},
|
||||
reportAbuse(isOpen, reply = {}) {
|
||||
|
|
@ -469,8 +462,8 @@ export default {
|
|||
:discussion-filter="discussionFilter"
|
||||
:use-h2="useH2"
|
||||
:small-header-style="smallHeaderStyle"
|
||||
@changeSort="changeNotesSortOrder"
|
||||
@changeFilter="filterDiscussions"
|
||||
@changeSort="setSort"
|
||||
@changeFilter="setFilter"
|
||||
/>
|
||||
<work-item-notes-loading v-if="initialLoading" class="gl-mt-5" />
|
||||
<div v-if="someNotesLoaded" class="issuable-discussion gl-mb-5 !gl-clearfix">
|
||||
|
|
@ -522,10 +515,7 @@ export default {
|
|||
</template>
|
||||
</template>
|
||||
|
||||
<work-item-history-only-filter-note
|
||||
v-if="commentsDisabled"
|
||||
@changeFilter="filterDiscussions"
|
||||
/>
|
||||
<work-item-history-only-filter-note v-if="commentsDisabled" @changeFilter="setFilter" />
|
||||
</ul>
|
||||
<work-item-notes-loading v-if="!formAtTop && isLoadingMore" />
|
||||
<div v-if="!formAtTop && !commentsDisabled" class="js-comment-form">
|
||||
|
|
|
|||
|
|
@ -60,11 +60,13 @@ module Groups
|
|||
end
|
||||
|
||||
def sort(items)
|
||||
return super unless params[:sort]
|
||||
|
||||
if params[:sort] == :similarity && params[:search].present?
|
||||
return items.sorted_by_similarity_desc(params[:search])
|
||||
end
|
||||
|
||||
super
|
||||
items.sort_by_attribute(params[:sort])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,68 +4,62 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!, $refType:
|
|||
id
|
||||
repository {
|
||||
__typename
|
||||
paginatedTree(path: $path, ref: $ref, refType: $refType) {
|
||||
lastCommit(path: $path, ref: $ref, refType: $refType) {
|
||||
__typename
|
||||
nodes {
|
||||
id
|
||||
sha
|
||||
title
|
||||
titleHtml
|
||||
descriptionHtml
|
||||
message
|
||||
webPath
|
||||
authoredDate
|
||||
authorName
|
||||
authorGravatar
|
||||
author {
|
||||
__typename
|
||||
lastCommit {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
webPath
|
||||
}
|
||||
signature {
|
||||
__typename
|
||||
... on GpgSignature {
|
||||
gpgKeyPrimaryKeyid
|
||||
verificationStatus
|
||||
}
|
||||
... on X509Signature {
|
||||
verificationStatus
|
||||
x509Certificate {
|
||||
id
|
||||
subject
|
||||
subjectKeyIdentifier
|
||||
x509Issuer {
|
||||
id
|
||||
subject
|
||||
subjectKeyIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
... on SshSignature {
|
||||
verificationStatus
|
||||
keyFingerprintSha256
|
||||
}
|
||||
}
|
||||
pipelines(ref: $ref, first: 1) {
|
||||
__typename
|
||||
edges {
|
||||
__typename
|
||||
id
|
||||
sha
|
||||
title
|
||||
titleHtml
|
||||
descriptionHtml
|
||||
message
|
||||
webPath
|
||||
authoredDate
|
||||
authorName
|
||||
authorGravatar
|
||||
author {
|
||||
node {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
webPath
|
||||
}
|
||||
signature {
|
||||
__typename
|
||||
... on GpgSignature {
|
||||
gpgKeyPrimaryKeyid
|
||||
verificationStatus
|
||||
}
|
||||
... on X509Signature {
|
||||
verificationStatus
|
||||
x509Certificate {
|
||||
id
|
||||
subject
|
||||
subjectKeyIdentifier
|
||||
x509Issuer {
|
||||
id
|
||||
subject
|
||||
subjectKeyIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
... on SshSignature {
|
||||
verificationStatus
|
||||
keyFingerprintSha256
|
||||
}
|
||||
}
|
||||
pipelines(ref: $ref, first: 1) {
|
||||
__typename
|
||||
edges {
|
||||
detailedStatus {
|
||||
__typename
|
||||
node {
|
||||
__typename
|
||||
id
|
||||
detailedStatus {
|
||||
__typename
|
||||
id
|
||||
detailsPath
|
||||
icon
|
||||
text
|
||||
}
|
||||
}
|
||||
id
|
||||
detailsPath
|
||||
icon
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,21 @@ module Resolvers
|
|||
required: false,
|
||||
description: 'Filter catalog resources by verification level.'
|
||||
|
||||
def resolve_with_lookahead(scope:, search: nil, sort: nil, verification_level: nil)
|
||||
argument :topics, [GraphQL::Types::String],
|
||||
required: false,
|
||||
description: 'Filter catalog resources by project topic names.'
|
||||
|
||||
def resolve_with_lookahead(scope:, search: nil, sort: nil, verification_level: nil, topics: nil)
|
||||
apply_lookahead(
|
||||
::Ci::Catalog::Listing
|
||||
.new(context[:current_user])
|
||||
.resources(sort: sort, search: search, scope: scope, verification_level: verification_level)
|
||||
.resources(
|
||||
sort: sort,
|
||||
search: search,
|
||||
scope: scope,
|
||||
verification_level: verification_level,
|
||||
topics: topics
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,19 +26,21 @@ module Resolvers
|
|||
GitlabSchema.parse_gids(global_ids, expected_type: ::Group).map(&:model_id)
|
||||
}
|
||||
|
||||
argument :sort, Types::Namespaces::GroupSortEnum,
|
||||
required: false,
|
||||
description: 'Sort groups by given criteria.',
|
||||
default_value: :name_asc
|
||||
|
||||
alias_method :parent, :object
|
||||
|
||||
private
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def resolve_groups(args)
|
||||
return Group.none unless parent.present?
|
||||
|
||||
GroupsFinder
|
||||
.new(context[:current_user], args.merge(parent: parent))
|
||||
.execute
|
||||
.reorder(name: :asc)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,27 @@ module Types
|
|||
value 'SIMILARITY',
|
||||
'Most similar to the search query.',
|
||||
value: :similarity
|
||||
|
||||
value 'NAME_ASC',
|
||||
'Sort by name, ascending order.',
|
||||
value: :name_asc
|
||||
value 'NAME_DESC',
|
||||
'Sort by name, descending order.',
|
||||
value: :name_desc
|
||||
|
||||
value 'PATH_ASC',
|
||||
'Sort by path, ascending order.',
|
||||
value: :path_asc
|
||||
value 'PATH_DESC',
|
||||
'Sort by path, descending order.',
|
||||
value: :path_desc
|
||||
|
||||
value 'ID_ASC',
|
||||
'Sort by ID, ascending order.',
|
||||
value: :id_asc
|
||||
value 'ID_DESC',
|
||||
'Sort by ID, descending order.',
|
||||
value: :id_desc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -184,6 +184,10 @@ module MergeRequestsHelper
|
|||
Feature.enabled?(:notifications_todos_buttons, current_user)
|
||||
end
|
||||
|
||||
def can_use_description_composer(_user, _merge_request)
|
||||
false
|
||||
end
|
||||
|
||||
def diffs_tab_pane_data(project, merge_request, params)
|
||||
{
|
||||
"is-locked": merge_request.discussion_locked?,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ module Ci
|
|||
@current_user = current_user
|
||||
end
|
||||
|
||||
def resources(sort: nil, search: nil, scope: :all, verification_level: nil)
|
||||
def resources(sort: nil, search: nil, scope: :all, verification_level: nil, topics: nil)
|
||||
relation = Ci::Catalog::Resource.published.includes(:project)
|
||||
relation = by_scope(relation, scope)
|
||||
relation = by_search(relation, search)
|
||||
relation = by_verification_level(relation, verification_level)
|
||||
relation = by_topics(relation, topics)
|
||||
|
||||
case sort.to_s
|
||||
when 'name_desc' then relation.order_by_name_desc
|
||||
|
|
@ -65,6 +66,12 @@ module Ci
|
|||
|
||||
relation.for_verification_level(level)
|
||||
end
|
||||
|
||||
def by_topics(relation, topics)
|
||||
return relation if topics.blank?
|
||||
|
||||
relation.with_topics(topics)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,6 +52,15 @@ module Ci
|
|||
)
|
||||
end
|
||||
|
||||
scope :with_topics, ->(topic_names) do
|
||||
joins(:project)
|
||||
.where(project_id: Projects::ProjectTopic
|
||||
.joins(:topic)
|
||||
.where(topics: { name: topic_names })
|
||||
.select(:project_id))
|
||||
.distinct
|
||||
end
|
||||
|
||||
# The usage counts are updated daily by Ci::Catalog::Resources::AggregateLast30DayUsageWorker
|
||||
scope :order_by_last_30_day_usage_count_desc, -> { reorder(last_30_day_usage_count: :desc) }
|
||||
scope :order_by_last_30_day_usage_count_asc, -> { reorder(last_30_day_usage_count: :asc) }
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ module Groups # rubocop:disable Gitlab/BoundedContexts -- existing top-level mod
|
|||
return error(_('Cannot mark group for deletion: feature not supported')) unless licensed || feature_downtiered?
|
||||
|
||||
result = create_deletion_schedule
|
||||
log_event if result[:status] == :success
|
||||
|
||||
send_group_deletion_notification
|
||||
if result[:status] == :success
|
||||
log_event
|
||||
send_group_deletion_notification
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
autofocus: 'false',
|
||||
form_field_classes: 'js-gfm-input markdown-area note-textarea rspec-issuable-form-description',
|
||||
project_id: @project.id,
|
||||
can_use_composer: is_merge_request ? can_use_description_composer(current_user, model).to_s : nil,
|
||||
source_branch: is_merge_request ? @merge_request.source_branch : nil,
|
||||
target_branch: is_merge_request ? @merge_request.target_branch : nil,
|
||||
can_summarize: is_merge_request ? can?(current_user, :access_summarize_new_merge_request, @project).to_s : nil } }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
%div{ data: { testid: 'issue-title-input-field' } }
|
||||
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
|
||||
autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { testid: 'issuable-form-title-field' }
|
||||
autocomplete: 'off', class: 'form-control pad js-issuable-title', dir: 'auto', data: { testid: 'issuable-form-title-field' }
|
||||
|
||||
- if issuable.respond_to?(:draft?)
|
||||
.gl-pt-3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
migration_job_name: DeleteOrphanedRoutes
|
||||
description: Deletes the orphaned routes that were not deleted by the loose foreign key
|
||||
feature_category: groups_and_projects
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186659
|
||||
milestone: '17.11'
|
||||
queued_migration_version: 20250401113424
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSnoozedUntilToSecurityPipelineExecutionProjectSchedules < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.11'
|
||||
|
||||
def change
|
||||
add_column :security_pipeline_execution_project_schedules, :snoozed_until, :datetime_with_timezone
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueDeleteOrphanedRoutes < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.11'
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
|
||||
|
||||
MIGRATION = "DeleteOrphanedRoutes"
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:routes,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :routes, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
1ebfd67fe8514f3b06285f4f76a8e7a6352515fd72ac1dffc5a7db77235ad7e4
|
||||
|
|
@ -0,0 +1 @@
|
|||
ed8f85ba760246615dd19e270ac09c0dc1b381d24dee2143dcc8296ed9bc8c71
|
||||
|
|
@ -22547,6 +22547,7 @@ CREATE TABLE security_pipeline_execution_project_schedules (
|
|||
time_window_seconds integer NOT NULL,
|
||||
cron text NOT NULL,
|
||||
cron_timezone text NOT NULL,
|
||||
snoozed_until timestamp with time zone,
|
||||
CONSTRAINT check_b93315bfbb CHECK ((char_length(cron_timezone) <= 255)),
|
||||
CONSTRAINT check_bbbe4b1b8d CHECK ((char_length(cron) <= 128)),
|
||||
CONSTRAINT check_c440017377 CHECK ((time_window_seconds > 0))
|
||||
|
|
|
|||
|
|
@ -390,6 +390,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="querycicatalogresourcesscope"></a>`scope` | [`CiCatalogResourceScope`](#cicatalogresourcescope) | Scope of the returned catalog resources. |
|
||||
| <a id="querycicatalogresourcessearch"></a>`search` | [`String`](#string) | Search term to filter the catalog resources by name or description. |
|
||||
| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort catalog resources by given criteria. |
|
||||
| <a id="querycicatalogresourcestopics"></a>`topics` | [`[String!]`](#string) | Filter catalog resources by project topic names. |
|
||||
| <a id="querycicatalogresourcesverificationlevel"></a>`verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Filter catalog resources by verification level. |
|
||||
|
||||
### `Query.ciConfig`
|
||||
|
|
@ -27569,6 +27570,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="groupdescendantgroupsincludeparentdescendants"></a>`includeParentDescendants` | [`Boolean`](#boolean) | List of descendant groups of the parent group. |
|
||||
| <a id="groupdescendantgroupsowned"></a>`owned` | [`Boolean`](#boolean) | Limit result to groups owned by authenticated user. |
|
||||
| <a id="groupdescendantgroupssearch"></a>`search` | [`String`](#string) | Search query for group name or group full path. |
|
||||
| <a id="groupdescendantgroupssort"></a>`sort` | [`GroupSort`](#groupsort) | Sort groups by given criteria. |
|
||||
|
||||
##### `Group.doraPerformanceScoreCounts`
|
||||
|
||||
|
|
@ -43006,6 +43008,12 @@ Values for sorting groups.
|
|||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="groupsortid_asc"></a>`ID_ASC` | Sort by ID, ascending order. |
|
||||
| <a id="groupsortid_desc"></a>`ID_DESC` | Sort by ID, descending order. |
|
||||
| <a id="groupsortname_asc"></a>`NAME_ASC` | Sort by name, ascending order. |
|
||||
| <a id="groupsortname_desc"></a>`NAME_DESC` | Sort by name, descending order. |
|
||||
| <a id="groupsortpath_asc"></a>`PATH_ASC` | Sort by path, ascending order. |
|
||||
| <a id="groupsortpath_desc"></a>`PATH_DESC` | Sort by path, descending order. |
|
||||
| <a id="groupsortsimilarity"></a>`SIMILARITY` | Most similar to the search query. |
|
||||
|
||||
### `GroupingEnum`
|
||||
|
|
|
|||
|
|
@ -147,12 +147,6 @@ Fallback keys follow the same processing logic as `cache:key`:
|
|||
|
||||
### Global fallback key
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/1534) in GitLab Runner 13.4.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
You can use the `$CI_COMMIT_REF_SLUG` [predefined variable](../variables/predefined_variables.md)
|
||||
to specify your [`cache:key`](../yaml/_index.md#cachekey). For example, if your
|
||||
`$CI_COMMIT_REF_SLUG` is `test`, you can set a job to download cache that's tagged with `test`.
|
||||
|
|
|
|||
|
|
@ -106,8 +106,6 @@ to include the file.
|
|||
[[runners.kubernetes.volumes.config_map]]
|
||||
name = "docker-client-config"
|
||||
mount_path = "/root/.docker/config.json"
|
||||
# If you are running GitLab Runner 13.5
|
||||
# or lower you can remove this
|
||||
sub_path = "config.json"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -277,11 +277,6 @@ variables:
|
|||
# The 'docker' hostname is the alias of the service container as described at
|
||||
# https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services
|
||||
#
|
||||
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
|
||||
# the variable must be set to tcp://localhost:2375 because of how the
|
||||
# Kubernetes executor connects services to the job container
|
||||
# DOCKER_HOST: tcp://localhost:2375
|
||||
#
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
#
|
||||
# This instructs Docker not to start over TLS.
|
||||
|
|
@ -348,10 +343,6 @@ To use Docker-in-Docker with TLS enabled in Kubernetes:
|
|||
#
|
||||
# The 'docker' hostname is the alias of the service container as described at
|
||||
# https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
|
||||
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
|
||||
# the variable must be set to tcp://localhost:2376 because of how the
|
||||
# Kubernetes executor connects services to the job container
|
||||
# DOCKER_HOST: tcp://localhost:2376
|
||||
#
|
||||
# Specify to Docker where to create the certificates. Docker
|
||||
# creates them automatically on boot, and creates
|
||||
|
|
@ -416,10 +407,6 @@ For example:
|
|||
#
|
||||
# The 'docker' hostname is the alias of the service container as described at
|
||||
# https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
|
||||
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
|
||||
# the variable must be set to tcp://localhost:2376 because of how the
|
||||
# Kubernetes executor connects services to the job container
|
||||
# DOCKER_HOST: tcp://localhost:2376
|
||||
#
|
||||
# This instructs Docker not to start over TLS.
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
|
|
|
|||
|
|
@ -249,10 +249,6 @@ To define which option should be used, the runner process reads the configuratio
|
|||
|
||||
### Requirements and limitations
|
||||
|
||||
- Available for [Docker executor](https://docs.gitlab.com/runner/executors/docker/)
|
||||
in GitLab Runner 12.0 and later.
|
||||
- Available for [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes/)
|
||||
in GitLab Runner 13.1 and later.
|
||||
- [Credentials Store](#use-a-credentials-store) and [Credential Helpers](#use-credential-helpers)
|
||||
require binaries to be added to the GitLab Runner `$PATH`, and require access to do so. Therefore,
|
||||
these features are not available on instance runners, or any other runner where the user does not
|
||||
|
|
@ -260,13 +256,6 @@ To define which option should be used, the runner process reads the configuratio
|
|||
|
||||
### Use statically-defined credentials
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Introduced in GitLab Runner 1.8 for Docker executor.
|
||||
- Introduced in GitLab Runner 13.1 for Kubernetes executor.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
You can access a private registry using two approaches. Both require setting the CI/CD variable
|
||||
`DOCKER_AUTH_CONFIG` with appropriate authentication information.
|
||||
|
||||
|
|
@ -411,13 +400,6 @@ To add `DOCKER_AUTH_CONFIG` to a runner:
|
|||
|
||||
### Use a Credentials Store
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Introduced in GitLab Runner 9.5 for Docker executor.
|
||||
- Introduced in GitLab Runner 13.1 for Kubernetes executor.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
To configure a Credentials Store:
|
||||
|
||||
1. To use a Credentials Store, you need an external helper program to interact with a specific keychain or external store.
|
||||
|
|
@ -446,13 +428,6 @@ pulling from Docker Hub fails. Docker daemon tries to use the same credentials f
|
|||
|
||||
### Use Credential Helpers
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Introduced in GitLab Runner 12.0 for Docker executor.
|
||||
- Introduced in GitLab Runner 13.1 for Kubernetes executor.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
As an example, let's assume that you want to use the `<aws_account_id>.dkr.ecr.<region>.amazonaws.com/private/image:latest`
|
||||
image. This image is private and requires you to sign in to a private container registry.
|
||||
|
||||
|
|
|
|||
|
|
@ -258,19 +258,6 @@ a `service`.
|
|||
This functionality is covered in [the CI services](../services/_index.md)
|
||||
documentation.
|
||||
|
||||
## Testing things locally
|
||||
|
||||
With GitLab Runner 1.0 you can also test any changes locally. From your
|
||||
terminal execute:
|
||||
|
||||
```shell
|
||||
# Check using docker executor
|
||||
gitlab-runner exec docker test:app
|
||||
|
||||
# Check using shell executor
|
||||
gitlab-runner exec shell test:app
|
||||
```
|
||||
|
||||
## Example project
|
||||
|
||||
We have set up an [Example PHP Project](https://gitlab.com/gitlab-examples/php) for your convenience
|
||||
|
|
|
|||
|
|
@ -104,10 +104,17 @@ The pipeline now executes the jobs as configured.
|
|||
|
||||
#### Prefill variables in manual pipelines
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Markdown rendering on the **Run pipeline** page [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/441474) in GitLab 17.11.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
You can use the [`description` and `value`](../yaml/_index.md#variablesdescription)
|
||||
keywords to [define pipeline-level (global) variables](../variables/_index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
|
||||
that are prefilled when running a pipeline manually. Use the description to explain
|
||||
information such as what the variable is used for, and what the acceptable values are.
|
||||
You can use Markdown in the description.
|
||||
|
||||
Job-level variables cannot be pre-filled.
|
||||
|
||||
|
|
|
|||
|
|
@ -691,12 +691,6 @@ Where `$REFSPECS` is a value provided to the runner internally by GitLab.
|
|||
|
||||
### Sync or exclude specific submodules from CI jobs
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/2249) in GitLab Runner 14.0.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Use the `GIT_SUBMODULE_PATHS` variable to control which submodules have to be synced or updated.
|
||||
You can set it globally or per-job in the [`variables`](../yaml/_index.md#variables) section.
|
||||
|
||||
|
|
|
|||
|
|
@ -269,34 +269,16 @@ test:
|
|||
|
||||
## Available settings for `services`
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Introduced in GitLab and GitLab Runner 9.4.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
| Setting | Required | GitLab version | Description |
|
||||
|-----------------------------------|--------------------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | yes, when used with any other option | 9.4 | Full name of the image to use. If the full image name includes a registry hostname, use the `alias` option to define a shorter service access name. For more information, see [Accessing the services](#accessing-the-services). |
|
||||
| `entrypoint` | no | 9.4 | Command or script to execute as the container's entrypoint. It's translated to the Docker `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint) directive, where each shell token is a separate string in the array. |
|
||||
| `command` | no | 9.4 | Command or script that should be used as the container's command. It's translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`](https://docs.docker.com/reference/dockerfile/#cmd) directive, where each shell token is a separate string in the array. |
|
||||
| `alias` <sup>1</sup> <sup>3</sup> | no | 9.4 | Additional aliases to access the service from the job's container. Multiple aliases can be separated by spaces or commas. For more information, see [Accessing the services](#accessing-the-services). |
|
||||
| `variables` <sup>2</sup> | no | 14.5 | Additional environment variables that are passed exclusively to the service. The syntax is the same as [Job Variables](../variables/_index.md). Service variables cannot reference themselves. |
|
||||
|
||||
**Footnotes:**
|
||||
|
||||
1. Alias support for the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2229) in GitLab Runner 12.8, and is only available for Kubernetes version 1.7 or later.
|
||||
1. Service variables support for the Docker and the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3158) in GitLab Runner 14.8.
|
||||
1. Use alias as a container name for the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421131) in GitLab Runner 17.9. For more information, see [Configuring the service containers name with the Kubernetes executor](#using-aliases-as-service-container-names-for-the-kubernetes-executor).
|
||||
| Setting | Required | GitLab version | Description |
|
||||
|--------------|--------------------------------------|----------------|-------------|
|
||||
| `name` | yes, when used with any other option | 9.4 | Full name of the image to use. If the full image name includes a registry hostname, use the `alias` option to define a shorter service access name. For more information, see [Accessing the services](#accessing-the-services). |
|
||||
| `entrypoint` | no | 9.4 | Command or script to execute as the container's entrypoint. It's translated to the Docker `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint) directive, where each shell token is a separate string in the array. |
|
||||
| `command` | no | 9.4 | Command or script that should be used as the container's command. It's translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`](https://docs.docker.com/reference/dockerfile/#cmd) directive, where each shell token is a separate string in the array. |
|
||||
| `alias` | no | 9.4 | Additional aliases to access the service from the job's container. Multiple aliases can be separated by spaces or commas. For more information, see [Accessing the services](#accessing-the-services). Using alias as a container name for the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421131) in GitLab Runner 17.9. For more information, see [Configuring the service containers name with the Kubernetes executor](#using-aliases-as-service-container-names-for-the-kubernetes-executor). |
|
||||
| `variables` | no | 14.5 | Additional environment variables that are passed exclusively to the service. The syntax is the same as [Job Variables](../variables/_index.md). Service variables cannot reference themselves. |
|
||||
|
||||
## Starting multiple services from the same image
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended configuration options](../docker/using_docker_images.md#extended-docker-configuration-options).
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Before the new extended Docker configuration options, the following configuration
|
||||
would not work properly:
|
||||
|
||||
|
|
@ -328,12 +310,6 @@ in `.gitlab-ci.yml` file.
|
|||
|
||||
## Setting a command for the service
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended configuration options](../docker/using_docker_images.md#extended-docker-configuration-options).
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Let's assume you have a `super/sql:latest` image with some SQL database
|
||||
in it. You would like to use it as a service for your job. Let's also
|
||||
assume that this image does not start the database process while starting
|
||||
|
|
|
|||
|
|
@ -78,9 +78,6 @@ java:
|
|||
junit: build/test-results/test/**/TEST-*.xml
|
||||
```
|
||||
|
||||
In [GitLab Runner 13.0](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620)
|
||||
and later, you can use `**`.
|
||||
|
||||
### Maven
|
||||
|
||||
For parsing [Surefire](https://maven.apache.org/surefire/maven-surefire-plugin/)
|
||||
|
|
|
|||
|
|
@ -1344,11 +1344,7 @@ link outside it.
|
|||
**Supported values**:
|
||||
|
||||
- An array of file paths, relative to the project directory.
|
||||
- You can use Wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming))
|
||||
patterns and:
|
||||
- In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620),
|
||||
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
|
||||
- In GitLab Runner 12.10 and earlier, [`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
|
||||
- You can use Wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns and [`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match) patterns.
|
||||
- For [GitLab Pages job](#pages):
|
||||
- In [GitLab 17.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/428018),
|
||||
the [`pages.publish`](#pagespublish) path is automatically appended to `artifacts:paths`,
|
||||
|
|
@ -1787,12 +1783,8 @@ Use the `cache:paths` keyword to choose which files or directories to cache.
|
|||
**Supported values**:
|
||||
|
||||
- An array of paths relative to the project directory (`$CI_PROJECT_DIR`).
|
||||
You can use wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming))
|
||||
patterns:
|
||||
- In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620),
|
||||
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
|
||||
- In GitLab Runner 12.10 and earlier,
|
||||
[`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
|
||||
You can use wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) and
|
||||
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match) patterns.
|
||||
|
||||
[CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file) are supported.
|
||||
|
||||
|
|
@ -6113,7 +6105,7 @@ The description displays with [the prefilled variable name when running a pipeli
|
|||
|
||||
**Supported values**:
|
||||
|
||||
- A string.
|
||||
- A string. You can use Markdown.
|
||||
|
||||
**Example of `variables:description`**:
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,34 @@ The [subscription for Chat](duo_chat.md#graphql-subscription) behaves differentl
|
|||
|
||||
To not have many concurrent subscriptions, you should also only subscribe to the subscription once the mutation is sent by using [`skip()`](https://apollo.vuejs.org/guide-option/subscriptions.html#skipping-the-subscription).
|
||||
|
||||
##### Clarifying different ID parameters
|
||||
|
||||
When working with the `aiAction` mutation, several ID parameters are used for routing requests and responses correctly. Here's what each parameter does:
|
||||
|
||||
- **user_id** (required)
|
||||
- Purpose: Identifies and authenticates the requesting user
|
||||
- Used for: Permission checks, request attribution, and response routing
|
||||
- Example: `gid://gitlab/User/123`
|
||||
- Note: This ID is automatically included by the GraphQL API framework
|
||||
|
||||
- **client_subscription_id** (recommended for streaming or multiple features)
|
||||
- Client-generated UUID for tracking specific request/response pairs
|
||||
- Required when using streaming responses or when multiple AI features share the same page
|
||||
- Example: `"9f5dedb3-c58d-46e3-8197-73d653c71e69"`
|
||||
- Can be omitted for simple, isolated requests with no streaming
|
||||
|
||||
- **resource_id** (contextual - required for some features)
|
||||
- Purpose: References a specific GitLab entity (project, issue, MR) that provides context for the AI operation
|
||||
- Used for: Permission verification and contextual information gathering
|
||||
- Real example: `"gid://gitlab/Issue/164723626"`
|
||||
- Note: Some features may not require a specific resource
|
||||
|
||||
- **project_id** (contextual - required for some features)
|
||||
- Purpose: Identifies the project context for the AI operation
|
||||
- Used for: Project-specific permission checks and context
|
||||
- Real example: `"gid://gitlab/Project/278964"`
|
||||
- Note: Some features may not require a specific project
|
||||
|
||||
#### Current abstraction layer flow
|
||||
|
||||
The following graph uses VertexAI as an example. You can use different providers.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ query: label = "AI Model Migration" AND opened = true
|
|||
|
||||
LLM models are constantly evolving, and GitLab needs to regularly update our AI features to support newer models. This guide provides a structured approach for migrating AI features to new models while maintaining stability and reliability.
|
||||
|
||||
*Note: GitLab strives to leverage the latest AI model capabilities to help provide optimal performance and features, which means model updates from existing GitLab subprocessors might occur without specific customer notifications beyond documentation updates.*
|
||||
|
||||
## Model Migration Timelines
|
||||
|
||||
Model migrations typically follow these general timelines:
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@ This page contains possible solutions for problems you might encounter when usin
|
|||
|
||||
## SAML debugging tools
|
||||
|
||||
SAML responses are base64 encoded, so we recommend the following browser plugins to decode them on the fly:
|
||||
|
||||
- [SAML-tracer](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/) for Firefox.
|
||||
- [SAML Message Decoder](https://chromewebstore.google.com/detail/mpabchoaimgbdbbjjieoaeiibojelbhm?hl=en) for Chrome.
|
||||
SAML responses are base64 encoded. To decode them on the fly you can use the **SAML-tracer** browser extension ([Firefox](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/), [Chrome](https://chromewebstore.google.com/detail/saml-tracer/mpdajninpobndbfcldcmbpnnbhibjmch?hl=en)).
|
||||
|
||||
If you cannot install a browser plugin, you can [manually generate and capture a SAML response](#manually-generate-a-saml-response) instead.
|
||||
|
||||
|
|
|
|||
|
|
@ -85,12 +85,40 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
def immediately_delete_project_error(project)
|
||||
if !project.marked_for_deletion_at?
|
||||
'Project must be marked for deletion first.'
|
||||
elsif project.full_path != params[:full_path]
|
||||
'`full_path` is incorrect. You must enter the complete path for the project.'
|
||||
end
|
||||
end
|
||||
|
||||
def delete_project(user_project)
|
||||
destroy_conditionally!(user_project) do
|
||||
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
|
||||
permanently_remove = ::Gitlab::Utils.to_boolean(params[:permanently_remove])
|
||||
|
||||
if permanently_remove && user_project.adjourned_deletion_configured?
|
||||
error = immediately_delete_project_error(user_project)
|
||||
|
||||
return render_api_error!(error, 400) if error
|
||||
end
|
||||
|
||||
accepted!
|
||||
if permanently_remove || !user_project.adjourned_deletion_configured?
|
||||
destroy_conditionally!(user_project) do
|
||||
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
|
||||
end
|
||||
|
||||
return accepted!
|
||||
end
|
||||
|
||||
result = destroy_conditionally!(user_project) do
|
||||
::Projects::MarkForDeletionService.new(user_project, current_user, {}).execute
|
||||
end
|
||||
|
||||
if result[:status] == :success
|
||||
accepted!
|
||||
else
|
||||
render_api_error!(result[:message], 400)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_projects_api_rate_limit_for_unauthenticated_users!
|
||||
|
|
|
|||
|
|
@ -237,11 +237,15 @@ module Gitlab
|
|||
end
|
||||
|
||||
def save_current_token_in_env
|
||||
::Current.token_info = {
|
||||
token_info = {
|
||||
token_id: access_token.id,
|
||||
token_type: access_token.class.to_s,
|
||||
token_scopes: access_token.scopes.map(&:to_sym)
|
||||
}
|
||||
|
||||
token_info[:token_application_id] = access_token.application_id if access_token.respond_to?(:application_id)
|
||||
|
||||
::Current.token_info = token_info
|
||||
end
|
||||
|
||||
def save_auth_failure_in_application_context(access_token, cause, requested_scopes)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class DeleteOrphanedRoutes < BatchedMigrationJob
|
||||
operation_name :delete_orphaned_routes
|
||||
feature_category :groups_and_projects
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
sub_batch
|
||||
.joins('LEFT JOIN namespaces ON namespaces.id = routes.namespace_id')
|
||||
.where(namespaces: { id: nil })
|
||||
.delete_all
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
|
||||
return {} unless params
|
||||
|
||||
params.slice(:token_type, :token_id)
|
||||
params.slice(:token_type, :token_id, :token_application_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1055,6 +1055,9 @@ msgstr ""
|
|||
msgid "%{labelStart}Project:%{labelEnd} %{project}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Reachable:%{labelEnd} %{reachability}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{labelStart}Report type:%{labelEnd} %{reportType}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6388,9 +6391,6 @@ msgstr ""
|
|||
msgid "AmazonQ|Audience"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Automatic code reviews can only be enabled when Amazon Q is set to \"On by default\""
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Availability"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6430,9 +6430,6 @@ msgstr ""
|
|||
msgid "AmazonQ|Delete the IAM role."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Enable automatic code reviews"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Enter the IAM role's ARN."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6457,6 +6454,9 @@ msgstr ""
|
|||
msgid "AmazonQ|GitLab Duo with Amazon Q is ready to go! 🎉"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Have Amazon Q review code in merge requests automatically"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|I understand that by selecting Save changes, GitLab creates a service account for Amazon Q and sends its credentials to AWS. Use of the Amazon Q Developer capabilities as part of GitLab Duo with Amazon Q is governed by the %{helpStart}AWS Customer Agreement%{helpEnd} or other written agreement between you and AWS governing your use of AWS services. Amazon Q Developer processes data across all US Regions and makes cross-region API calls when your requests require it."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9420,6 +9420,9 @@ msgstr ""
|
|||
msgid "Best Regards,"
|
||||
msgstr ""
|
||||
|
||||
msgid "Best practices"
|
||||
msgstr ""
|
||||
|
||||
msgid "Beta"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10410,6 +10413,9 @@ msgstr ""
|
|||
msgid "Branch rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branch settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branch target"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15619,6 +15625,9 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|A rule is configured to require two approvals."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|API Security testing identifies vulnerabilities specific to your application APIs before they can be exploited"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|All attached frameworks"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15628,12 +15637,36 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|At least two approvals"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Change your project visibility settings to comply with organizational security requirements"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Check"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Checks"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Code quality scanning identifies maintainability issues that could lead to increased security risks over time"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Configure DAST in your CI/CD pipeline to automatically test your application for security issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Configure DAST scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Configure approval requirements"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Configure your project settings to prevent merge request authors from approving their own changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Configure your project to require at least two approvals on merge requests to improve code quality and security"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Container scanning checks your container images for known vulnerabilities to prevent exploitation in production"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|DAST scan"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15646,6 +15679,15 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|Date since last status change"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Default branch protection prevents direct commits and ensures changes are reviewed through merge requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Dependency scanning identifies vulnerable dependencies in your project that could be exploited by attackers"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Dynamic Application Security Testing (DAST) identifies runtime vulnerabilities by analyzing your application while it runs"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Enable DAST scanner"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15658,12 +15700,42 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|Enable SAST scanner in the project's security configuration to satisfy this requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Enable code quality scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Enable code quality scanning to improve code maintainability and reduce technical debt"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Enable dependency scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Enable dependency scanning to automatically detect vulnerable libraries in your application"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Enforcing minimum approval requirements ensures code changes are properly reviewed before merging"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Ensuring that code committers cannot approve their contributed merge requests maintains separation of duties"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|External control"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Failed controls: %{failed}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Failure reason"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Filter by"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Fix available"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Fix suggestions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15673,6 +15745,9 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|Frameworks"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Fuzz testing automatically generates random inputs to find unexpected behavior and potential security issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Group by"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15697,9 +15772,33 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|How to fix"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Implement API security testing"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Implement API security testing to protect your application interfaces from attacks"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Implement secret detection"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Implement secret detection scanning in your CI/CD pipeline to identify and remove exposed credentials"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Infrastructure as Code (IaC) scanning detects misconfigurations in your infrastructure definitions before deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Last scanned"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Learn about code review best practices"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Learn more about implementing effective code review practices to enhance security"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|License compliance scanning identifies potentially problematic open source licenses that could create legal issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Merge request approval rules"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15724,21 +15823,39 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|None"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Organization policy requires that projects are not set to internal visibility to protect sensitive data"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Other compliance frameworks applied to %{linkStart}%{projectName}%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Passed"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Passed controls: %{passed}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Pending"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Pending controls: %{pending}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Prevent author approvals"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Prevent authors as approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Prevent committer approvals"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Prevent committers as approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Preventing authors from approving their own merge requests ensures independent code review"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Project"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15757,6 +15874,12 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|Requirements"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Review best practices for secure container deployments"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Review container security practices"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|SAST scan"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15766,12 +15889,39 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|SAST scanner is not configured in the pipeline configuration for the default branch."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Secret detection prevents sensitive information like API keys from being accidentally committed to your repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Set up branch protection"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Set up branch protection rules for your default branch to enforce quality standards"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Set up container scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Set up container scanning in your pipeline to identify vulnerabilities in your container images"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Set up fuzz testing"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Set up fuzz testing in your pipeline to identify edge cases and potential crashes"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Single Sign-On authentication improves security by centralizing user access management"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Standard"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Standards"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Static Application Security Testing (SAST) scans your code for vulnerabilities that may lead to exploits"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15781,12 +15931,21 @@ msgstr ""
|
|||
msgid "ComplianceStandardsAdherence|The following features help satisfy this requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|This is an external control for %{link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Unable to load the standards adherence report. Refresh the page and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Update approval settings in the project's merge request settings to satisfy this requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Update project visibility"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|Update your approval settings to prevent committers from approving merge requests containing their commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceStandardsAdherence|View details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15853,6 +16012,9 @@ msgstr ""
|
|||
msgid "Configuration help"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15928,6 +16090,9 @@ msgstr ""
|
|||
msgid "Configure advanced permissions, Large File Storage, two-factor authentication, and customer relations settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure approval rules"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure checkin reminder frequency"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15943,6 +16108,9 @@ msgstr ""
|
|||
msgid "Configure it later"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure pipeline"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23326,6 +23494,9 @@ msgstr ""
|
|||
msgid "Enter a number from 0 to 100."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a prompt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a search query to find more branches, or use * to create a wildcard."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26279,6 +26450,9 @@ msgstr ""
|
|||
msgid "General settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Generate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Generate API key at %{site}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28103,6 +28277,9 @@ msgstr ""
|
|||
msgid "Go to the milestone list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to the pipeline editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to the project's activity feed"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30661,6 +30838,12 @@ msgstr ""
|
|||
msgid "Impersonation tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Implementation details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Implementation guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31565,6 +31748,9 @@ msgstr ""
|
|||
msgid "Insert code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert code change summary"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert column left"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35605,6 +35791,9 @@ msgstr ""
|
|||
msgid "Manage usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage visibility"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage your subscription"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44568,6 +44757,9 @@ msgstr ""
|
|||
msgid "Please follow the Let's Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please match the requested format."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a name"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46172,6 +46364,9 @@ msgstr ""
|
|||
msgid "Project security status help page"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project settings were successfully updated."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -48701,6 +48896,15 @@ msgstr ""
|
|||
msgid "Re-request review"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reachability|Not available"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reachability|Not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reachability|Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Read documentation"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -48835,6 +49039,9 @@ msgstr[1] ""
|
|||
msgid "Refreshing…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Regenerate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Regenerate export"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -52942,6 +53149,9 @@ msgstr ""
|
|||
msgid "Security reports last updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -56032,6 +56242,9 @@ msgstr ""
|
|||
msgid "Settings|What is experiment?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Setup guide"
|
||||
msgstr ""
|
||||
|
||||
msgid "Severity"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62939,6 +63152,9 @@ msgstr ""
|
|||
msgid "UTC"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ultimate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to apply suggestions to a deleted line."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -65382,6 +65598,9 @@ msgstr ""
|
|||
msgid "View trigger token usage examples"
|
||||
msgstr ""
|
||||
|
||||
msgid "View tutorial"
|
||||
msgstr ""
|
||||
|
||||
msgid "View usage details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -66003,6 +66222,9 @@ msgstr ""
|
|||
msgid "Vulnerability|Stacktrace snippet:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Static reachability is a factor of dependencies (and the relevant CVEs) which indicates that there is a high probability that the package is in use, and therefore the risk of it is higher."
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -66069,6 +66291,9 @@ msgstr ""
|
|||
msgid "Vulnerability|What is EPSS?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|What is Reachability?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|What is code flow?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -68307,6 +68532,9 @@ msgstr ""
|
|||
msgid "Write milestone description…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Write with GitLab Duo"
|
||||
msgstr ""
|
||||
|
||||
msgid "Write your release notes or drag your files here…"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"preinstall": "node ./scripts/frontend/preinstall.mjs",
|
||||
"postinstall": "node ./scripts/frontend/postinstall.js",
|
||||
"storybook:install": "yarn --cwd ./storybook install",
|
||||
"storybook:doctor": "yarn --cwd ./storybook doctor",
|
||||
"storybook:build": "yarn tailwindcss:build && yarn --cwd ./storybook build --quiet",
|
||||
"storybook:start": "./scripts/frontend/start_storybook.sh",
|
||||
"storybook:start:skip-fixtures-update": "./scripts/frontend/start_storybook.sh --skip-fixtures-update",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ module QA
|
|||
element 'add-issue-field'
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
|
||||
element 'markdown-editor-form-field'
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue' do
|
||||
element 'issue-author'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ module QA
|
|||
element 'work-item-title', required: true
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
|
||||
element 'markdown-editor-form-field'
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/work_items/components/work_item_created_updated.vue' do
|
||||
element 'work-item-author'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,10 +18,20 @@ module QA
|
|||
element 'remove-related-issue-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/issues/show/components/description.vue' do
|
||||
element 'gfm-content'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/issues/show/components/edit_actions.vue' do
|
||||
element 'issuable-save-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/issues/show/components/header_actions.vue' do
|
||||
element 'toggle-issue-state-button'
|
||||
element 'desktop-dropdown'
|
||||
element 'delete-issue-button'
|
||||
element 'desktop-dropdown'
|
||||
element 'edit-button'
|
||||
element 'issue-header'
|
||||
element 'toggle-issue-state-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/related_issues/components/related_issues_block.vue' do
|
||||
|
|
@ -37,6 +47,15 @@ module QA
|
|||
Page::Project::Issue::Index.perform(&:work_item_enabled?)
|
||||
end
|
||||
|
||||
def edit_description(new_description)
|
||||
within_element('issue-header') do
|
||||
click_element('edit-button')
|
||||
end
|
||||
|
||||
fill_element('markdown-editor-form-field', new_description)
|
||||
click_element('issuable-save-button')
|
||||
end
|
||||
|
||||
def relate_issue(issue)
|
||||
click_element('crud-form-toggle')
|
||||
fill_element('add-issue-field', issue.web_url)
|
||||
|
|
@ -63,6 +82,10 @@ module QA
|
|||
click_element('toggle-issue-state-button', text: 'Close issue')
|
||||
end
|
||||
|
||||
def has_description?(description)
|
||||
find_element('gfm-content').text.include?(description)
|
||||
end
|
||||
|
||||
def has_reopen_issue_button?
|
||||
open_actions_dropdown
|
||||
has_element?('toggle-issue-state-button', text: 'Reopen issue')
|
||||
|
|
|
|||
|
|
@ -13,23 +13,10 @@ module QA
|
|||
element 'crud-loading'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue' do
|
||||
element 'link-item-add-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/shared/work_item_token_input.vue' do
|
||||
element 'work-item-token-select-input'
|
||||
end
|
||||
|
||||
view "app/assets/javascripts/work_items/components/" \
|
||||
"work_item_relationships/work_item_add_relationship_form.vue" do
|
||||
element 'link-work-item-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue' do
|
||||
element 'work-item-linked-items-list'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue' do
|
||||
element 'remove-work-item-link'
|
||||
end
|
||||
|
|
@ -40,6 +27,32 @@ module QA
|
|||
element 'state-toggle-action'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_description.vue' do
|
||||
element 'save-description'
|
||||
element 'work-item-description-wrapper'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_description_rendered.vue' do
|
||||
element 'work-item-description'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_detail.vue' do
|
||||
element 'work-item-edit-form-button'
|
||||
end
|
||||
|
||||
view "app/assets/javascripts/work_items/components/" \
|
||||
"work_item_relationships/work_item_add_relationship_form.vue" do
|
||||
element 'link-work-item-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue' do
|
||||
element 'link-item-add-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue' do
|
||||
element 'work-item-linked-items-list'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/work_items/components/work_item_title.vue' do
|
||||
element 'work-item-title'
|
||||
end
|
||||
|
|
@ -48,6 +61,22 @@ module QA
|
|||
element 'work-item-feedback-popover'
|
||||
end
|
||||
|
||||
def edit_description(new_description)
|
||||
close_new_issue_popover if has_element?('work-item-feedback-popover')
|
||||
wait_for_requests
|
||||
|
||||
click_element('work-item-edit-form-button')
|
||||
|
||||
within_element('work-item-description-wrapper') do
|
||||
fill_element('markdown-editor-form-field', new_description)
|
||||
click_element('save-description')
|
||||
end
|
||||
end
|
||||
|
||||
def has_description?(description)
|
||||
find_element('work-item-description').text.include?(description)
|
||||
end
|
||||
|
||||
def has_delete_issue_button?
|
||||
open_actions_dropdown
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,22 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/526755
|
||||
it(
|
||||
'creates an issue and updates the description',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/533855'
|
||||
) do
|
||||
resource, _, show_page_type = create_new_issue
|
||||
updated_description = "Updated issue description"
|
||||
|
||||
resource.visit!
|
||||
show_page_type.perform do |show|
|
||||
show.edit_description(updated_description)
|
||||
|
||||
expect(show).to have_description(updated_description)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using attachments in comments', :object_storage do
|
||||
let(:png_file_name) { 'testfile.png' }
|
||||
let(:file_to_attach) { Runtime::Path.fixture('designs', png_file_name) }
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
|
|||
'LICENSE',
|
||||
'Pipfile.lock',
|
||||
'storybook/.env.template',
|
||||
'storybook/.babelrc.json',
|
||||
'yarn-error.log'
|
||||
] +
|
||||
Dir.glob('.bundle/**/*') +
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat
|
|||
expect(page).to have_content('pipeline schedule')
|
||||
|
||||
within_testid('next-run-cell') do
|
||||
expect(find('time')['title']).to include(pipeline_schedule.real_next_run.strftime('%B %-d, %Y'))
|
||||
# validate the format instead of the actual time because timezone issues were causing flaky tests
|
||||
expect(find('time')['title']).to match(/[A-Z][a-z]+ \d+, \d{4} at \d+:\d+:\d+ [AP]M [A-Z]{3,4}/)
|
||||
end
|
||||
|
||||
expect(page).to have_link('master')
|
||||
|
|
@ -210,6 +211,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat
|
|||
end
|
||||
|
||||
it 'prevents an invalid form from being submitted' do
|
||||
fill_in 'schedule-description', with: 'my fancy description'
|
||||
create_pipeline_schedule
|
||||
|
||||
expect(page).to have_content("Cron timezone can't be blank")
|
||||
|
|
|
|||
|
|
@ -67,6 +67,110 @@ RSpec.describe Groups::UserGroupsFinder, feature_category: :groups_and_projects
|
|||
end
|
||||
end
|
||||
|
||||
context 'when sorting results' do
|
||||
context 'when sorting by name' do
|
||||
context 'in ascending order' do
|
||||
let(:arguments) { { sort: :name_asc } }
|
||||
|
||||
it 'sorts the groups by name in ascending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
public_maintainer_group,
|
||||
public_owner_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in descending order' do
|
||||
let(:arguments) { { sort: :name_desc } }
|
||||
|
||||
it 'sorts the groups by name in descending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
guest_group,
|
||||
public_developer_group,
|
||||
private_maintainer_group,
|
||||
public_owner_group,
|
||||
public_maintainer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting by path' do
|
||||
context 'in ascending order' do
|
||||
let(:arguments) { { sort: :path_asc } }
|
||||
|
||||
it 'sorts the groups by path in ascending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
public_maintainer_group,
|
||||
public_owner_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in descending order' do
|
||||
let(:arguments) { { sort: :path_desc } }
|
||||
|
||||
it 'sorts the groups by path in descending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
guest_group,
|
||||
public_developer_group,
|
||||
private_maintainer_group,
|
||||
public_owner_group,
|
||||
public_maintainer_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting by ID' do
|
||||
context 'in ascending order' do
|
||||
let(:arguments) { { sort: :id_asc } }
|
||||
|
||||
it 'sorts the groups by ID in ascending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
guest_group,
|
||||
private_maintainer_group,
|
||||
public_developer_group,
|
||||
public_maintainer_group,
|
||||
public_owner_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in descending order' do
|
||||
let(:arguments) { { sort: :id_desc } }
|
||||
|
||||
it 'sorts the groups by ID in descending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
public_owner_group,
|
||||
public_maintainer_group,
|
||||
public_developer_group,
|
||||
private_maintainer_group,
|
||||
guest_group
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns all groups where the user is a direct member' do
|
||||
is_expected.to contain_exactly(
|
||||
public_maintainer_group,
|
||||
|
|
|
|||
|
|
@ -51,10 +51,6 @@ describe('InputsAdoptionAlert', () => {
|
|||
it('sets the correct props', () => {
|
||||
expect(findAlert().props()).toMatchObject({
|
||||
variant: 'tip',
|
||||
primaryButtonLink: defaultProvide.pipelineEditorPath,
|
||||
primaryButtonText: 'Go to the pipeline editor',
|
||||
secondaryButtonLink: '/help/ci/yaml/inputs',
|
||||
secondaryButtonText: 'Learn more',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ export const mockPipelineInputsResponse = {
|
|||
options: [],
|
||||
regex: null,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
description: 'Tags for deployment',
|
||||
default: '',
|
||||
type: 'ARRAY',
|
||||
required: false,
|
||||
options: [],
|
||||
regex: null,
|
||||
},
|
||||
],
|
||||
__typename: 'Project',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ describe('PipelineInputsForm', () => {
|
|||
let wrapper;
|
||||
let pipelineInputsHandler;
|
||||
|
||||
const createComponent = ({ props = {}, provide = {} } = {}) => {
|
||||
const createComponent = async ({ props = {}, provide = {} } = {}) => {
|
||||
const handlers = [[getPipelineInputsQuery, pipelineInputsHandler]];
|
||||
const mockApollo = createMockApollo(handlers);
|
||||
wrapper = shallowMountExtended(PipelineInputsForm, {
|
||||
|
|
@ -45,6 +45,7 @@ describe('PipelineInputsForm', () => {
|
|||
},
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
const findSkeletonLoader = () => wrapper.findComponent(InputsTableSkeletonLoader);
|
||||
|
|
@ -104,12 +105,21 @@ describe('PipelineInputsForm', () => {
|
|||
options: [],
|
||||
regex: null,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
description: 'Tags for deployment',
|
||||
default: '',
|
||||
type: 'ARRAY',
|
||||
required: false,
|
||||
options: [],
|
||||
regex: null,
|
||||
},
|
||||
];
|
||||
expect(findInputsTable().props('inputs')).toEqual(expectedInputs);
|
||||
});
|
||||
|
||||
it('updates the count in the crud component', () => {
|
||||
expect(findCrudComponent().props('count')).toBe(2);
|
||||
expect(findCrudComponent().props('count')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -135,7 +145,6 @@ describe('PipelineInputsForm', () => {
|
|||
|
||||
it('handles GraphQL error', async () => {
|
||||
await createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'There was a problem fetching the pipeline inputs.',
|
||||
|
|
@ -161,7 +170,6 @@ describe('PipelineInputsForm', () => {
|
|||
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
|
||||
const savedInputs = [{ name: 'deploy_environment', value: 'saved-value' }];
|
||||
await createComponent({ props: { savedInputs } });
|
||||
await waitForPromises();
|
||||
|
||||
const updatedInput = findInputsTable()
|
||||
.props('inputs')
|
||||
|
|
@ -174,7 +182,6 @@ describe('PipelineInputsForm', () => {
|
|||
it('processes and emits update events from the table component', async () => {
|
||||
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
|
||||
await createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
const updatedInput = { ...wrapper.vm.inputs[0], value: 'updated-value' };
|
||||
findInputsTable().vm.$emit('update', updatedInput);
|
||||
|
|
@ -190,5 +197,49 @@ describe('PipelineInputsForm', () => {
|
|||
}));
|
||||
expect(wrapper.emitted()['update-inputs'][0][0]).toEqual(expectedEmittedValue);
|
||||
});
|
||||
|
||||
it('converts string values to arrays for ARRAY type inputs', async () => {
|
||||
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
|
||||
await createComponent();
|
||||
|
||||
// Get the array input from the current inputs prop of the table
|
||||
const inputs = findInputsTable().props('inputs');
|
||||
const arrayInput = inputs.find((input) => input.type === 'ARRAY');
|
||||
|
||||
const updatedInput = {
|
||||
...arrayInput,
|
||||
default: '[1,2,3]',
|
||||
};
|
||||
|
||||
findInputsTable().vm.$emit('update', updatedInput);
|
||||
|
||||
// Check that the emitted value contains the converted array
|
||||
const emittedValues = wrapper.emitted()['update-inputs'][0][0];
|
||||
const emittedArrayValue = emittedValues.find((item) => item.name === 'tags').value;
|
||||
|
||||
expect(Array.isArray(emittedArrayValue)).toBe(true);
|
||||
expect(emittedArrayValue).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('converts complex object arrays correctly', async () => {
|
||||
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
|
||||
await createComponent();
|
||||
|
||||
const inputs = findInputsTable().props('inputs');
|
||||
const arrayInput = inputs.find((input) => input.type === 'ARRAY');
|
||||
|
||||
const updatedInput = {
|
||||
...arrayInput,
|
||||
default: '[{"key": "value"}, {"another": "object"}]',
|
||||
};
|
||||
|
||||
findInputsTable().vm.$emit('update', updatedInput);
|
||||
|
||||
const emittedValues = wrapper.emitted()['update-inputs'][0][0];
|
||||
const emittedArrayValue = emittedValues.find((item) => item.name === 'tags').value;
|
||||
|
||||
expect(Array.isArray(emittedArrayValue)).toBe(true);
|
||||
expect(emittedArrayValue).toEqual([{ key: 'value' }, { another: 'object' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlButton, GlDisclosureDropdown, GlDropdownDivider, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlDisclosureDropdown, GlLoadingIcon } from '@gitlab/ui';
|
||||
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { createAlert } from '~/alert';
|
||||
|
|
@ -46,7 +46,7 @@ describe('PipelineStageDropdown', () => {
|
|||
|
||||
const findCiIcon = () => wrapper.findComponent(CiIcon);
|
||||
const findDropdownButton = () => wrapper.findComponent(GlButton);
|
||||
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
|
||||
const findDropdownGroupJobs = () => wrapper.findByTestId('passed-jobs');
|
||||
const findJobDropdownItems = () => wrapper.findAllComponents(JobDropdownItem);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findStageDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
|
|
@ -175,7 +175,7 @@ describe('PipelineStageDropdown', () => {
|
|||
});
|
||||
|
||||
it('renders divider', () => {
|
||||
expect(findDropdownDivider().exists()).toBe(true);
|
||||
expect(findDropdownGroupJobs().attributes('class')).toContain('gl-border-t-dropdown-divider');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ describe('PipelineStageDropdown', () => {
|
|||
});
|
||||
|
||||
it('does not render divider', () => {
|
||||
expect(findDropdownDivider().exists()).toBe(false);
|
||||
expect(findDropdownGroupJobs().props('bordered')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_
|
|||
import { VARIABLE_TYPE } from '~/ci/pipeline_new/constants';
|
||||
import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue';
|
||||
import PipelineVariablesForm from '~/ci/pipeline_new/components/pipeline_variables_form.vue';
|
||||
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/ci/utils');
|
||||
|
|
@ -49,6 +50,19 @@ describe('PipelineVariablesForm', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const configVariablesWithMarkdown = [
|
||||
{
|
||||
key: 'VAR_WITH_MARKDOWN',
|
||||
value: 'some-value',
|
||||
description: 'Variable with **Markdown** _description_',
|
||||
},
|
||||
{
|
||||
key: 'SIMPLE_VAR',
|
||||
value: 'simple-value',
|
||||
description: 'Simple variable',
|
||||
},
|
||||
];
|
||||
|
||||
const createComponent = async ({
|
||||
props = {},
|
||||
configVariables = [],
|
||||
|
|
@ -85,6 +99,7 @@ describe('PipelineVariablesForm', () => {
|
|||
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row-container');
|
||||
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key-field');
|
||||
const findRemoveButton = () => wrapper.findByTestId('remove-ci-variable-row');
|
||||
const findMarkdown = () => wrapper.findComponent(Markdown);
|
||||
|
||||
beforeEach(() => {
|
||||
mockCiConfigVariables = jest.fn().mockResolvedValue({
|
||||
|
|
@ -186,6 +201,23 @@ describe('PipelineVariablesForm', () => {
|
|||
const emptyRowExists = keyInputs.wrappers.some((w) => w.props('value') === '');
|
||||
expect(emptyRowExists).toBe(true);
|
||||
});
|
||||
|
||||
it('renders markdown if variable has description', async () => {
|
||||
await createComponent({ configVariables: configVariablesWithMarkdown });
|
||||
|
||||
expect(findMarkdown().exists()).toBe(true);
|
||||
expect(findMarkdown().props('markdown')).toBe('Variable with **Markdown** _description_');
|
||||
});
|
||||
|
||||
it('does not render anything when description is missing', async () => {
|
||||
await createComponent({
|
||||
props: {
|
||||
variableParams: { CUSTOM_VAR: 'custom-value' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(findMarkdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query configuration', () => {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
|
|||
queryToObject: jest.fn().mockReturnValue({ id: '1' }),
|
||||
}));
|
||||
|
||||
const preventDefault = jest.fn();
|
||||
|
||||
const {
|
||||
data: {
|
||||
project: {
|
||||
|
|
@ -287,8 +289,7 @@ describe('Pipeline schedules form', () => {
|
|||
findPipelineVariables().vm.$emit('update-variables', updatedVariables);
|
||||
findPipelineInputsForm().vm.$emit('update-inputs', updatedInputs);
|
||||
|
||||
findSubmitButton().vm.$emit('click');
|
||||
|
||||
findForm().vm.$emit('submit', { preventDefault });
|
||||
await waitForPromises();
|
||||
|
||||
expect(createMutationHandlerSuccess).toHaveBeenCalledWith({
|
||||
|
|
@ -311,7 +312,7 @@ describe('Pipeline schedules form', () => {
|
|||
createComponent({
|
||||
requestHandlers: [[createPipelineScheduleMutation, createMutationHandlerFailed]],
|
||||
});
|
||||
findSubmitButton().vm.$emit('click');
|
||||
findForm().vm.$emit('submit', { preventDefault });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -327,7 +328,7 @@ describe('Pipeline schedules form', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findSubmitButton().vm.$emit('click');
|
||||
findForm().vm.$emit('submit', { preventDefault });
|
||||
|
||||
expect(createMutationHandlerSuccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -437,7 +438,7 @@ describe('Pipeline schedules form', () => {
|
|||
findPipelineVariables().vm.$emit('update-variables', updatedVariables);
|
||||
findPipelineInputsForm().vm.$emit('update-inputs', updatedInputs);
|
||||
|
||||
findSubmitButton().vm.$emit('click');
|
||||
findForm().vm.$emit('submit', { preventDefault });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -466,7 +467,7 @@ describe('Pipeline schedules form', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
findSubmitButton().vm.$emit('click');
|
||||
findForm().vm.$emit('submit', { preventDefault });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -483,7 +484,7 @@ describe('Pipeline schedules form', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
await findSubmitButton().vm.$emit('click');
|
||||
findForm().vm.$emit('submit', { preventDefault });
|
||||
|
||||
expect(updateMutationHandlerSuccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
import { nextTick } from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
|
||||
import TreeList from '~/diffs/components/tree_list.vue';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { getCookie, removeCookie, setCookie } from '~/lib/utils/common_utils';
|
||||
import { TREE_LIST_WIDTH_STORAGE_KEY } from '~/diffs/constants';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import store from '~/mr_notes/stores';
|
||||
import * as types from '~/diffs/store/mutation_types';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
describe('DiffsFileTree', () => {
|
||||
useLocalStorageSpy();
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('DiffsFileTree', () => {
|
||||
let pinia;
|
||||
let wrapper;
|
||||
|
||||
useLocalStorageSpy();
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(DiffsFileTree, {
|
||||
store,
|
||||
pinia,
|
||||
propsData,
|
||||
}),
|
||||
);
|
||||
|
|
@ -32,6 +37,11 @@ describe('DiffsFileTree', () => {
|
|||
global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia();
|
||||
useLegacyDiffs();
|
||||
});
|
||||
|
||||
it('re-emits clickFile event', () => {
|
||||
const obj = {};
|
||||
createComponent();
|
||||
|
|
@ -40,15 +50,10 @@ describe('DiffsFileTree', () => {
|
|||
});
|
||||
|
||||
it('sets current file on click', () => {
|
||||
const spy = jest.spyOn(store, 'commit');
|
||||
const file = { fileHash: 'foo' };
|
||||
createComponent();
|
||||
wrapper.findComponent(TreeList).vm.$emit('clickFile', file);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
`diffs/${types.SET_CURRENT_DIFF_FILE}`,
|
||||
file.fileHash,
|
||||
undefined,
|
||||
);
|
||||
expect(useLegacyDiffs()[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith(file.fileHash);
|
||||
});
|
||||
|
||||
describe('size', () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import TreeList from '~/diffs/components/tree_list.vue';
|
||||
import createStore from '~/diffs/store/modules';
|
||||
import DiffFileRow from '~/diffs/components//diff_file_row.vue';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -10,22 +9,24 @@ import { SET_LINKED_FILE_HASH, SET_TREE_DATA, SET_DIFF_FILES } from '~/diffs/sto
|
|||
import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
|
||||
import { sortTree } from '~/ide/stores/utils';
|
||||
import { isElementClipped } from '~/lib/utils/common_utils';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
|
||||
|
||||
jest.mock('~/lib/utils/common_utils');
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('Diffs tree list component', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let setRenderTreeListMock;
|
||||
let pinia;
|
||||
const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' });
|
||||
const getFileRow = () => wrapper.findComponent(DiffFileRow);
|
||||
const findDiffTreeSearch = () => wrapper.findByTestId('diff-tree-search');
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const createComponent = ({ hideFileStats = false, ...rest } = {}) => {
|
||||
wrapper = shallowMountExtended(TreeList, {
|
||||
store,
|
||||
pinia,
|
||||
propsData: { hideFileStats, ...rest },
|
||||
stubs: {
|
||||
// eslint will fail if we import the real component
|
||||
|
|
@ -46,39 +47,14 @@ describe('Diffs tree list component', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const { getters, mutations, actions, state } = createStore();
|
||||
|
||||
setRenderTreeListMock = jest.fn();
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
diffs: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
isTreeLoaded: true,
|
||||
diffFiles: ['test'],
|
||||
addedLines: 10,
|
||||
removedLines: 20,
|
||||
mergeRequestDiff: {},
|
||||
realSize: 20,
|
||||
...state,
|
||||
},
|
||||
getters: {
|
||||
allBlobs: getters.allBlobs,
|
||||
flatBlobsList: getters.flatBlobsList,
|
||||
linkedFile: getters.linkedFile,
|
||||
fileTree: getters.fileTree,
|
||||
},
|
||||
mutations: { ...mutations },
|
||||
actions: {
|
||||
toggleTreeOpen: actions.toggleTreeOpen,
|
||||
setTreeOpen: actions.setTreeOpen,
|
||||
goToFile: actions.goToFile,
|
||||
setRenderTreeList: setRenderTreeListMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
|
||||
useLegacyDiffs().isTreeLoaded = true;
|
||||
useLegacyDiffs().diffFiles = [getDiffFileMock()];
|
||||
useLegacyDiffs().addedLines = 10;
|
||||
useLegacyDiffs().addedLines = 20;
|
||||
useLegacyDiffs().mergeRequestDiff = {};
|
||||
useLegacyDiffs().realSize = '20';
|
||||
useLegacyDiffs().setTreeOpen.mockReturnValue();
|
||||
});
|
||||
|
||||
const setupFilesInState = () => {
|
||||
|
|
@ -166,16 +142,14 @@ describe('Diffs tree list component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
Object.assign(store.state.diffs, {
|
||||
treeEntries,
|
||||
tree: [
|
||||
treeEntries.LICENSE,
|
||||
{
|
||||
...treeEntries.app,
|
||||
tree: [treeEntries.javascript, treeEntries['index.js'], treeEntries['test.rb']],
|
||||
},
|
||||
],
|
||||
});
|
||||
useLegacyDiffs().treeEntries = treeEntries;
|
||||
useLegacyDiffs().tree = [
|
||||
treeEntries.LICENSE,
|
||||
{
|
||||
...treeEntries.app,
|
||||
tree: [treeEntries.javascript, treeEntries['index.js'], treeEntries['test.rb']],
|
||||
},
|
||||
];
|
||||
|
||||
return treeEntries;
|
||||
};
|
||||
|
|
@ -247,32 +221,28 @@ describe('Diffs tree list component', () => {
|
|||
});
|
||||
|
||||
it('calls toggleTreeOpen when clicking folder', () => {
|
||||
jest.spyOn(store, 'dispatch').mockReturnValue(undefined);
|
||||
|
||||
getFileRow().vm.$emit('toggleTreeOpen', 'app');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
|
||||
expect(useLegacyDiffs().toggleTreeOpen).toHaveBeenCalledWith('app');
|
||||
});
|
||||
|
||||
it('renders when renderTreeList is false', async () => {
|
||||
store.state.diffs.renderTreeList = false;
|
||||
useLegacyDiffs().renderTreeList = false;
|
||||
|
||||
await nextTick();
|
||||
expect(getScroller().props('items')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('dispatches setTreeOpen with all paths for the current diff file', async () => {
|
||||
jest.spyOn(store, 'dispatch').mockReturnValue(undefined);
|
||||
|
||||
store.state.diffs.currentDiffFileId = 'appjavascriptfile';
|
||||
useLegacyDiffs().currentDiffFileId = 'appjavascriptfile';
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('diffs/setTreeOpen', {
|
||||
expect(useLegacyDiffs().setTreeOpen).toHaveBeenCalledWith({
|
||||
opened: true,
|
||||
path: 'app',
|
||||
});
|
||||
expect(store.dispatch).toHaveBeenCalledWith('diffs/setTreeOpen', {
|
||||
expect(useLegacyDiffs().setTreeOpen).toHaveBeenCalledWith({
|
||||
opened: true,
|
||||
path: 'app/javascript',
|
||||
});
|
||||
|
|
@ -284,7 +254,7 @@ describe('Diffs tree list component', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
setupFilesInState();
|
||||
store.state.diffs.viewedDiffFileIds = viewedDiffFileIds;
|
||||
useLegacyDiffs().viewedDiffFileIds = viewedDiffFileIds;
|
||||
});
|
||||
|
||||
it('passes the viewedDiffFileIds to the FileTree', async () => {
|
||||
|
|
@ -312,7 +282,7 @@ describe('Diffs tree list component', () => {
|
|||
|
||||
const setupFiles = (diffFiles) => {
|
||||
const { treeEntries, tree } = generateTreeList(diffFiles);
|
||||
store.commit(`diffs/${SET_TREE_DATA}`, {
|
||||
useLegacyDiffs()[SET_TREE_DATA]({
|
||||
treeEntries,
|
||||
tree: sortTree(tree),
|
||||
});
|
||||
|
|
@ -327,7 +297,7 @@ describe('Diffs tree list component', () => {
|
|||
wrapper.element.insertAdjacentHTML('afterbegin', `<div data-file-row="05.txt"><div>`);
|
||||
isElementClipped.mockReturnValueOnce(true);
|
||||
wrapper.vm.$refs.scroller.scrollToItem = jest.fn();
|
||||
store.state.diffs.currentDiffFileId = '05.txt';
|
||||
useLegacyDiffs().currentDiffFileId = '05.txt';
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.currentDiffFileId).toBe('05.txt');
|
||||
|
|
@ -349,13 +319,13 @@ describe('Diffs tree list component', () => {
|
|||
];
|
||||
|
||||
const linkFile = (fileHash) => {
|
||||
store.commit(`diffs/${SET_LINKED_FILE_HASH}`, fileHash);
|
||||
useLegacyDiffs()[SET_LINKED_FILE_HASH](fileHash);
|
||||
};
|
||||
|
||||
const setupFiles = (diffFiles) => {
|
||||
const { treeEntries, tree } = generateTreeList(diffFiles);
|
||||
store.commit(`diffs/${SET_DIFF_FILES}`, diffFiles);
|
||||
store.commit(`diffs/${SET_TREE_DATA}`, {
|
||||
useLegacyDiffs()[SET_DIFF_FILES](diffFiles);
|
||||
useLegacyDiffs()[SET_TREE_DATA]({
|
||||
treeEntries,
|
||||
tree: sortTree(tree),
|
||||
});
|
||||
|
|
@ -396,13 +366,13 @@ describe('Diffs tree list component', () => {
|
|||
${'list-view-toggle'} | ${false}
|
||||
${'tree-view-toggle'} | ${true}
|
||||
`(
|
||||
'calls setRenderTreeListMock with `$renderTreeList` when clicking $toggle clicked',
|
||||
'calls setRenderTreeList with `$renderTreeList` when clicking $toggle clicked',
|
||||
({ toggle, renderTreeList }) => {
|
||||
createComponent();
|
||||
|
||||
wrapper.findByTestId(toggle).vm.$emit('click');
|
||||
|
||||
expect(setRenderTreeListMock).toHaveBeenCalledWith(expect.anything(), {
|
||||
expect(useLegacyDiffs().setRenderTreeList).toHaveBeenCalledWith({
|
||||
renderTreeList,
|
||||
});
|
||||
},
|
||||
|
|
@ -415,7 +385,7 @@ describe('Diffs tree list component', () => {
|
|||
`(
|
||||
'sets $selectedToggle as selected when renderTreeList is $renderTreeList',
|
||||
({ selectedToggle, deselectedToggle, renderTreeList }) => {
|
||||
store.state.diffs.renderTreeList = renderTreeList;
|
||||
useLegacyDiffs().renderTreeList = renderTreeList;
|
||||
|
||||
createComponent();
|
||||
|
||||
|
|
@ -427,10 +397,12 @@ describe('Diffs tree list component', () => {
|
|||
|
||||
describe('loading state', () => {
|
||||
const getLoadedFiles = (offset = 1) =>
|
||||
store.state.diffs.tree.slice(offset).reduce((acc, el) => {
|
||||
acc[el.fileHash] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
useLegacyDiffs()
|
||||
.tree.slice(offset)
|
||||
.reduce((acc, el) => {
|
||||
acc[el.fileHash] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
beforeEach(() => {
|
||||
setupFilesInState();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser';
|
|||
import { StreamingError } from '~/rapid_diffs/streaming_error';
|
||||
import { useDiffsView } from '~/rapid_diffs/stores/diffs_view';
|
||||
|
||||
jest.mock('~/lib/graphql');
|
||||
jest.mock('~/awards_handler');
|
||||
jest.mock('~/mr_notes/stores');
|
||||
jest.mock('~/rapid_diffs/app/view_settings');
|
||||
jest.mock('~/rapid_diffs/app/init_hidden_files_warning');
|
||||
|
|
|
|||
|
|
@ -183,4 +183,12 @@ describe('Repository last commit component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('polling', () => {
|
||||
it('polls for last commit and pipeline data', () => {
|
||||
createComponent();
|
||||
|
||||
expect(LastCommit.apollo.commit.pollInterval).toBe(30000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -327,38 +327,30 @@ export const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signatu
|
|||
id: 'gid://gitlab/Project/6',
|
||||
repository: {
|
||||
__typename: 'Repository',
|
||||
paginatedTree: {
|
||||
__typename: 'TreeConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'Tree',
|
||||
lastCommit: {
|
||||
__typename: 'Commit',
|
||||
id: 'gid://gitlab/CommitPresenter/123456789',
|
||||
sha: '123456789',
|
||||
title: 'Commit title',
|
||||
titleHtml: 'Commit title',
|
||||
descriptionHtml: '',
|
||||
message: '',
|
||||
webPath: '/commit/123',
|
||||
authoredDate: '2019-01-01',
|
||||
authorName: 'Test',
|
||||
authorGravatar: 'https://test.com',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Test',
|
||||
avatarUrl: 'https://test.com',
|
||||
webPath: '/test',
|
||||
},
|
||||
signature,
|
||||
pipelines: {
|
||||
__typename: 'PipelineConnection',
|
||||
edges: pipelineEdges,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
lastCommit: {
|
||||
__typename: 'Commit',
|
||||
id: 'gid://gitlab/CommitPresenter/123456789',
|
||||
sha: '123456789',
|
||||
title: 'Commit title',
|
||||
titleHtml: 'Commit title',
|
||||
descriptionHtml: '',
|
||||
message: '',
|
||||
webPath: '/commit/123',
|
||||
authoredDate: '2019-01-01',
|
||||
authorName: 'Test',
|
||||
authorGravatar: 'https://test.com',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Test',
|
||||
avatarUrl: 'https://test.com',
|
||||
webPath: '/test',
|
||||
},
|
||||
signature,
|
||||
pipelines: {
|
||||
__typename: 'PipelineConnection',
|
||||
edges: pipelineEdges,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,24 +24,22 @@ describe('Work Item Activity/Discussions Filtering', () => {
|
|||
const createComponent = ({
|
||||
loading = false,
|
||||
workItemType = 'Task',
|
||||
sortFilterProp = ASC,
|
||||
sortFilter = ASC,
|
||||
items = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
|
||||
trackingLabel = 'item_track_notes_sorting',
|
||||
trackingAction = 'work_item_notes_sort_order_changed',
|
||||
filterEvent = 'changeSort',
|
||||
defaultSortFilterProp = ASC,
|
||||
defaultSortFilter = ASC,
|
||||
storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(WorkItemActivitySortFilter, {
|
||||
propsData: {
|
||||
loading,
|
||||
workItemType,
|
||||
sortFilterProp,
|
||||
sortFilter,
|
||||
items,
|
||||
trackingLabel,
|
||||
trackingAction,
|
||||
filterEvent,
|
||||
defaultSortFilterProp,
|
||||
defaultSortFilter,
|
||||
storageKey,
|
||||
},
|
||||
});
|
||||
|
|
@ -85,10 +83,10 @@ describe('Work Item Activity/Discussions Filtering', () => {
|
|||
expect(findLocalStorageSync().props('storageKey')).toBe(storageKey);
|
||||
});
|
||||
|
||||
it(`emits ${filterEvent} event when local storage input is emitted`, () => {
|
||||
it(`emits "select" event when local storage input is emitted`, () => {
|
||||
findLocalStorageSync().vm.$emit('input', newInputOption);
|
||||
|
||||
expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]);
|
||||
expect(wrapper.emitted('select')).toEqual([[newInputOption]]);
|
||||
});
|
||||
|
||||
it('emits tracking event when the a non default dropdown item is clicked', () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ Vue.use(VueApollo);
|
|||
|
||||
describe('Work Item Note Actions', () => {
|
||||
let wrapper;
|
||||
const noteId = '1';
|
||||
const showSpy = jest.fn();
|
||||
|
||||
const findReplyButton = () => wrapper.findComponent(ReplyButton);
|
||||
|
|
@ -54,7 +53,6 @@ describe('Work Item Note Actions', () => {
|
|||
showEdit,
|
||||
workItemIid: '1',
|
||||
note: {},
|
||||
noteId,
|
||||
showAwardEmoji,
|
||||
showAssignUnassign,
|
||||
canReportAbuse,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ describe('Work Item Note Awards List', () => {
|
|||
fullPath,
|
||||
workItemIid,
|
||||
note,
|
||||
isModal: false,
|
||||
},
|
||||
apolloProvider,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('WorkItemNotesActivityHeader component', () => {
|
|||
it('emits `changeFilter` when filtering discussions', () => {
|
||||
createComponent();
|
||||
|
||||
findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
|
||||
findActivityFilterDropdown().vm.$emit('select', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
|
||||
|
||||
expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
|
||||
});
|
||||
|
|
@ -68,7 +68,7 @@ describe('WorkItemNotesActivityHeader component', () => {
|
|||
it('emits `changeSort` when sorting discussions/activity', () => {
|
||||
createComponent();
|
||||
|
||||
findActivitySortDropdown().vm.$emit('changeSort', ASC);
|
||||
findActivitySortDropdown().vm.$emit('select', ASC);
|
||||
|
||||
expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -194,7 +194,6 @@ describe('WorkItemDetail component', () => {
|
|||
hasSubepicsFeature,
|
||||
fullPath: 'group/project',
|
||||
groupPath: 'group',
|
||||
reportAbusePath: '/report/abuse/path',
|
||||
hasLinkedItemsEpicsFeature,
|
||||
},
|
||||
stubs: {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@ describe('WorkItemNotes component', () => {
|
|||
workItemId,
|
||||
workItemIid,
|
||||
workItemType: 'task',
|
||||
reportAbusePath: '/report/abuse/path',
|
||||
isDrawer,
|
||||
isModal,
|
||||
isWorkItemConfidential,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
|
|||
let(:scope) { nil }
|
||||
let(:project_path) { nil }
|
||||
let(:verification_level) { nil }
|
||||
let(:topics) { [] }
|
||||
|
||||
let(:args) do
|
||||
{
|
||||
|
|
@ -35,7 +36,8 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
|
|||
sort: sort,
|
||||
search: search,
|
||||
scope: scope,
|
||||
verification_level: verification_level
|
||||
verification_level: verification_level,
|
||||
topics: topics
|
||||
}.compact
|
||||
end
|
||||
|
||||
|
|
@ -107,6 +109,95 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
|
|||
expect(result.items.pluck(:name)).to contain_exactly('public')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with topics argument' do
|
||||
let_it_be(:topic_ruby) { create(:topic, name: 'ruby') }
|
||||
let_it_be(:topic_rails) { create(:topic, name: 'rails') }
|
||||
let_it_be(:topic_gitlab) { create(:topic, name: 'gitlab') }
|
||||
|
||||
before_all do
|
||||
create(:project_topic, project: public_namespace_project, topic: topic_ruby)
|
||||
create(:project_topic, project: public_namespace_project, topic: topic_rails)
|
||||
create(:project_topic, project: internal_project, topic: topic_gitlab)
|
||||
end
|
||||
|
||||
context 'when filtering by multiple topics' do
|
||||
let(:topics) { %w[ruby gitlab] }
|
||||
|
||||
it 'returns resources with projects matching any of the given topic names' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to eq(%w[public internal])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by a single topic' do
|
||||
let(:topics) { %w[ruby] }
|
||||
|
||||
it 'returns resources with projects matching the topic name' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to contain_exactly('public')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when combining with other filters' do
|
||||
context 'with search' do
|
||||
let(:topics) { %w[ruby] }
|
||||
let(:search) { 'Test' }
|
||||
|
||||
it 'returns resources matching both filters' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to contain_exactly('public')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with verification level' do
|
||||
let(:topics) { %w[ruby] }
|
||||
let(:verification_level) { :gitlab_maintained }
|
||||
|
||||
it 'returns resources matching both filters' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to contain_exactly('public')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with scope' do
|
||||
let(:topics) { %w[ruby] }
|
||||
let(:scope) { 'NAMESPACES' }
|
||||
|
||||
it 'returns resources matching both filters' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to contain_exactly('public')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sort' do
|
||||
let(:topics) { %w[ruby gitlab] }
|
||||
let(:sort) { 'NAME_DESC' }
|
||||
|
||||
it 'returns filtered resources in sorted order' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to eq(%w[public internal])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by non-existent topics' do
|
||||
let(:topics) { %w[nonexistent] }
|
||||
|
||||
it 'returns no resources' do
|
||||
expect(result.items).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when topics argument is empty' do
|
||||
let(:topics) { [] }
|
||||
|
||||
it 'returns all visible resources' do
|
||||
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
|
||||
expect(ordered_names).to contain_exactly('public', 'internal', 'z private test')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is anonymous' do
|
||||
|
|
|
|||
|
|
@ -136,5 +136,104 @@ RSpec.describe Resolvers::NestedGroupsResolver, feature_category: :groups_and_pr
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting results' do
|
||||
let_it_be(:parent_group) { create(:group, name: 'Parent Group') }
|
||||
let_it_be(:group_1) { create(:group, parent: parent_group, name: 'C-group_1', path: 'a-group_1') }
|
||||
let_it_be(:group_2) { create(:group, parent: parent_group, name: 'B-group_2', path: 'c-group_2') }
|
||||
let_it_be(:group_3) { create(:group, parent: parent_group, name: 'A-group_3', path: 'b-group_3') }
|
||||
|
||||
subject { resolve(described_class, obj: parent_group, args: params, ctx: { current_user: user }) }
|
||||
|
||||
context 'when sorting by name' do
|
||||
context 'in ascending order' do
|
||||
let(:params) { { sort: 'NAME_ASC' } }
|
||||
|
||||
it 'sorts the groups by name in ascending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
group_3,
|
||||
group_2,
|
||||
group_1
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in descending order' do
|
||||
let(:params) { { sort: 'NAME_DESC' } }
|
||||
|
||||
it 'sorts the groups by name in descending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
group_1,
|
||||
group_2,
|
||||
group_3
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting by path' do
|
||||
context 'in ascending order' do
|
||||
let(:params) { { sort: 'PATH_ASC' } }
|
||||
|
||||
it 'sorts the groups by path in ascending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
group_1,
|
||||
group_3,
|
||||
group_2
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in descending order' do
|
||||
let(:params) { { sort: 'PATH_DESC' } }
|
||||
|
||||
it 'sorts the groups by path in descending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
group_2,
|
||||
group_3,
|
||||
group_1
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting by ID' do
|
||||
context 'in ascending order' do
|
||||
let(:params) { { sort: 'ID_ASC' } }
|
||||
|
||||
it 'sorts the groups by ID in ascending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
group_1,
|
||||
group_2,
|
||||
group_3
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in descending order' do
|
||||
let(:params) { { sort: 'ID_DESC' } }
|
||||
|
||||
it 'sorts the groups by ID in descending order' do
|
||||
is_expected.to eq(
|
||||
[
|
||||
group_3,
|
||||
group_2,
|
||||
group_1
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['GroupSort'], feature_category: :groups_and_projects do
|
||||
specify { expect(described_class.graphql_name).to eq('GroupSort') }
|
||||
|
||||
it 'exposes all the existing sort values' do
|
||||
expect(described_class.values.keys).to include(
|
||||
*%w[
|
||||
SIMILARITY
|
||||
NAME_ASC
|
||||
NAME_DESC
|
||||
PATH_ASC
|
||||
PATH_DESC
|
||||
ID_ASC
|
||||
ID_DESC
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1151,6 +1151,14 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
|
|||
allow_any_instance_of(described_class).to receive(:access_token).and_return(oauth_access_token)
|
||||
end
|
||||
|
||||
it 'includes the OAuth application ID in the token info' do
|
||||
validate_and_save_access_token!(reset_token: true)
|
||||
|
||||
expect(::Current.token_info).to match(a_hash_including({
|
||||
token_application_id: oauth_access_token.application_id
|
||||
}))
|
||||
end
|
||||
|
||||
context 'when reset_token is true' do
|
||||
it 'reloads the access token before validation' do
|
||||
expect(oauth_access_token).to receive(:reload)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedRoutes, feature_category: :groups_and_projects do
|
||||
let(:organizations) { table(:organizations) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:routes) { table(:routes) }
|
||||
let(:organization) { organizations.create!(name: 'Foobar', path: 'path1') }
|
||||
let!(:namespace) do
|
||||
namespaces.create!(name: 'Group', type: 'Group', path: 'group', organization_id: organization.id)
|
||||
end
|
||||
|
||||
subject(:background_migration) do
|
||||
described_class.new(
|
||||
start_id: routes.minimum(:id),
|
||||
end_id: routes.maximum(:id),
|
||||
batch_table: :routes,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 1,
|
||||
pause_ms: 0,
|
||||
connection: ApplicationRecord.connection
|
||||
).perform
|
||||
end
|
||||
|
||||
before do
|
||||
# Remove constraint so we can create invalid records
|
||||
ApplicationRecord.connection.execute("ALTER TABLE routes DROP CONSTRAINT fk_679ff8213d;")
|
||||
|
||||
routes.create!(path: 'route1', source_id: namespace.id, source_type: 'Namespace', namespace_id: namespace.id)
|
||||
routes.create!(
|
||||
path: 'orphaned_route', source_id: non_existing_record_id, source_type: 'Namespace',
|
||||
namespace_id: non_existing_record_id
|
||||
)
|
||||
end
|
||||
|
||||
after do
|
||||
# Re-create constraint after the test
|
||||
ApplicationRecord.connection.execute(<<~SQL)
|
||||
ALTER TABLE ONLY routes
|
||||
ADD CONSTRAINT fk_679ff8213d FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE NOT VALID;
|
||||
SQL
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'deletes the orphaned routes' do
|
||||
expect { background_migration }.to change { routes.count }.from(2).to(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -28,5 +28,23 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::TokenLogger do
|
|||
expect(subject).to eq({ token_id: 1, token_type: "PersonalAccessToken" })
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when token is available with an OAuth application ID' do
|
||||
let(:token_type) { "OAuthAccessToken" }
|
||||
let(:token_application_id) { 1000 }
|
||||
|
||||
before do
|
||||
::Current.token_info = {
|
||||
token_id: token_id,
|
||||
token_type: token_type,
|
||||
token_scopes: [:api],
|
||||
token_application_id: token_application_id
|
||||
}
|
||||
end
|
||||
|
||||
it 'adds the token information with OAuth application ID to log parameters' do
|
||||
expect(subject).to eq({ token_id: 1, token_type: "OAuthAccessToken", token_application_id: 1000 })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue