Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fe2f83b699
commit
addf13e0d0
|
|
@ -1,45 +1,4 @@
|
||||||
.review-docs:
|
.review-docs:
|
||||||
extends:
|
|
||||||
- .default-retry
|
|
||||||
- .docs:rules:review-docs
|
|
||||||
image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine
|
|
||||||
stage: review
|
|
||||||
needs: []
|
|
||||||
variables:
|
|
||||||
# We're cloning the repo instead of downloading the script for now
|
|
||||||
# because some repos are private and CI_JOB_TOKEN cannot access files.
|
|
||||||
# See https://gitlab.com/gitlab-org/gitlab/issues/191273
|
|
||||||
GIT_DEPTH: 1
|
|
||||||
# By default, deploy the Review App using the `main` branch of the `gitlab-org/gitlab-docs` project
|
|
||||||
DOCS_BRANCH: main
|
|
||||||
environment:
|
|
||||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
|
|
||||||
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
|
|
||||||
# Discussion: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14236/diffs#note_40140693
|
|
||||||
auto_stop_in: 2 weeks
|
|
||||||
url: http://${DOCS_BRANCH}-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}.${DOCS_REVIEW_APPS_DOMAIN}/${DOCS_GITLAB_REPO_SUFFIX}
|
|
||||||
on_stop: review-docs-cleanup
|
|
||||||
before_script:
|
|
||||||
- source ./scripts/utils.sh
|
|
||||||
- install_gitlab_gem
|
|
||||||
|
|
||||||
# Always trigger a docs build in gitlab-docs only on docs-only branches.
|
|
||||||
# Useful to preview the docs changes live.
|
|
||||||
review-docs-deploy:
|
|
||||||
extends: .review-docs
|
|
||||||
script:
|
|
||||||
- ./scripts/trigger-build.rb docs deploy
|
|
||||||
|
|
||||||
# Cleanup remote environment of gitlab-docs
|
|
||||||
review-docs-cleanup:
|
|
||||||
extends: .review-docs
|
|
||||||
environment:
|
|
||||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
|
|
||||||
action: stop
|
|
||||||
script:
|
|
||||||
- ./scripts/trigger-build.rb docs cleanup
|
|
||||||
|
|
||||||
.review-docs-hugo:
|
|
||||||
extends:
|
extends:
|
||||||
- .default-retry
|
- .default-retry
|
||||||
- .docs:rules:review-docs
|
- .docs:rules:review-docs
|
||||||
|
|
@ -51,25 +10,25 @@ review-docs-cleanup:
|
||||||
# By default, deploy the Review App using the `main` branch of the `gitlab-org/technical-writing/docs-gitlab-com` project
|
# By default, deploy the Review App using the `main` branch of the `gitlab-org/technical-writing/docs-gitlab-com` project
|
||||||
DOCS_BRANCH: main
|
DOCS_BRANCH: main
|
||||||
environment:
|
environment:
|
||||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
|
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
|
||||||
auto_stop_in: 2 weeks
|
auto_stop_in: 2 weeks
|
||||||
url: https://new.docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
|
url: https://docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
|
||||||
on_stop: review-docs-hugo-cleanup
|
on_stop: review-docs-cleanup
|
||||||
before_script:
|
before_script:
|
||||||
- source ./scripts/utils.sh
|
- source ./scripts/utils.sh
|
||||||
- install_gitlab_gem
|
- install_gitlab_gem
|
||||||
|
|
||||||
# Deploy documentation review app by using GitLab Docs Hugo project (gitlab-org/technical-writing/docs-gitlab-com)
|
# Deploy documentation review app by using GitLab Docs project (gitlab-org/technical-writing/docs-gitlab-com)
|
||||||
review-docs-hugo-deploy:
|
review-docs-deploy:
|
||||||
extends: .review-docs-hugo
|
extends: .review-docs
|
||||||
script:
|
script:
|
||||||
- ./scripts/trigger-build.rb docs-hugo deploy
|
- ./scripts/trigger-build.rb docs-hugo deploy
|
||||||
|
|
||||||
# Cleanup remote environment of gitlab-org/technical-writing/docs-gitlab-com
|
# Cleanup remote environment of gitlab-org/technical-writing/docs-gitlab-com
|
||||||
review-docs-hugo-cleanup:
|
review-docs-cleanup:
|
||||||
extends: .review-docs-hugo
|
extends: .review-docs
|
||||||
environment:
|
environment:
|
||||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
|
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
|
||||||
action: stop
|
action: stop
|
||||||
script:
|
script:
|
||||||
- ./scripts/trigger-build.rb docs-hugo cleanup
|
- ./scripts/trigger-build.rb docs-hugo cleanup
|
||||||
|
|
|
||||||
|
|
@ -1125,12 +1125,9 @@ Gitlab/BoundedContexts:
|
||||||
- 'app/models/preloaders/commit_status_preloader.rb'
|
- 'app/models/preloaders/commit_status_preloader.rb'
|
||||||
- 'app/models/preloaders/environments/deployment_preloader.rb'
|
- 'app/models/preloaders/environments/deployment_preloader.rb'
|
||||||
- 'app/models/preloaders/group_policy_preloader.rb'
|
- 'app/models/preloaders/group_policy_preloader.rb'
|
||||||
- 'app/models/preloaders/group_root_ancestor_preloader.rb'
|
|
||||||
- 'app/models/preloaders/labels_preloader.rb'
|
- 'app/models/preloaders/labels_preloader.rb'
|
||||||
- 'app/models/preloaders/merge_request_diff_preloader.rb'
|
- 'app/models/preloaders/merge_request_diff_preloader.rb'
|
||||||
- 'app/models/preloaders/namespace_root_ancestor_preloader.rb'
|
|
||||||
- 'app/models/preloaders/project_policy_preloader.rb'
|
- 'app/models/preloaders/project_policy_preloader.rb'
|
||||||
- 'app/models/preloaders/project_root_ancestor_preloader.rb'
|
|
||||||
- 'app/models/preloaders/projects/notes_preloader.rb'
|
- 'app/models/preloaders/projects/notes_preloader.rb'
|
||||||
- 'app/models/preloaders/runner_manager_policy_preloader.rb'
|
- 'app/models/preloaders/runner_manager_policy_preloader.rb'
|
||||||
- 'app/models/preloaders/user_max_access_level_in_groups_preloader.rb'
|
- 'app/models/preloaders/user_max_access_level_in_groups_preloader.rb'
|
||||||
|
|
|
||||||
|
|
@ -3475,7 +3475,6 @@ RSpec/FeatureCategory:
|
||||||
- 'spec/presenters/blobs/notebook_presenter_spec.rb'
|
- 'spec/presenters/blobs/notebook_presenter_spec.rb'
|
||||||
- 'spec/presenters/blobs/unfold_presenter_spec.rb'
|
- 'spec/presenters/blobs/unfold_presenter_spec.rb'
|
||||||
- 'spec/presenters/ci/bridge_presenter_spec.rb'
|
- 'spec/presenters/ci/bridge_presenter_spec.rb'
|
||||||
- 'spec/presenters/ci/build_presenter_spec.rb'
|
|
||||||
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
|
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
|
||||||
- 'spec/presenters/ci/group_variable_presenter_spec.rb'
|
- 'spec/presenters/ci/group_variable_presenter_spec.rb'
|
||||||
- 'spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb'
|
- 'spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb'
|
||||||
|
|
@ -3678,7 +3677,6 @@ RSpec/FeatureCategory:
|
||||||
- 'spec/serializers/blob_entity_spec.rb'
|
- 'spec/serializers/blob_entity_spec.rb'
|
||||||
- 'spec/serializers/build_action_entity_spec.rb'
|
- 'spec/serializers/build_action_entity_spec.rb'
|
||||||
- 'spec/serializers/build_artifact_entity_spec.rb'
|
- 'spec/serializers/build_artifact_entity_spec.rb'
|
||||||
- 'spec/serializers/build_details_entity_spec.rb'
|
|
||||||
- 'spec/serializers/build_trace_entity_spec.rb'
|
- 'spec/serializers/build_trace_entity_spec.rb'
|
||||||
- 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb'
|
- 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb'
|
||||||
- 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb'
|
- 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb'
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import DomElementListener from '~/vue_shared/components/dom_element_listener.vue
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
||||||
import UserDate from '~/vue_shared/components/user_date.vue';
|
import UserDate from '~/vue_shared/components/user_date.vue';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
||||||
import { createAlert, VARIANT_DANGER } from '~/alert';
|
import { createAlert, VARIANT_DANGER } from '~/alert';
|
||||||
import { EVENT_SUCCESS, FIELDS, FORM_SELECTOR, INITIAL_PAGE, PAGE_SIZE } from './constants';
|
import { EVENT_SUCCESS, FIELDS, FORM_SELECTOR, INITIAL_PAGE, PAGE_SIZE } from './constants';
|
||||||
|
|
||||||
|
|
@ -41,7 +40,6 @@ export default {
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagsMixin()],
|
|
||||||
lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
|
lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
|
||||||
anchor: 'view-token-usage-information',
|
anchor: 'view-token-usage-information',
|
||||||
}),
|
}),
|
||||||
|
|
@ -117,10 +115,6 @@ export default {
|
||||||
ignoredFields.push('role');
|
ignoredFields.push('role');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.glFeatures.patIp) {
|
|
||||||
ignoredFields.push('lastUsedIps');
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = FIELDS.filter(({ key }) => !ignoredFields.includes(key));
|
const fields = FIELDS.filter(({ key }) => !ignoredFields.includes(key));
|
||||||
|
|
||||||
// Remove the sortability of the columns if backend pagination is on.
|
// Remove the sortability of the columns if backend pagination is on.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
<script>
|
||||||
|
import { GlButton, GlFormGroup, GlFormInput, GlAnimatedUploadIcon } from '@gitlab/ui';
|
||||||
|
import { kebabCase } from 'lodash';
|
||||||
|
import validation from '~/vue_shared/directives/validation';
|
||||||
|
import csrf from '~/lib/utils/csrf';
|
||||||
|
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||||
|
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||||
|
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
|
||||||
|
import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
|
||||||
|
import NewProjectDestinationSelect from '~/projects/new_v2/components/project_destination_select.vue';
|
||||||
|
|
||||||
|
const feedbackMap = {
|
||||||
|
valueMissing: {
|
||||||
|
isInvalid: (el) => el.validity?.valueMissing,
|
||||||
|
},
|
||||||
|
nameStartPattern: {
|
||||||
|
isInvalid: (el) => el.validity?.patternMismatch && !START_RULE.reg.test(el.value),
|
||||||
|
message: START_RULE.msg,
|
||||||
|
},
|
||||||
|
nameContainsPattern: {
|
||||||
|
isInvalid: (el) => el.validity?.patternMismatch && !CONTAINS_RULE.reg.test(el.value),
|
||||||
|
message: CONTAINS_RULE.msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initFormField = ({ value = null, required = true } = {}) => ({
|
||||||
|
value,
|
||||||
|
required,
|
||||||
|
state: null,
|
||||||
|
feedback: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlButton,
|
||||||
|
GlFormGroup,
|
||||||
|
GlFormInput,
|
||||||
|
GlAnimatedUploadIcon,
|
||||||
|
FileIcon,
|
||||||
|
MultiStepFormTemplate,
|
||||||
|
NewProjectDestinationSelect,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
validation: validation(feedbackMap),
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
backButtonPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
namespaceFullPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
namespaceId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
rootPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
importGitlabProjectPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const form = {
|
||||||
|
state: false,
|
||||||
|
showValidation: false,
|
||||||
|
fields: {
|
||||||
|
name: initFormField(),
|
||||||
|
path: initFormField(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
file: null,
|
||||||
|
filePreviewURL: null,
|
||||||
|
form,
|
||||||
|
animateUploadIcon: false,
|
||||||
|
dropzoneState: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formattedFileSize() {
|
||||||
|
return numberToHumanSize(this.file.size);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
'form.fields.name.value': function (newVal) {
|
||||||
|
this.form.fields.path.value = kebabCase(newVal);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setFile() {
|
||||||
|
this.file = this.$refs.fileUpload.files['0'];
|
||||||
|
|
||||||
|
const fileUrlReader = new FileReader();
|
||||||
|
|
||||||
|
fileUrlReader.readAsDataURL(this.file);
|
||||||
|
|
||||||
|
fileUrlReader.onload = (e) => {
|
||||||
|
this.filePreviewURL = e.target?.result;
|
||||||
|
};
|
||||||
|
this.dropzoneState = true;
|
||||||
|
},
|
||||||
|
onDropzoneMouseEnter() {
|
||||||
|
this.animateUploadIcon = true;
|
||||||
|
},
|
||||||
|
onDropzoneMouseLeave() {
|
||||||
|
this.animateUploadIcon = false;
|
||||||
|
},
|
||||||
|
openFileUpload() {
|
||||||
|
this.$refs.fileUpload.click();
|
||||||
|
},
|
||||||
|
onSubmit() {
|
||||||
|
if (!this.form.state) {
|
||||||
|
this.form.showValidation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.file === null) {
|
||||||
|
this.dropzoneState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.form.state || !this.dropzoneState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.form.submit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
csrf,
|
||||||
|
projectNamePattern: `(${START_RULE.reg.source})|(${CONTAINS_RULE.reg.source})`,
|
||||||
|
validFileMimetypes: ['application/gzip'],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
ref="form"
|
||||||
|
:action="importGitlabProjectPath"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
method="post"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
|
||||||
|
<multi-step-form-template
|
||||||
|
:title="__('Import an exported GitLab project')"
|
||||||
|
:current-step="3"
|
||||||
|
:steps-total="3"
|
||||||
|
>
|
||||||
|
<template #form>
|
||||||
|
<gl-form-group
|
||||||
|
:label="__('Project name')"
|
||||||
|
label-for="name"
|
||||||
|
:description="
|
||||||
|
s__(
|
||||||
|
'ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces.',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:invalid-feedback="form.fields.name.feedback"
|
||||||
|
data-testid="project-name-form-group"
|
||||||
|
>
|
||||||
|
<gl-form-input
|
||||||
|
id="name"
|
||||||
|
v-model="form.fields.name.value"
|
||||||
|
v-validation:[form.showValidation]
|
||||||
|
:validation-message="s__('ProjectsNew|Please enter a valid project name.')"
|
||||||
|
:state="form.fields.name.state"
|
||||||
|
:pattern="$options.projectNamePattern"
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
:placeholder="s__('ProjectsNew|My awesome project')"
|
||||||
|
data-testid="project-name"
|
||||||
|
/>
|
||||||
|
</gl-form-group>
|
||||||
|
|
||||||
|
<div class="gl-flex gl-flex-col gl-gap-4 sm:gl-flex-row">
|
||||||
|
<gl-form-group
|
||||||
|
:label="s__('ProjectsNew|Choose a group')"
|
||||||
|
class="sm:gl-w-1/2"
|
||||||
|
label-for="namespace"
|
||||||
|
>
|
||||||
|
<new-project-destination-select
|
||||||
|
toggle-aria-labelled-by="namespace"
|
||||||
|
:namespace-full-path="namespaceFullPath"
|
||||||
|
:namespace-id="namespaceId"
|
||||||
|
:root-url="rootPath"
|
||||||
|
/>
|
||||||
|
</gl-form-group>
|
||||||
|
|
||||||
|
<div class="gl-mt-2 gl-hidden gl-pt-6 sm:gl-block">{{ __('/') }}</div>
|
||||||
|
|
||||||
|
<gl-form-group
|
||||||
|
:label="s__('ProjectsNew|Project slug')"
|
||||||
|
label-for="path"
|
||||||
|
class="sm:gl-w-1/2"
|
||||||
|
:invalid-feedback="form.fields.path.feedback"
|
||||||
|
>
|
||||||
|
<gl-form-input
|
||||||
|
id="path"
|
||||||
|
v-model="form.fields.path.value"
|
||||||
|
v-validation:[form.showValidation]
|
||||||
|
:validation-message="s__('ProjectsNew|Please enter a valid project slug.')"
|
||||||
|
:state="form.fields.path.state"
|
||||||
|
name="path"
|
||||||
|
required
|
||||||
|
:placeholder="s__('ProjectsNew|my-awesome-project')"
|
||||||
|
data-testid="project-slug"
|
||||||
|
/>
|
||||||
|
</gl-form-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="-gl-mt-3 gl-text-base gl-leading-normal gl-text-subtle">
|
||||||
|
{{
|
||||||
|
s__(
|
||||||
|
"ProjectsNew|To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.",
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<gl-form-group
|
||||||
|
:label="s__('ProjectsNew|GitLab project export')"
|
||||||
|
label-for="file-button"
|
||||||
|
:invalid-feedback="s__('ProjectsNew|Please upload a valid GitLab project export file.')"
|
||||||
|
:state="dropzoneState"
|
||||||
|
data-testid="project-file-form-group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="file-button"
|
||||||
|
class="upload-dropzone-card upload-dropzone-border gl-mb-0 gl-h-full gl-w-full gl-items-center gl-justify-center gl-bg-default gl-px-5 gl-py-4"
|
||||||
|
type="button"
|
||||||
|
data-testid="dropzone-button"
|
||||||
|
@click="openFileUpload"
|
||||||
|
@mouseenter="onDropzoneMouseEnter"
|
||||||
|
@mouseleave="onDropzoneMouseLeave"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="file"
|
||||||
|
class="gl-flex gl-w-full gl-flex-col gl-items-center gl-justify-center gl-gap-3"
|
||||||
|
>
|
||||||
|
<file-icon :file-name="file.name" :size="24" class="gl-flex" />
|
||||||
|
<span>
|
||||||
|
{{ file.name }}
|
||||||
|
·
|
||||||
|
<span class="gl-text-subtle">{{ formattedFileSize }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="gl-flex gl-items-center gl-justify-center gl-gap-3 gl-text-center">
|
||||||
|
<gl-animated-upload-icon :is-on="animateUploadIcon" />
|
||||||
|
<span>{{ __('Drop or upload file to attach') }}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="fileUpload"
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
hidden
|
||||||
|
:accept="$options.validFileMimetypes"
|
||||||
|
required
|
||||||
|
:multiple="false"
|
||||||
|
data-testid="dropzone-input"
|
||||||
|
@change="setFile"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</gl-form-group>
|
||||||
|
</template>
|
||||||
|
<template #back>
|
||||||
|
<gl-button
|
||||||
|
category="primary"
|
||||||
|
variant="default"
|
||||||
|
:href="backButtonPath"
|
||||||
|
data-testid="back-button"
|
||||||
|
>
|
||||||
|
{{ __('Go back') }}
|
||||||
|
</gl-button>
|
||||||
|
</template>
|
||||||
|
<template #next>
|
||||||
|
<gl-button
|
||||||
|
type="submit"
|
||||||
|
category="primary"
|
||||||
|
variant="confirm"
|
||||||
|
data-testid="next-button"
|
||||||
|
@click.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
{{ __('Import project') }}
|
||||||
|
</gl-button>
|
||||||
|
</template>
|
||||||
|
</multi-step-form-template>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import createDefaultClient from '~/lib/graphql';
|
||||||
|
import importFromGitlabExportApp from './import_from_gitlab_export_app.vue';
|
||||||
|
|
||||||
|
export function initGitLabImportProjectForm() {
|
||||||
|
const el = document.getElementById('js-import-gitlab-project-root');
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
backButtonPath,
|
||||||
|
namespaceFullPath,
|
||||||
|
namespaceId,
|
||||||
|
rootPath,
|
||||||
|
importGitlabProjectPath,
|
||||||
|
userNamespaceId,
|
||||||
|
canCreateProject,
|
||||||
|
rootUrl,
|
||||||
|
} = el.dataset;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
backButtonPath,
|
||||||
|
namespaceFullPath,
|
||||||
|
namespaceId,
|
||||||
|
rootPath,
|
||||||
|
importGitlabProjectPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const provide = {
|
||||||
|
userNamespaceId,
|
||||||
|
canCreateProject,
|
||||||
|
rootUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
name: 'ImportGitLabProjectRoot',
|
||||||
|
apolloProvider: new VueApollo({
|
||||||
|
defaultClient: createDefaultClient(),
|
||||||
|
}),
|
||||||
|
provide,
|
||||||
|
render(h) {
|
||||||
|
return h(importFromGitlabExportApp, { props });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import initGitLabImportProject from '~/projects/project_import_gitlab_project';
|
import initGitLabImportProject from '~/projects/project_import_gitlab_project';
|
||||||
import { initNewProjectUrlSelect } from '~/projects/new';
|
import { initNewProjectUrlSelect } from '~/projects/new';
|
||||||
|
import { initGitLabImportProjectForm } from '~/import/gitlab_project';
|
||||||
|
|
||||||
initNewProjectUrlSelect();
|
initNewProjectUrlSelect();
|
||||||
initGitLabImportProject();
|
initGitLabImportProject();
|
||||||
|
initGitLabImportProjectForm();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlTruncate, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
|
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||||
import { PATH_SEPARATOR } from '~/lib/utils/url_utility';
|
import { joinPaths, PATH_SEPARATOR } from '~/lib/utils/url_utility';
|
||||||
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
|
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
|
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
import { __, s__, n__ } from '~/locale';
|
import { __, s__, n__ } from '~/locale';
|
||||||
import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
|
import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlButton,
|
|
||||||
GlTruncate,
|
|
||||||
GlCollapsibleListbox,
|
GlCollapsibleListbox,
|
||||||
GlIcon,
|
|
||||||
},
|
},
|
||||||
mixins: [Tracking.mixin()],
|
mixins: [Tracking.mixin()],
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|
@ -49,6 +47,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
toggleAriaLabelledBy: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
groupsOnly: {
|
groupsOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -155,6 +158,19 @@ export default {
|
||||||
this.shouldSkipQuery = false;
|
this.shouldSkipQuery = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleDropdownItemClick(namespaceId) {
|
||||||
|
const namespace = this.allItems.find((item) => item.id === namespaceId);
|
||||||
|
|
||||||
|
if (namespace) {
|
||||||
|
eventHub.$emit('update-visibility', {
|
||||||
|
name: namespace.name,
|
||||||
|
visibility: namespace.visibility,
|
||||||
|
showPath: namespace.webUrl,
|
||||||
|
editPath: joinPaths(namespace.webUrl, '-', 'edit'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setNamespace(namespace);
|
||||||
|
},
|
||||||
handleSelectTemplate(id, fullPath) {
|
handleSelectTemplate(id, fullPath) {
|
||||||
this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
|
this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
|
||||||
this.setNamespace({ id, fullPath });
|
this.setNamespace({ id, fullPath });
|
||||||
|
|
@ -187,31 +203,34 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<gl-collapsible-listbox
|
<gl-collapsible-listbox
|
||||||
searchable
|
searchable
|
||||||
fluid-width
|
fluid-width
|
||||||
:searching="loading"
|
:searching="loading"
|
||||||
:items="items"
|
:items="items"
|
||||||
:toggle-text="dropdownText"
|
:toggle-text="dropdownText"
|
||||||
|
toggle-class="gl-w-full"
|
||||||
|
:toggle-aria-labelled-by="toggleAriaLabelledBy"
|
||||||
:no-results-text="$options.i18n.emptySearchResult"
|
:no-results-text="$options.i18n.emptySearchResult"
|
||||||
class="gl-w-full"
|
class="project-destination-select gl-w-full gl-max-w-full"
|
||||||
@show="trackDropdownShow"
|
@show="trackDropdownShow"
|
||||||
@shown="handleDropdownShown"
|
@shown="handleDropdownShown"
|
||||||
|
@select="handleDropdownItemClick"
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
>
|
>
|
||||||
<template #toggle>
|
|
||||||
<gl-button :class="dropdownPlaceholderClass">
|
|
||||||
<gl-truncate
|
|
||||||
:text="dropdownText"
|
|
||||||
position="start"
|
|
||||||
class="gl-mr-auto gl-overflow-hidden"
|
|
||||||
with-tooltip
|
|
||||||
/>
|
|
||||||
<gl-icon class="gl-button-icon dropdown-chevron !gl-ml-2 !gl-mr-0" name="chevron-down" />
|
|
||||||
</gl-button>
|
|
||||||
</template>
|
|
||||||
<template #search-summary-sr-only>
|
<template #search-summary-sr-only>
|
||||||
{{ searchSummary }}
|
{{ searchSummary }}
|
||||||
</template>
|
</template>
|
||||||
</gl-collapsible-listbox>
|
</gl-collapsible-listbox>
|
||||||
|
|
||||||
|
<input type="hidden" name="project[selected_namespace_id]" :value="selectedNamespace.id" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="project[namespace_id]"
|
||||||
|
type="hidden"
|
||||||
|
name="namespace_id"
|
||||||
|
:value="selectedNamespace.id || userNamespaceUniqueId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export default () => {
|
||||||
const $projectPath = document.querySelector('.js-path-name');
|
const $projectPath = document.querySelector('.js-path-name');
|
||||||
const { name, path } = prepareParameters();
|
const { name, path } = prepareParameters();
|
||||||
|
|
||||||
|
if ($projectName || $projectPath) {
|
||||||
// get the project name from the URL and set it as input value
|
// get the project name from the URL and set it as input value
|
||||||
$projectName.value = name;
|
$projectName.value = name;
|
||||||
|
|
||||||
|
|
@ -41,4 +42,5 @@ export default () => {
|
||||||
$projectPath.addEventListener('keyup', () =>
|
$projectPath.addEventListener('keyup', () =>
|
||||||
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
|
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
|
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||||
|
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
||||||
|
import { InternalEvents } from '~/tracking';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -7,16 +9,26 @@ export default {
|
||||||
GlButtonGroup,
|
GlButtonGroup,
|
||||||
GlDisclosureDropdownItem,
|
GlDisclosureDropdownItem,
|
||||||
},
|
},
|
||||||
|
mixins: [InternalEvents.mixin()],
|
||||||
props: {
|
props: {
|
||||||
ideItem: {
|
ideItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
shortcutsDisabled() {
|
||||||
|
return shouldDisableShortcuts();
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
this.$emit('close-dropdown');
|
this.$emit('close-dropdown');
|
||||||
},
|
},
|
||||||
|
trackAndClose({ action, label }) {
|
||||||
|
this.trackEvent(action, { label });
|
||||||
|
this.closeDropdown();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -41,5 +53,16 @@ export default {
|
||||||
</gl-button>
|
</gl-button>
|
||||||
</gl-button-group>
|
</gl-button-group>
|
||||||
</gl-disclosure-dropdown-item>
|
</gl-disclosure-dropdown-item>
|
||||||
<gl-disclosure-dropdown-item v-else-if="ideItem.href" :item="ideItem" @action="closeDropdown" />
|
<gl-disclosure-dropdown-item
|
||||||
|
v-else-if="ideItem.href"
|
||||||
|
:item="ideItem"
|
||||||
|
@action="trackAndClose(ideItem.tracking)"
|
||||||
|
>
|
||||||
|
<template #list-item>
|
||||||
|
<span class="gl-mb-2 gl-flex gl-items-center gl-justify-between">
|
||||||
|
<span>{{ ideItem.text }}</span>
|
||||||
|
<kbd v-if="ideItem.shortcut && !shortcutsDisabled" class="flat">{{ ideItem.shortcut }}</kbd>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</gl-disclosure-dropdown-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
|
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
|
||||||
import { getHTTPProtocol } from '~/lib/utils/url_utility';
|
import { getHTTPProtocol } from '~/lib/utils/url_utility';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
|
import { GO_TO_PROJECT_WEBIDE, keysFor } from '~/behaviors/shortcuts/keybindings';
|
||||||
import CodeDropdownCloneItem from './code_dropdown_clone_item.vue';
|
import CodeDropdownCloneItem from './code_dropdown_clone_item.vue';
|
||||||
import CodeDropdownDownloadItems from './code_dropdown_download_items.vue';
|
import CodeDropdownDownloadItems from './code_dropdown_download_items.vue';
|
||||||
import CodeDropdownIdeItem from './code_dropdown_ide_item.vue';
|
import CodeDropdownIdeItem from './code_dropdown_ide_item.vue';
|
||||||
|
|
@ -36,6 +37,16 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
webIdeUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
gitpodUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
currentPath: {
|
currentPath: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -46,6 +57,16 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
showWebIdeButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showGitpodButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
httpLabel() {
|
httpLabel() {
|
||||||
|
|
@ -58,22 +79,52 @@ export default {
|
||||||
httpUrlEncoded() {
|
httpUrlEncoded() {
|
||||||
return encodeURIComponent(this.httpUrl);
|
return encodeURIComponent(this.httpUrl);
|
||||||
},
|
},
|
||||||
|
webIdeActionShortcutKey() {
|
||||||
|
return keysFor(GO_TO_PROJECT_WEBIDE)[0];
|
||||||
|
},
|
||||||
|
webIdeAction() {
|
||||||
|
return {
|
||||||
|
text: __('Web IDE'),
|
||||||
|
shortcut: this.webIdeActionShortcutKey,
|
||||||
|
tracking: {
|
||||||
|
action: 'click_consolidated_edit',
|
||||||
|
label: 'web_ide',
|
||||||
|
},
|
||||||
|
href: this.webIdeUrl,
|
||||||
|
extraAttrs: {
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
gitPodAction() {
|
||||||
|
return {
|
||||||
|
text: __('GitPod'),
|
||||||
|
tracking: {
|
||||||
|
action: 'click_consolidated_edit',
|
||||||
|
label: 'gitpod',
|
||||||
|
},
|
||||||
|
href: this.gitpodUrl,
|
||||||
|
extraAttrs: {
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
ideGroup() {
|
ideGroup() {
|
||||||
const groups = [
|
const actions = [];
|
||||||
/* eslint-disable-next-line @gitlab/require-i18n-strings */
|
|
||||||
this.createIdeGroup('Visual Studio Code', VSCODE_BASE_URL),
|
|
||||||
this.createIdeGroup('IntelliJ IDEA', JETBRAINS_BASE_URL),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (this.xcodeUrl) {
|
if (this.showWebIdeButton) actions.push(this.webIdeAction);
|
||||||
groups.push({
|
if (this.showGitpodButton) actions.push(this.gitPodAction);
|
||||||
/* eslint-disable-next-line @gitlab/require-i18n-strings */
|
|
||||||
text: 'Xcode',
|
if (this.httpUrl || this.sshUrl) {
|
||||||
href: this.xcodeUrl,
|
actions.push(this.createIdeGroup(__('Visual Studio Code'), VSCODE_BASE_URL));
|
||||||
});
|
actions.push(this.createIdeGroup(__('IntelliJ IDEA'), JETBRAINS_BASE_URL));
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups.filter((group) => group.items?.length || group.href);
|
if (this.xcodeUrl) {
|
||||||
|
actions.push({ text: __('Xcode'), href: this.xcodeUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
},
|
},
|
||||||
sourceCodeGroup() {
|
sourceCodeGroup() {
|
||||||
return this.directoryDownloadLinks.map((link) => ({
|
return this.directoryDownloadLinks.map((link) => ({
|
||||||
|
|
@ -107,7 +158,7 @@ export default {
|
||||||
...(this.sshUrl
|
...(this.sshUrl
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
text: 'SSH',
|
text: __('SSH'),
|
||||||
href: `${baseUrl}${this.sshUrlEncoded}`,
|
href: `${baseUrl}${this.sshUrlEncoded}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -115,7 +166,7 @@ export default {
|
||||||
...(this.httpUrl
|
...(this.httpUrl
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
text: 'HTTPS',
|
text: __('HTTPS'),
|
||||||
href: `${baseUrl}${this.httpUrlEncoded}`,
|
href: `${baseUrl}${this.httpUrlEncoded}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -323,8 +323,12 @@ export default {
|
||||||
:http-url="httpUrl"
|
:http-url="httpUrl"
|
||||||
:kerberos-url="kerberosUrl"
|
:kerberos-url="kerberosUrl"
|
||||||
:xcode-url="xcodeUrl"
|
:xcode-url="xcodeUrl"
|
||||||
|
:web-ide-url="webIDEUrl"
|
||||||
|
:gitpod-url="gitpodUrl"
|
||||||
:current-path="currentPath"
|
:current-path="currentPath"
|
||||||
:directory-download-links="downloadLinks"
|
:directory-download-links="downloadLinks"
|
||||||
|
:show-web-ide-button="showWebIdeButton"
|
||||||
|
:show-gitpod-button="showGitpodButton"
|
||||||
/>
|
/>
|
||||||
<repository-overflow-menu v-if="comparePath" />
|
<repository-overflow-menu v-if="comparePath" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { s__ } from '~/locale';
|
||||||
|
|
||||||
export const BASE_IMPORT_TABLE_ROW_GRID_CLASSES = 'gl-grid-cols-[repeat(2,1fr),200px,200px]';
|
export const BASE_IMPORT_TABLE_ROW_GRID_CLASSES = 'gl-grid-cols-[repeat(2,1fr),200px,200px]';
|
||||||
|
|
||||||
|
export const SOURCE_TYPE_GROUP = 'group';
|
||||||
|
export const SOURCE_TYPE_PROJECT = 'project';
|
||||||
|
export const SOURCE_TYPE_FILE = 'file';
|
||||||
|
|
||||||
export const IMPORT_HISTORY_TABLE_STATUS = {
|
export const IMPORT_HISTORY_TABLE_STATUS = {
|
||||||
inProgress: 'started',
|
inProgress: 'started',
|
||||||
complete: 'finished',
|
complete: 'finished',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { basic } from 'jest/vue_shared/components/import_history_table/mock_data';
|
||||||
|
|
||||||
|
import ImportHistoryTableSource from './import_history_table_source.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: ImportHistoryTableSource,
|
||||||
|
title: 'vue_shared/import/import_history_table_source',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
item: basic.items[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
components: { ImportHistoryTableSource },
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
template: `<import-history-table-source v-bind="$props"/>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = defaultProps;
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { GlIcon, GlLink, GlTruncate } from '@gitlab/ui';
|
||||||
|
import { SOURCE_TYPE_GROUP, SOURCE_TYPE_PROJECT, SOURCE_TYPE_FILE } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic formatter for showing the source information of an import
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'ImportHistoryTableSource',
|
||||||
|
components: {
|
||||||
|
GlIcon,
|
||||||
|
GlLink,
|
||||||
|
GlTruncate,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Should accept the data that comes form the BulkImport API
|
||||||
|
*/
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sourceIconName() {
|
||||||
|
switch (this.item.entity_type) {
|
||||||
|
case SOURCE_TYPE_PROJECT:
|
||||||
|
return 'project';
|
||||||
|
case SOURCE_TYPE_FILE:
|
||||||
|
return 'project';
|
||||||
|
case SOURCE_TYPE_GROUP:
|
||||||
|
return 'group';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isFile() {
|
||||||
|
return this.item.entity_type === SOURCE_TYPE_FILE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="gl-flex gl-items-start gl-gap-3 gl-pt-1">
|
||||||
|
<gl-icon :name="sourceIconName" class="gl-mt-1 gl-flex-shrink-0" />
|
||||||
|
<span v-if="isFile">{{ item.fileName }}</span>
|
||||||
|
<gl-link
|
||||||
|
v-else
|
||||||
|
class="gl-overflow-hidden !gl-text-default hover:gl-underline"
|
||||||
|
:href="item.source_full_path"
|
||||||
|
>
|
||||||
|
<gl-truncate :text="item.source_full_path" position="middle" with-tooltip />
|
||||||
|
</gl-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -10,6 +10,7 @@ import Tracking from '~/tracking';
|
||||||
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
|
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
|
||||||
import { keysFor, GO_TO_PROJECT_WEBIDE } from '~/behaviors/shortcuts/keybindings';
|
import { keysFor, GO_TO_PROJECT_WEBIDE } from '~/behaviors/shortcuts/keybindings';
|
||||||
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
||||||
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
|
import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
|
||||||
|
|
||||||
export const i18n = {
|
export const i18n = {
|
||||||
|
|
@ -32,7 +33,7 @@ export default {
|
||||||
ConfirmForkModal,
|
ConfirmForkModal,
|
||||||
},
|
},
|
||||||
i18n,
|
i18n,
|
||||||
mixins: [Tracking.mixin()],
|
mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
|
||||||
props: {
|
props: {
|
||||||
isFork: {
|
isFork: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
@ -141,13 +142,15 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
hideIDEActionsInDirectoryView() {
|
||||||
|
return this.glFeatures.directoryCodeDropdownUpdates && !this.isBlob;
|
||||||
|
},
|
||||||
actions() {
|
actions() {
|
||||||
return [
|
return this.hideIDEActionsInDirectoryView
|
||||||
this.pipelineEditorAction,
|
? [this.pipelineEditorAction, this.editAction].filter(Boolean)
|
||||||
this.webIdeAction,
|
: [this.pipelineEditorAction, this.webIdeAction, this.editAction, this.gitpodAction].filter(
|
||||||
this.editAction,
|
Boolean,
|
||||||
this.gitpodAction,
|
);
|
||||||
].filter((action) => action);
|
|
||||||
},
|
},
|
||||||
hasActions() {
|
hasActions() {
|
||||||
return this.actions.length > 0;
|
return this.actions.length > 0;
|
||||||
|
|
|
||||||
|
|
@ -557,3 +557,8 @@
|
||||||
@apply gl-line-clamp-2 gl-whitespace-normal;
|
@apply gl-line-clamp-2 gl-whitespace-normal;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stylelint-disable-next-line gitlab/no-gl-class
|
||||||
|
.project-destination-select .gl-button-text {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ module UserSettings
|
||||||
feature_category :system_access
|
feature_category :system_access
|
||||||
|
|
||||||
before_action :check_personal_access_tokens_enabled
|
before_action :check_personal_access_tokens_enabled
|
||||||
before_action do
|
|
||||||
push_frontend_feature_flag(:pat_ip, current_user)
|
|
||||||
end
|
|
||||||
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:ics) }
|
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:ics) }
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ module Resolvers
|
||||||
private
|
private
|
||||||
|
|
||||||
def unconditional_includes
|
def unconditional_includes
|
||||||
[:trigger_requests]
|
[:trigger_requests, :pipelines]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,11 @@ module Types
|
||||||
end
|
end
|
||||||
|
|
||||||
def triggered
|
def triggered
|
||||||
object.try(:trigger_request)
|
if Feature.enabled?(:ci_read_trigger_from_ci_pipeline, object.project)
|
||||||
|
object.pipeline.trigger_id.present?
|
||||||
|
else
|
||||||
|
object.try(:trigger_request).present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def manual_variables
|
def manual_variables
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module WorkItems
|
||||||
|
module Widgets
|
||||||
|
module ErrorTracking
|
||||||
|
# rubocop:disable Graphql/AuthorizeTypes -- we already authorize the work item itself
|
||||||
|
class StackTraceContextType < BaseObject
|
||||||
|
graphql_name 'WorkItemWidgetErrorTrackingStackTraceContext'
|
||||||
|
description 'Represents details about a line of code of the stack trace'
|
||||||
|
|
||||||
|
field :line_number, GraphQL::Types::Int,
|
||||||
|
null: true,
|
||||||
|
description: 'Line number of code.', method: :first
|
||||||
|
|
||||||
|
field :line, GraphQL::Types::String,
|
||||||
|
null: true,
|
||||||
|
description: 'Line of code.', method: :last
|
||||||
|
end
|
||||||
|
# rubocop:enable Graphql/AuthorizeTypes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module WorkItems
|
||||||
|
module Widgets
|
||||||
|
module ErrorTracking
|
||||||
|
# Disabling widget level authorization as it might be too granular
|
||||||
|
# and we already authorize the parent work item
|
||||||
|
# rubocop:disable Graphql/AuthorizeTypes -- reason above
|
||||||
|
class StackTraceType < BaseObject
|
||||||
|
graphql_name 'ErrorTrackingStackTrace'
|
||||||
|
description 'Represents a stack trace'
|
||||||
|
|
||||||
|
connection_type_class Types::CountableConnectionType
|
||||||
|
|
||||||
|
field :filename, GraphQL::Types::String,
|
||||||
|
null: true,
|
||||||
|
description: 'Filename of the stack trace.'
|
||||||
|
|
||||||
|
field :absolute_path, GraphQL::Types::String,
|
||||||
|
null: true,
|
||||||
|
description: 'Absolute path of the stack trace.', hash_key: "absPath"
|
||||||
|
|
||||||
|
field :function, GraphQL::Types::String,
|
||||||
|
null: true,
|
||||||
|
description: 'Name of the function where the error occured.'
|
||||||
|
|
||||||
|
field :line_number, GraphQL::Types::Int,
|
||||||
|
null: true,
|
||||||
|
description: 'Line number of the stack trace.', hash_key: "lineNo"
|
||||||
|
|
||||||
|
field :column_number, GraphQL::Types::Int,
|
||||||
|
null: true,
|
||||||
|
description: 'Column number of the stack trace.', hash_key: "colNo"
|
||||||
|
|
||||||
|
field :context, [Types::WorkItems::Widgets::ErrorTracking::StackTraceContextType],
|
||||||
|
null: true,
|
||||||
|
description: 'Context of the stack trace.', hash_key: "context"
|
||||||
|
end
|
||||||
|
# rubocop:enable Graphql/AuthorizeTypes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module WorkItems
|
||||||
|
module Widgets
|
||||||
|
class ErrorTrackingStatusEnum < BaseEnum
|
||||||
|
graphql_name 'ErrorTrackingStatus'
|
||||||
|
description 'Status of the error tracking service'
|
||||||
|
|
||||||
|
value 'SUCCESS', value: :success, description: 'Successfuly fetch the stack trace.'
|
||||||
|
value 'ERROR', value: :error, description: 'Error tracking service respond with an error.'
|
||||||
|
value 'NOT_FOUND', value: :not_found, description: 'Sentry issue not found.'
|
||||||
|
value 'RETRY', value: :retry, description: 'Error tracking service is not ready.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -13,7 +13,56 @@ module Types
|
||||||
implements ::Types::WorkItems::WidgetInterface
|
implements ::Types::WorkItems::WidgetInterface
|
||||||
|
|
||||||
field :identifier, GraphQL::Types::BigInt, null: true,
|
field :identifier, GraphQL::Types::BigInt, null: true,
|
||||||
description: 'Error tracking issue id.', method: :sentry_issue_identifier
|
description: 'Error tracking issue id.' \
|
||||||
|
'This field can only be resolved for one work item in any single request.',
|
||||||
|
method: :sentry_issue_identifier do
|
||||||
|
extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
field :stack_trace, ::Types::WorkItems::Widgets::ErrorTracking::StackTraceType.connection_type,
|
||||||
|
null: true,
|
||||||
|
description: 'Stack trace details of the error.' \
|
||||||
|
'This field can only be resolved for one work item in any single request.' do
|
||||||
|
extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
field :status, ErrorTrackingStatusEnum, null: true,
|
||||||
|
description: 'Response status of error service.' \
|
||||||
|
'This field can only be resolved for one work item in any single request.' do
|
||||||
|
extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stack_trace
|
||||||
|
return [] if object.sentry_issue_identifier.nil?
|
||||||
|
|
||||||
|
if latest_event_result[:status] == :success
|
||||||
|
Gitlab::ErrorTracking::StackTraceHighlightDecorator
|
||||||
|
.decorate(latest_event_result[:latest_event])
|
||||||
|
.stack_trace_entries
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def status
|
||||||
|
return :not_found if object.sentry_issue_identifier.nil?
|
||||||
|
|
||||||
|
if latest_event_result[:status] == :success
|
||||||
|
:success
|
||||||
|
elsif latest_event_result[:http_status] == :no_content
|
||||||
|
:retry
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def latest_event_result
|
||||||
|
@latest_event ||= ::ErrorTracking::IssueLatestEventService
|
||||||
|
.new(object.work_item.project, current_user, issue_id: object.sentry_issue_identifier)
|
||||||
|
.execute
|
||||||
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Graphql/AuthorizeTypes
|
# rubocop:enable Graphql/AuthorizeTypes
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,6 @@ module BreadcrumbsHelper
|
||||||
@breadcrumb_title = title
|
@breadcrumb_title = title
|
||||||
end
|
end
|
||||||
|
|
||||||
def breadcrumb_list_item(link)
|
|
||||||
content_tag :li, link, class: 'gl-breadcrumb-item gl-inline-flex'
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_to_breadcrumb_collapsed_links(link, location: :before)
|
def add_to_breadcrumb_collapsed_links(link, location: :before)
|
||||||
@breadcrumb_collapsed_links ||= {}
|
@breadcrumb_collapsed_links ||= {}
|
||||||
@breadcrumb_collapsed_links[location] ||= []
|
@breadcrumb_collapsed_links[location] ||= []
|
||||||
|
|
|
||||||
|
|
@ -41,29 +41,12 @@ module GroupsHelper
|
||||||
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
|
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_title(group)
|
def push_group_breadcrumbs(group)
|
||||||
@has_group_title = true
|
sorted_ancestors(group).with_route.reverse_each do |parent|
|
||||||
full_title = []
|
|
||||||
|
|
||||||
sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index|
|
|
||||||
if index > 0
|
|
||||||
add_to_breadcrumb_collapsed_links(
|
|
||||||
{ text: simple_sanitize(parent.name), href: group_path(parent), avatar_url: parent.try(:avatar_url) },
|
|
||||||
location: :before
|
|
||||||
)
|
|
||||||
else
|
|
||||||
full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
|
|
||||||
end
|
|
||||||
|
|
||||||
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent), parent.try(:avatar_url))
|
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent), parent.try(:avatar_url))
|
||||||
end
|
end
|
||||||
|
|
||||||
full_title << render("layouts/nav/breadcrumbs/collapsed_inline_list", location: :before, title: _("Show all breadcrumbs"))
|
|
||||||
|
|
||||||
full_title << breadcrumb_list_item(group_title_link(group))
|
|
||||||
push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group), group.try(:avatar_url))
|
push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group), group.try(:avatar_url))
|
||||||
|
|
||||||
full_title.join.html_safe
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def projects_lfs_status(group)
|
def projects_lfs_status(group)
|
||||||
|
|
|
||||||
|
|
@ -88,14 +88,10 @@ module PageLayoutHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def header_title(title = nil, title_url = nil)
|
def header_title(title = nil, title_url = nil)
|
||||||
if title
|
return @header_title unless title
|
||||||
|
|
||||||
@header_title = title
|
@header_title = title
|
||||||
@header_title_url = title_url
|
@header_title_url = title_url
|
||||||
else
|
|
||||||
return @header_title unless @header_title_url
|
|
||||||
|
|
||||||
breadcrumb_list_item(link_to(@header_title, @header_title_url))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def sidebar(name = nil)
|
def sidebar(name = nil)
|
||||||
|
|
|
||||||
|
|
@ -103,14 +103,18 @@ module ProjectsHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_title(project)
|
def push_project_breadcrumbs(project)
|
||||||
namespace_link = build_namespace_breadcrumb_link(project)
|
if project.group
|
||||||
project_link = build_project_breadcrumb_link(project)
|
push_group_breadcrumbs(project.group)
|
||||||
|
else
|
||||||
|
owner = project.namespace.owner
|
||||||
|
name = sanitize(owner.name, tags: [])
|
||||||
|
url = user_path(owner)
|
||||||
|
|
||||||
namespace_link = breadcrumb_list_item(namespace_link) unless project.group
|
push_to_schema_breadcrumb(name, url)
|
||||||
project_link = breadcrumb_list_item project_link
|
end
|
||||||
|
|
||||||
"#{namespace_link} #{project_link}".html_safe
|
push_to_schema_breadcrumb(simple_sanitize(project.name), project_path(project), project.try(:avatar_url))
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_project_message(project)
|
def remove_project_message(project)
|
||||||
|
|
@ -1071,38 +1075,6 @@ module ProjectsHelper
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_project_breadcrumb_link(project)
|
|
||||||
project_name = simple_sanitize(project.name)
|
|
||||||
|
|
||||||
push_to_schema_breadcrumb(project_name, project_path(project), project.try(:avatar_url))
|
|
||||||
|
|
||||||
link_to project_path(project), class: '!gl-inline-flex' do
|
|
||||||
if project.avatar_url && !Rails.env.test?
|
|
||||||
icon = render Pajamas::AvatarComponent.new(
|
|
||||||
project,
|
|
||||||
alt: project.name,
|
|
||||||
size: 16,
|
|
||||||
class: 'avatar-tile'
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
[icon, content_tag("span", project_name, class: "js-breadcrumb-item-text")].join.html_safe
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_namespace_breadcrumb_link(project)
|
|
||||||
if project.group
|
|
||||||
group_title(project.group)
|
|
||||||
else
|
|
||||||
owner = project.namespace.owner
|
|
||||||
name = sanitize(owner.name, tags: [])
|
|
||||||
url = user_path(owner)
|
|
||||||
|
|
||||||
push_to_schema_breadcrumb(name, url)
|
|
||||||
link_to(name, url)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_inactive_projects?
|
def delete_inactive_projects?
|
||||||
strong_memoize(:delete_inactive_projects_setting) do
|
strong_memoize(:delete_inactive_projects_setting) do
|
||||||
::Gitlab::CurrentSettings.delete_inactive_projects?
|
::Gitlab::CurrentSettings.delete_inactive_projects?
|
||||||
|
|
|
||||||
|
|
@ -170,20 +170,6 @@ module Ci
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :eager_load_everything, -> do
|
|
||||||
includes(
|
|
||||||
[
|
|
||||||
{ pipeline: [:project, :user] },
|
|
||||||
:job_artifacts_archive,
|
|
||||||
:metadata,
|
|
||||||
:trigger_request,
|
|
||||||
:project,
|
|
||||||
:user,
|
|
||||||
:tags
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
scope :with_exposed_artifacts, -> do
|
scope :with_exposed_artifacts, -> do
|
||||||
joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
|
joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
|
||||||
.includes(:metadata, :job_artifacts_metadata)
|
.includes(:metadata, :job_artifacts_metadata)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,11 @@ module Ci
|
||||||
|
|
||||||
has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
|
has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
|
||||||
has_one :sourced_pipeline, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id, inverse_of: :source_job
|
has_one :sourced_pipeline, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id, inverse_of: :source_job
|
||||||
|
has_one :trigger, through: :pipeline
|
||||||
|
|
||||||
belongs_to :trigger_request
|
belongs_to :trigger_request
|
||||||
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
|
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
|
||||||
|
|
||||||
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
|
|
||||||
|
|
||||||
accepts_nested_attributes_for :needs
|
accepts_nested_attributes_for :needs
|
||||||
|
|
||||||
scope :preload_needs, -> { preload(:needs) }
|
scope :preload_needs, -> { preload(:needs) }
|
||||||
|
|
@ -268,6 +267,14 @@ module Ci
|
||||||
options[:manual_confirmation] if manual_job?
|
options[:manual_confirmation] if manual_job?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def trigger_short_token
|
||||||
|
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
|
||||||
|
trigger&.short_token
|
||||||
|
else
|
||||||
|
trigger_request&.trigger_short_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def dependencies
|
def dependencies
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,12 @@ module Ci
|
||||||
self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
|
self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_trigger_request
|
|
||||||
trigger_requests.last
|
|
||||||
end
|
|
||||||
|
|
||||||
def last_used
|
def last_used
|
||||||
last_trigger_request.try(:created_at)
|
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
|
||||||
|
pipelines.last&.created_at
|
||||||
|
else
|
||||||
|
trigger_requests.last&.created_at
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def short_token
|
def short_token
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Namespaces
|
||||||
|
module Preloaders
|
||||||
|
class GroupRootAncestorPreloader < NamespaceRootAncestorPreloader
|
||||||
|
extend Gitlab::Utils::Override
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
override :join_sql
|
||||||
|
def join_sql
|
||||||
|
Group.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Namespaces
|
||||||
|
module Preloaders
|
||||||
|
class NamespaceRootAncestorPreloader
|
||||||
|
def initialize(namespaces, root_ancestor_preloads = [])
|
||||||
|
@namespaces = namespaces
|
||||||
|
@root_ancestor_preloads = root_ancestor_preloads
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
|
||||||
|
.select('namespaces.*, root_query.id as source_id')
|
||||||
|
|
||||||
|
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
|
||||||
|
|
||||||
|
root_ancestors_by_id = root_query.group_by(&:source_id)
|
||||||
|
|
||||||
|
@namespaces.each do |namespace|
|
||||||
|
namespace.root_ancestor = root_ancestors_by_id[namespace.id].first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def join_sql
|
||||||
|
Namespace.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Namespaces
|
||||||
|
module Preloaders
|
||||||
|
class ProjectRootAncestorPreloader
|
||||||
|
def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = [])
|
||||||
|
@projects = projects
|
||||||
|
@namespace_sti_name = namespace_sti_name
|
||||||
|
@root_ancestor_preloads = root_ancestor_preloads
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return unless @projects.is_a?(ActiveRecord::Relation)
|
||||||
|
|
||||||
|
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
|
||||||
|
.select('namespaces.*, root_query.project_id as source_id')
|
||||||
|
|
||||||
|
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
|
||||||
|
|
||||||
|
root_ancestors_by_id = root_query.group_by(&:source_id)
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: @projects, associations: :namespace).call
|
||||||
|
@projects.each do |project|
|
||||||
|
root_ancestor = root_ancestors_by_id[project.id]&.first
|
||||||
|
project.namespace.root_ancestor = root_ancestor if root_ancestor.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def join_sql
|
||||||
|
@projects
|
||||||
|
.joins(@namespace_sti_name)
|
||||||
|
.select('projects.id as project_id, namespaces.traversal_ids[1] as root_id')
|
||||||
|
.to_sql
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Preloaders
|
|
||||||
class GroupRootAncestorPreloader < NamespaceRootAncestorPreloader
|
|
||||||
extend Gitlab::Utils::Override
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
override :join_sql
|
|
||||||
def join_sql
|
|
||||||
Group.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Preloaders
|
|
||||||
class NamespaceRootAncestorPreloader
|
|
||||||
def initialize(namespaces, root_ancestor_preloads = [])
|
|
||||||
@namespaces = namespaces
|
|
||||||
@root_ancestor_preloads = root_ancestor_preloads
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute
|
|
||||||
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
|
|
||||||
.select('namespaces.*, root_query.id as source_id')
|
|
||||||
|
|
||||||
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
|
|
||||||
|
|
||||||
root_ancestors_by_id = root_query.group_by(&:source_id)
|
|
||||||
|
|
||||||
@namespaces.each do |namespace|
|
|
||||||
namespace.root_ancestor = root_ancestors_by_id[namespace.id].first
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def join_sql
|
|
||||||
Namespace.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Preloaders
|
|
||||||
class ProjectRootAncestorPreloader
|
|
||||||
def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = [])
|
|
||||||
@projects = projects
|
|
||||||
@namespace_sti_name = namespace_sti_name
|
|
||||||
@root_ancestor_preloads = root_ancestor_preloads
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute
|
|
||||||
return unless @projects.is_a?(ActiveRecord::Relation)
|
|
||||||
|
|
||||||
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
|
|
||||||
.select('namespaces.*, root_query.project_id as source_id')
|
|
||||||
|
|
||||||
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
|
|
||||||
|
|
||||||
root_ancestors_by_id = root_query.group_by(&:source_id)
|
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(records: @projects, associations: :namespace).call
|
|
||||||
@projects.each do |project|
|
|
||||||
root_ancestor = root_ancestors_by_id[project.id]&.first
|
|
||||||
project.namespace.root_ancestor = root_ancestor if root_ancestor.present?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def join_sql
|
|
||||||
@projects
|
|
||||||
.joins(@namespace_sti_name)
|
|
||||||
.select('projects.id as project_id, namespaces.traversal_ids[1] as root_id')
|
|
||||||
.to_sql
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -13,15 +13,21 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def trigger_variables
|
def trigger_variables
|
||||||
|
@trigger_variables ||=
|
||||||
|
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
|
||||||
|
return [] if pipeline.trigger_id.blank?
|
||||||
|
|
||||||
|
pipeline.variables.map(&:to_hash_variable)
|
||||||
|
else
|
||||||
return [] unless trigger_request
|
return [] unless trigger_request
|
||||||
|
|
||||||
@trigger_variables ||=
|
|
||||||
if pipeline.variables.any?
|
if pipeline.variables.any?
|
||||||
pipeline.variables.map(&:to_hash_variable)
|
pipeline.variables.map(&:to_hash_variable)
|
||||||
else
|
else
|
||||||
trigger_request.user_variables
|
trigger_request.user_variables
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def execute_in
|
def execute_in
|
||||||
scheduled? && scheduled_at && [0, scheduled_at - Time.now].max
|
scheduled? && scheduled_at && [0, scheduled_at - Time.now].max
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,8 @@ class BuildDetailsEntity < Ci::JobEntity
|
||||||
raw_project_job_path(project, build)
|
raw_project_job_path(project, build)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :trigger, if: ->(*) { build.trigger_request } do
|
expose :trigger,
|
||||||
|
if: ->(*) { Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project) ? build.trigger : build.trigger_request } do
|
||||||
expose :trigger_short_token, as: :short_token
|
expose :trigger_short_token, as: :short_token
|
||||||
|
|
||||||
expose :trigger_variables, as: :variables, using: TriggerVariableEntity
|
expose :trigger_variables, as: :variables, using: TriggerVariableEntity
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class PipelineSerializer < BaseSerializer
|
||||||
:cancelable_statuses,
|
:cancelable_statuses,
|
||||||
:retryable_builds,
|
:retryable_builds,
|
||||||
:stages,
|
:stages,
|
||||||
|
:trigger,
|
||||||
:trigger_requests,
|
:trigger_requests,
|
||||||
:user,
|
:user,
|
||||||
(:latest_statuses if preload_statuses),
|
(:latest_statuses if preload_statuses),
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ module PersonalAccessTokens
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_used_ip_needs_update?
|
def last_used_ip_needs_update?
|
||||||
return false unless Feature.enabled?(:pat_ip, @personal_access_token.user)
|
|
||||||
return false unless Gitlab::IpAddressState.current
|
return false unless Gitlab::IpAddressState.current
|
||||||
return true if @personal_access_token.last_used_at.nil?
|
return true if @personal_access_token.last_used_at.nil?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,27 @@
|
||||||
- header_title _("New project"), new_project_path
|
- header_title _("New project"), new_project_path
|
||||||
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
|
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
|
||||||
|
|
||||||
= render ::Layouts::PageHeadingComponent.new('') do |c|
|
- if Feature.enabled?(:new_project_creation_form, @user)
|
||||||
|
- add_page_specific_style 'page_bundles/projects'
|
||||||
|
- namespace_id = namespace_id_from(params)
|
||||||
|
#js-import-gitlab-project-root{ data: {
|
||||||
|
back_button_path: new_project_path(anchor: 'import_project'),
|
||||||
|
namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path || current_user.namespace.full_path,
|
||||||
|
namespace_id: namespace_id_from(params) || @current_user_group&.id,
|
||||||
|
import_gitlab_project_path: import_gitlab_project_path,
|
||||||
|
root_path: root_path,
|
||||||
|
user_namespace_id: current_user.namespace_id,
|
||||||
|
can_create_project: current_user.can_create_project?.to_s,
|
||||||
|
root_url: root_url,
|
||||||
|
} }
|
||||||
|
- else
|
||||||
|
= render ::Layouts::PageHeadingComponent.new('') do |c|
|
||||||
- c.with_heading do
|
- c.with_heading do
|
||||||
%span.gl-inline-flex.gl-items-center.gl-gap-3
|
%span.gl-inline-flex.gl-items-center.gl-gap-3
|
||||||
= sprite_icon('tanuki', size: 32)
|
= sprite_icon('tanuki', size: 32)
|
||||||
= _('Import an exported GitLab project')
|
= _('Import an exported GitLab project')
|
||||||
|
|
||||||
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
|
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
|
||||||
= render 'import/shared/new_project_form'
|
= render 'import/shared/new_project_form'
|
||||||
|
|
||||||
.row
|
.row
|
||||||
|
|
@ -25,4 +39,3 @@
|
||||||
= _('Import project')
|
= _('Import project')
|
||||||
= render Pajamas::ButtonComponent.new(href: new_project_path) do
|
= render Pajamas::ButtonComponent.new(href: new_project_path) do
|
||||||
= _('Cancel')
|
= _('Cancel')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
- page_title @group.name
|
- page_title @group.name
|
||||||
- page_description @group.description_html unless page_description
|
- page_description @group.description_html unless page_description
|
||||||
- header_title group_title(@group) unless header_title
|
- push_group_breadcrumbs(@group)
|
||||||
- nav "group"
|
- nav "group"
|
||||||
- display_subscription_banner!
|
- display_subscription_banner!
|
||||||
- base_layout = local_assigns[:base_layout]
|
- base_layout = local_assigns[:base_layout]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
- dropdown_location = local_assigns.fetch(:location, nil)
|
|
||||||
- button_tooltip = local_assigns.fetch(:title, _("Show all breadcrumbs"))
|
|
||||||
- if defined?(@breadcrumb_collapsed_links) && @breadcrumb_collapsed_links.key?(dropdown_location)
|
|
||||||
%li.expander.gl-breadcrumb-item.gl-inline-flex
|
|
||||||
= render Pajamas::ButtonComponent.new(icon: 'ellipsis_h',
|
|
||||||
button_options: { class: 'button-ellipsis-horizontal js-breadcrumbs-collapsed-expander gl-ml-0', type: "button", data: { container: 'body' }, "aria-label": button_tooltip, title: button_tooltip })
|
|
||||||
- @breadcrumb_collapsed_links[dropdown_location].each_with_index do |item, index|
|
|
||||||
%li.gl-breadcrumb-item{ :class => "!gl-hidden" }
|
|
||||||
= link_to item[:href] do
|
|
||||||
- if item[:avatar_url]
|
|
||||||
= render Pajamas::AvatarComponent.new(item[:avatar_url], alt: item[:text], class: "avatar-tile", size: 16)
|
|
||||||
= item[:text]
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
- page_title @project.full_name
|
- page_title @project.full_name
|
||||||
- page_description @project.description_html unless page_description
|
- page_description @project.description_html unless page_description
|
||||||
- header_title project_title(@project) unless header_title
|
- push_project_breadcrumbs(@project)
|
||||||
- nav "project"
|
- nav "project"
|
||||||
- page_itemtype 'http://schema.org/SoftwareSourceCode'
|
- page_itemtype 'http://schema.org/SoftwareSourceCode'
|
||||||
- display_subscription_banner!
|
- display_subscription_banner!
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
description: "Selects an editor in the Edit dropdown menu"
|
description: "Selects an editor in the Edit dropdown menu"
|
||||||
category: default
|
internal_events: true
|
||||||
action: click_consolidated_edit
|
action: click_consolidated_edit
|
||||||
extra_properties:
|
extra_properties:
|
||||||
identifiers:
|
identifiers:
|
||||||
|
|
@ -14,4 +14,3 @@ tiers:
|
||||||
additional_properties:
|
additional_properties:
|
||||||
label:
|
label:
|
||||||
description: "The editor selected in the Edit dropdown menu"
|
description: "The editor selected in the Edit dropdown menu"
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
name: pat_ip
|
name: ci_read_trigger_from_ci_pipeline
|
||||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428577
|
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502767
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161076
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180728
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428577
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/508601
|
||||||
milestone: '17.8'
|
milestone: '17.10'
|
||||||
group: group::authentication
|
group: group::ci platform
|
||||||
type: beta
|
type: gitlab_com_derisk
|
||||||
default_enabled: true
|
default_enabled: false
|
||||||
|
|
@ -5,6 +5,5 @@ feature_category: continuous_integration
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163429
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163429
|
||||||
milestone: '17.4'
|
milestone: '17.4'
|
||||||
queued_migration_version: 20240827095907
|
queued_migration_version: 20240827095907
|
||||||
# Replace with the approximate date you think it's best to ensure the completion of this BBM.
|
|
||||||
finalize_after: '2024-09-25'
|
finalize_after: '2024-09-25'
|
||||||
finalized_by: # version of the migration that finalized this BBM
|
finalized_by: '20250218232001'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FinalizeHkBackfillCiBuildNeedsProjectId < Gitlab::Database::Migration[2.2]
|
||||||
|
milestone '17.10'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||||
|
|
||||||
|
def up
|
||||||
|
ensure_batched_background_migration_is_finished(
|
||||||
|
job_class_name: 'BackfillCiBuildNeedsProjectId',
|
||||||
|
table_name: :ci_build_needs,
|
||||||
|
column_name: :id,
|
||||||
|
job_arguments: [:project_id, :p_ci_builds, :project_id, :build_id, :partition_id],
|
||||||
|
finalize: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down; end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0ac37c0e9f2bc5df498fc0c1b095e42606eba52f120deb7e6e0df599eeab0a61
|
||||||
|
|
@ -8,7 +8,7 @@ title: GitLab Duo Self-Hosted
|
||||||
|
|
||||||
{{< details >}}
|
{{< details >}}
|
||||||
|
|
||||||
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||||
- Offering: GitLab Self-Managed
|
- Offering: GitLab Self-Managed
|
||||||
|
|
||||||
{{< /details >}}
|
{{< /details >}}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ title: Configure GitLab to access GitLab Duo Self-Hosted
|
||||||
|
|
||||||
{{< details >}}
|
{{< details >}}
|
||||||
|
|
||||||
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||||
- Offering: GitLab Self-Managed
|
- Offering: GitLab Self-Managed
|
||||||
|
|
||||||
{{< /details >}}
|
{{< /details >}}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ title: Enable logging for self-hosted models
|
||||||
|
|
||||||
{{< details >}}
|
{{< details >}}
|
||||||
|
|
||||||
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||||
- Offering: GitLab Self-Managed
|
- Offering: GitLab Self-Managed
|
||||||
|
|
||||||
{{< /details >}}
|
{{< /details >}}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ title: GitLab Duo Self-Hosted supported platforms
|
||||||
|
|
||||||
{{< details >}}
|
{{< details >}}
|
||||||
|
|
||||||
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||||
- Offering: GitLab Self-Managed
|
- Offering: GitLab Self-Managed
|
||||||
|
|
||||||
{{< /details >}}
|
{{< /details >}}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ title: Supported GitLab Duo Self-Hosted models and hardware requirements
|
||||||
|
|
||||||
{{< details >}}
|
{{< details >}}
|
||||||
|
|
||||||
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||||
- Offering: GitLab Self-Managed
|
- Offering: GitLab Self-Managed
|
||||||
|
|
||||||
{{< /details >}}
|
{{< /details >}}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ title: Troubleshooting GitLab Duo Self-Hosted
|
||||||
|
|
||||||
{{< details >}}
|
{{< details >}}
|
||||||
|
|
||||||
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||||
- Offering: GitLab Self-Managed
|
- Offering: GitLab Self-Managed
|
||||||
|
|
||||||
{{< /details >}}
|
{{< /details >}}
|
||||||
|
|
|
||||||
|
|
@ -15385,6 +15385,30 @@ The edge type for [`EpicList`](#epiclist).
|
||||||
| <a id="epiclistedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
| <a id="epiclistedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||||
| <a id="epiclistedgenode"></a>`node` | [`EpicList`](#epiclist) | The item at the end of the edge. |
|
| <a id="epiclistedgenode"></a>`node` | [`EpicList`](#epiclist) | The item at the end of the edge. |
|
||||||
|
|
||||||
|
#### `ErrorTrackingStackTraceConnection`
|
||||||
|
|
||||||
|
The connection type for [`ErrorTrackingStackTrace`](#errortrackingstacktrace).
|
||||||
|
|
||||||
|
##### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="errortrackingstacktraceconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
|
||||||
|
| <a id="errortrackingstacktraceconnectionedges"></a>`edges` | [`[ErrorTrackingStackTraceEdge]`](#errortrackingstacktraceedge) | A list of edges. |
|
||||||
|
| <a id="errortrackingstacktraceconnectionnodes"></a>`nodes` | [`[ErrorTrackingStackTrace]`](#errortrackingstacktrace) | A list of nodes. |
|
||||||
|
| <a id="errortrackingstacktraceconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||||
|
|
||||||
|
#### `ErrorTrackingStackTraceEdge`
|
||||||
|
|
||||||
|
The edge type for [`ErrorTrackingStackTrace`](#errortrackingstacktrace).
|
||||||
|
|
||||||
|
##### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="errortrackingstacktraceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||||
|
| <a id="errortrackingstacktraceedgenode"></a>`node` | [`ErrorTrackingStackTrace`](#errortrackingstacktrace) | The item at the end of the edge. |
|
||||||
|
|
||||||
#### `EscalationPolicyTypeConnection`
|
#### `EscalationPolicyTypeConnection`
|
||||||
|
|
||||||
The connection type for [`EscalationPolicyType`](#escalationpolicytype).
|
The connection type for [`EscalationPolicyType`](#escalationpolicytype).
|
||||||
|
|
@ -25463,6 +25487,21 @@ Check permissions for the current user on an epic.
|
||||||
| <a id="epicpermissionsreadepiciid"></a>`readEpicIid` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_epic_iid` on this resource. |
|
| <a id="epicpermissionsreadepiciid"></a>`readEpicIid` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_epic_iid` on this resource. |
|
||||||
| <a id="epicpermissionsupdateepic"></a>`updateEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_epic` on this resource. |
|
| <a id="epicpermissionsupdateepic"></a>`updateEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_epic` on this resource. |
|
||||||
|
|
||||||
|
### `ErrorTrackingStackTrace`
|
||||||
|
|
||||||
|
Represents a stack trace.
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="errortrackingstacktraceabsolutepath"></a>`absolutePath` | [`String`](#string) | Absolute path of the stack trace. |
|
||||||
|
| <a id="errortrackingstacktracecolumnnumber"></a>`columnNumber` | [`Int`](#int) | Column number of the stack trace. |
|
||||||
|
| <a id="errortrackingstacktracecontext"></a>`context` | [`[WorkItemWidgetErrorTrackingStackTraceContext!]`](#workitemwidgeterrortrackingstacktracecontext) | Context of the stack trace. |
|
||||||
|
| <a id="errortrackingstacktracefilename"></a>`filename` | [`String`](#string) | Filename of the stack trace. |
|
||||||
|
| <a id="errortrackingstacktracefunction"></a>`function` | [`String`](#string) | Name of the function where the error occured. |
|
||||||
|
| <a id="errortrackingstacktracelinenumber"></a>`lineNumber` | [`Int`](#int) | Line number of the stack trace. |
|
||||||
|
|
||||||
### `EscalationPolicyType`
|
### `EscalationPolicyType`
|
||||||
|
|
||||||
Represents an escalation policy.
|
Represents an escalation policy.
|
||||||
|
|
@ -39651,9 +39690,22 @@ Represents the error tracking widget.
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="workitemwidgeterrortrackingidentifier"></a>`identifier` | [`BigInt`](#bigint) | Error tracking issue id. |
|
| <a id="workitemwidgeterrortrackingidentifier"></a>`identifier` | [`BigInt`](#bigint) | Error tracking issue id.This field can only be resolved for one work item in any single request. |
|
||||||
|
| <a id="workitemwidgeterrortrackingstacktrace"></a>`stackTrace` | [`ErrorTrackingStackTraceConnection`](#errortrackingstacktraceconnection) | Stack trace details of the error.This field can only be resolved for one work item in any single request. (see [Connections](#connections)) |
|
||||||
|
| <a id="workitemwidgeterrortrackingstatus"></a>`status` | [`ErrorTrackingStatus`](#errortrackingstatus) | Response status of error service.This field can only be resolved for one work item in any single request. |
|
||||||
| <a id="workitemwidgeterrortrackingtype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
|
| <a id="workitemwidgeterrortrackingtype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
|
||||||
|
|
||||||
|
### `WorkItemWidgetErrorTrackingStackTraceContext`
|
||||||
|
|
||||||
|
Represents details about a line of code of the stack trace.
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="workitemwidgeterrortrackingstacktracecontextline"></a>`line` | [`String`](#string) | Line of code. |
|
||||||
|
| <a id="workitemwidgeterrortrackingstacktracecontextlinenumber"></a>`lineNumber` | [`Int`](#int) | Line number of code. |
|
||||||
|
|
||||||
### `WorkItemWidgetHealthStatus`
|
### `WorkItemWidgetHealthStatus`
|
||||||
|
|
||||||
Represents a health status widget.
|
Represents a health status widget.
|
||||||
|
|
@ -41402,6 +41454,17 @@ Epic ID wildcard values.
|
||||||
| <a id="epicwildcardidany"></a>`ANY` | Any epic is assigned. |
|
| <a id="epicwildcardidany"></a>`ANY` | Any epic is assigned. |
|
||||||
| <a id="epicwildcardidnone"></a>`NONE` | No epic is assigned. |
|
| <a id="epicwildcardidnone"></a>`NONE` | No epic is assigned. |
|
||||||
|
|
||||||
|
### `ErrorTrackingStatus`
|
||||||
|
|
||||||
|
Status of the error tracking service.
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
| ----- | ----------- |
|
||||||
|
| <a id="errortrackingstatuserror"></a>`ERROR` | Error tracking service respond with an error. |
|
||||||
|
| <a id="errortrackingstatusnot_found"></a>`NOT_FOUND` | Sentry issue not found. |
|
||||||
|
| <a id="errortrackingstatusretry"></a>`RETRY` | Error tracking service is not ready. |
|
||||||
|
| <a id="errortrackingstatussuccess"></a>`SUCCESS` | Successfuly fetch the stack trace. |
|
||||||
|
|
||||||
### `EscalationRuleStatus`
|
### `EscalationRuleStatus`
|
||||||
|
|
||||||
Escalation rule statuses.
|
Escalation rule statuses.
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,10 @@ A REST API request must start with the root endpoint and the path.
|
||||||
- The path must start with `/api/v4` (`v4` represents the API version).
|
- The path must start with `/api/v4` (`v4` represents the API version).
|
||||||
|
|
||||||
In the following example, the API request retrieves the list of all projects on GitLab host
|
In the following example, the API request retrieves the list of all projects on GitLab host
|
||||||
`example.com`:
|
`gitlab.example.com`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl "https://example.com/api/v4/projects"
|
curl "https://gitlab.example.com/api/v4/projects"
|
||||||
```
|
```
|
||||||
|
|
||||||
Access to some endpoints require authentication. For more information, see
|
Access to some endpoints require authentication. For more information, see
|
||||||
|
|
@ -69,14 +69,14 @@ send the payload body:
|
||||||
- Query string:
|
- Query string:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl --request POST "https://example.com/api/v4/projects?name=<example-name>&description=<example-description>"
|
curl --request POST "https://gitlab.example.com/api/v4/projects?name=<example-name>&description=<example-description>"
|
||||||
```
|
```
|
||||||
|
|
||||||
- Request payload (JSON):
|
- Request payload (JSON):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl --request POST --header "Content-Type: application/json" \
|
curl --request POST --header "Content-Type: application/json" \
|
||||||
--data '{"name":"<example-name>", "description":"<example-description>"}' "https://example.com/api/v4/projects"
|
--data '{"name":"<example-name>", "description":"<example-description>"}' "https://gitlab.example.com/api/v4/projects"
|
||||||
```
|
```
|
||||||
|
|
||||||
URL encoded query strings have a length limitation. Requests that are too large
|
URL encoded query strings have a length limitation. Requests that are too large
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ For example, this script uses a colon:
|
||||||
```yaml
|
```yaml
|
||||||
job:
|
job:
|
||||||
script:
|
script:
|
||||||
- curl --request POST --header 'Content-Type: application/json' "https://gitlab/api/v4/projects"
|
- curl --request POST --header 'Content-Type: application/json' "https://gitlab.example.com/api/v4/projects"
|
||||||
```
|
```
|
||||||
|
|
||||||
To be considered valid YAML, you must wrap the entire command in single quotes. If
|
To be considered valid YAML, you must wrap the entire command in single quotes. If
|
||||||
|
|
@ -41,7 +41,7 @@ if possible:
|
||||||
```yaml
|
```yaml
|
||||||
job:
|
job:
|
||||||
script:
|
script:
|
||||||
- 'curl --request POST --header "Content-Type: application/json" "https://gitlab/api/v4/projects"'
|
- 'curl --request POST --header "Content-Type: application/json" "https://gitlab.example.com/api/v4/projects"'
|
||||||
```
|
```
|
||||||
|
|
||||||
You can verify the syntax is valid with the [CI Lint](../yaml/lint.md) tool.
|
You can verify the syntax is valid with the [CI Lint](../yaml/lint.md) tool.
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ Dashboards support the following filters:
|
||||||
|
|
||||||
- **Date range**: Date selector to filter data by date.
|
- **Date range**: Date selector to filter data by date.
|
||||||
- **Anonymous users**: Toggle to include or exclude anonymous users from the dataset.
|
- **Anonymous users**: Toggle to include or exclude anonymous users from the dataset.
|
||||||
|
- **Project**: Dropdown list to filter data by project.
|
||||||
|
|
||||||
#### Dashboard status
|
#### Dashboard status
|
||||||
|
|
||||||
|
|
@ -132,6 +133,8 @@ To create a built-in analytics dashboard:
|
||||||
enabled: true
|
enabled: true
|
||||||
dateRange:
|
dateRange:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
projects:
|
||||||
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
Refer to the `DashboardFilters` type in the [`ee/app/validators/json_schemas/analytics_dashboard.json`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/analytics_dashboard.json) for a list of supported filters.
|
Refer to the `DashboardFilters` type in the [`ee/app/validators/json_schemas/analytics_dashboard.json`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/analytics_dashboard.json) for a list of supported filters.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ and thumbs-ups. React with emoji on:
|
||||||
|
|
||||||
- [Issues](project/issues/_index.md).
|
- [Issues](project/issues/_index.md).
|
||||||
- [Tasks](tasks.md).
|
- [Tasks](tasks.md).
|
||||||
- [Merge requests](project/merge_requests/_index.md), [snippets](snippets.md).
|
- [Merge requests](project/merge_requests/_index.md) and [snippets](snippets.md).
|
||||||
- [Epics](group/epics/_index.md).
|
- [Epics](group/epics/_index.md).
|
||||||
- [Objectives and key results](okrs.md).
|
- [Objectives and key results](okrs.md).
|
||||||
- Anywhere else you can have a comment thread.
|
- Anywhere else you can have a comment thread.
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ When you delete or block an enterprise user account, their personal access token
|
||||||
- In GitLab 16.0 and earlier, token usage information is updated every 24 hours.
|
- In GitLab 16.0 and earlier, token usage information is updated every 24 hours.
|
||||||
- The frequency of token usage information updates [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/410168) in GitLab 16.1 from 24 hours to 10 minutes.
|
- The frequency of token usage information updates [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/410168) in GitLab 16.1 from 24 hours to 10 minutes.
|
||||||
- Ability to view IP addresses [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/428577) in GitLab 17.8 [with a flag](../../administration/feature_flags.md) named `pat_ip`. Enabled by default in 17.9.
|
- Ability to view IP addresses [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/428577) in GitLab 17.8 [with a flag](../../administration/feature_flags.md) named `pat_ip`. Enabled by default in 17.9.
|
||||||
|
- Ability to view IP addresses made [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/513302) in GitLab 17.10. Feature flag `pat_ip` removed.
|
||||||
|
|
||||||
{{< /history >}}
|
{{< /history >}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ module API
|
||||||
authenticate!
|
authenticate!
|
||||||
authorize! :admin_build, user_project
|
authorize! :admin_build, user_project
|
||||||
|
|
||||||
triggers = user_project.triggers.includes(:trigger_requests)
|
triggers = user_project.triggers.includes(:trigger_requests, :pipelines)
|
||||||
|
|
||||||
present paginate(triggers), with: Entities::Trigger, current_user: current_user
|
present paginate(triggers), with: Entities::Trigger, current_user: current_user
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ module API
|
||||||
group_projects = projects_for_group_preload(projects_relation)
|
group_projects = projects_for_group_preload(projects_relation)
|
||||||
groups = group_projects.map(&:namespace)
|
groups = group_projects.map(&:namespace)
|
||||||
|
|
||||||
Preloaders::GroupRootAncestorPreloader.new(groups).execute
|
::Namespaces::Preloaders::GroupRootAncestorPreloader.new(groups).execute
|
||||||
|
|
||||||
group_projects.each do |project|
|
group_projects.each do |project|
|
||||||
project.group = project.namespace
|
project.group = project.namespace
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ module BulkImports
|
||||||
module Pipelines
|
module Pipelines
|
||||||
class MembersPipeline
|
class MembersPipeline
|
||||||
include Pipeline
|
include Pipeline
|
||||||
|
include HexdigestCacheStrategy
|
||||||
|
|
||||||
GROUP_MEMBER_RELATIONS = %i[direct inherited shared_from_groups].freeze
|
GROUP_MEMBER_RELATIONS = %i[direct inherited shared_from_groups].freeze
|
||||||
PROJECT_MEMBER_RELATIONS = %i[direct inherited invited_groups shared_into_ancestors].freeze
|
PROJECT_MEMBER_RELATIONS = %i[direct inherited invited_groups shared_into_ancestors].freeze
|
||||||
|
|
@ -16,12 +17,24 @@ module BulkImports
|
||||||
transformer Import::BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer
|
transformer Import::BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer
|
||||||
|
|
||||||
def extract(context)
|
def extract(context)
|
||||||
graphql_extractor.extract(context)
|
extracted_data = graphql_extractor.extract(context)
|
||||||
|
|
||||||
|
# add source_xid to each entry to ensure uniqueness when caching
|
||||||
|
extracted_data.each do |entry|
|
||||||
|
entry['source_xid'] = context.source_xid
|
||||||
|
entry['entity_type'] = context.entity_type
|
||||||
|
end
|
||||||
|
|
||||||
|
extracted_data
|
||||||
end
|
end
|
||||||
|
|
||||||
def load(_context, data)
|
def load(_context, data)
|
||||||
return unless data
|
return unless data
|
||||||
|
|
||||||
|
# Remove source_xid and entity_type since we don't use them in membership creation
|
||||||
|
data.delete('source_xid')
|
||||||
|
data.delete('entity_type')
|
||||||
|
|
||||||
if data[:source_user]
|
if data[:source_user]
|
||||||
create_placeholder_membership(data)
|
create_placeholder_membership(data)
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ module BulkImports
|
||||||
|
|
||||||
attr_reader :tracker
|
attr_reader :tracker
|
||||||
|
|
||||||
|
delegate :source_xid, :entity_type, to: :entity
|
||||||
|
|
||||||
def initialize(tracker, extra = {})
|
def initialize(tracker, extra = {})
|
||||||
@tracker = tracker
|
@tracker = tracker
|
||||||
@extra = extra
|
@extra = extra
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,19 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_payload(job)
|
def build_payload(job)
|
||||||
base_payload = { cell_id: Gitlab.config.cell.id }
|
base_payload = { scoped_user_id: job.scoped_user&.id }.compact_blank
|
||||||
base_payload.merge(extra_payload(job)).compact_blank
|
base_payload.merge(routable_payload(job))
|
||||||
end
|
end
|
||||||
|
|
||||||
def extra_payload(job)
|
# Creating routing information for routable tokens https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/routable_tokens/
|
||||||
|
def routable_payload(job)
|
||||||
{
|
{
|
||||||
scoped_user_id: job.scoped_user&.id,
|
c: Gitlab.config.cell.id,
|
||||||
organization_id: job.project.organization_id
|
o: job.project.organization_id,
|
||||||
}
|
u: job.user_id,
|
||||||
|
p: job.project_id,
|
||||||
|
g: job.project.group&.id
|
||||||
|
}.compact_blank.transform_values { |id| id.to_s(36) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def token_prefix
|
def token_prefix
|
||||||
|
|
@ -101,14 +105,30 @@ module Ci
|
||||||
strong_memoize_attr :scoped_user
|
strong_memoize_attr :scoped_user
|
||||||
|
|
||||||
def cell_id
|
def cell_id
|
||||||
@jwt.payload['cell_id']
|
decode(@jwt.payload['c'])
|
||||||
end
|
end
|
||||||
strong_memoize_attr :cell_id
|
|
||||||
|
|
||||||
def organization
|
def organization_id
|
||||||
job&.project&.organization
|
decode(@jwt.payload['o'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_id
|
||||||
|
decode(@jwt.payload['p'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_id
|
||||||
|
decode(@jwt.payload['u'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_id
|
||||||
|
decode(@jwt.payload['g'])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def decode(encoded_value)
|
||||||
|
encoded_value&.to_i(36)
|
||||||
end
|
end
|
||||||
strong_memoize_attr :organization
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -48,26 +48,26 @@ module Gitlab
|
||||||
'de' => 97,
|
'de' => 97,
|
||||||
'en' => 100,
|
'en' => 100,
|
||||||
'eo' => 0,
|
'eo' => 0,
|
||||||
'es' => 38,
|
'es' => 40,
|
||||||
'fil_PH' => 0,
|
'fil_PH' => 0,
|
||||||
'fr' => 98,
|
'fr' => 97,
|
||||||
'gl_ES' => 0,
|
'gl_ES' => 0,
|
||||||
'id_ID' => 0,
|
'id_ID' => 0,
|
||||||
'it' => 84,
|
'it' => 85,
|
||||||
'ja' => 99,
|
'ja' => 96,
|
||||||
'ko' => 30,
|
'ko' => 47,
|
||||||
'nb_NO' => 16,
|
'nb_NO' => 16,
|
||||||
'nl_NL' => 0,
|
'nl_NL' => 0,
|
||||||
'pl_PL' => 2,
|
'pl_PL' => 2,
|
||||||
'pt_BR' => 92,
|
'pt_BR' => 93,
|
||||||
'ro_RO' => 50,
|
'ro_RO' => 49,
|
||||||
'ru' => 15,
|
'ru' => 54,
|
||||||
'si_LK' => 9,
|
'si_LK' => 9,
|
||||||
'tr_TR' => 6,
|
'tr_TR' => 6,
|
||||||
'uk' => 38,
|
'uk' => 37,
|
||||||
'zh_CN' => 89,
|
'zh_CN' => 86,
|
||||||
'zh_HK' => 1,
|
'zh_HK' => 1,
|
||||||
'zh_TW' => 85
|
'zh_TW' => 81
|
||||||
}.freeze
|
}.freeze
|
||||||
private_constant :TRANSLATION_LEVELS
|
private_constant :TRANSLATION_LEVELS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,12 @@ module Gitlab
|
||||||
def wrap_mentions_in_backticks(text)
|
def wrap_mentions_in_backticks(text)
|
||||||
return text unless text.present?
|
return text unless text.present?
|
||||||
|
|
||||||
if MENTION_REGEX.match?(text)
|
resultant_array = []
|
||||||
text = MENTION_REGEX.replace_gsub(text) do |match|
|
|
||||||
|
split_array = text.split("\n")
|
||||||
|
split_array.each do |line|
|
||||||
|
if MENTION_REGEX.match?(line)
|
||||||
|
line = MENTION_REGEX.replace_gsub(line) do |match|
|
||||||
case match[0]
|
case match[0]
|
||||||
when /^`/
|
when /^`/
|
||||||
match[0]
|
match[0]
|
||||||
|
|
@ -40,7 +44,10 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
text
|
resultant_array << line
|
||||||
|
end
|
||||||
|
|
||||||
|
resultant_array.join("\n")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1021,6 +1021,7 @@ excluded_attributes:
|
||||||
- :pipeline_schedule_id
|
- :pipeline_schedule_id
|
||||||
- :merge_request_id
|
- :merge_request_id
|
||||||
- :external_pull_request_id
|
- :external_pull_request_id
|
||||||
|
- :trigger_id
|
||||||
- :ci_ref_id
|
- :ci_ref_id
|
||||||
- :locked
|
- :locked
|
||||||
pipeline_metadata:
|
pipeline_metadata:
|
||||||
|
|
|
||||||
|
|
@ -21399,6 +21399,9 @@ msgstr ""
|
||||||
msgid "Drop or %{linkStart}upload%{linkEnd} files to attach"
|
msgid "Drop or %{linkStart}upload%{linkEnd} files to attach"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Drop or upload file to attach"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Drop your designs to start your upload."
|
msgid "Drop your designs to start your upload."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -26441,6 +26444,9 @@ msgstr ""
|
||||||
msgid "GitLabPages|Your project is configured for GitLab Pages and the pipeline is running..."
|
msgid "GitLabPages|Your project is configured for GitLab Pages and the pipeline is running..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GitPod"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Gitaly servers"
|
msgid "Gitaly servers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -30991,6 +30997,9 @@ msgstr ""
|
||||||
msgid "Integration|Branches for which notifications are to be sent"
|
msgid "Integration|Branches for which notifications are to be sent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IntelliJ IDEA"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "IntelliJ IDEA (HTTPS)"
|
msgid "IntelliJ IDEA (HTTPS)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -46060,6 +46069,9 @@ msgstr ""
|
||||||
msgid "ProjectsNew|Get started with one of our popular project templates."
|
msgid "ProjectsNew|Get started with one of our popular project templates."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|GitLab project export"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectsNew|Gitea host URL"
|
msgid "ProjectsNew|Gitea host URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -46099,6 +46111,9 @@ msgstr ""
|
||||||
msgid "ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces."
|
msgid "ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|My awesome project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectsNew|New project"
|
msgid "ProjectsNew|New project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -46117,6 +46132,15 @@ msgstr ""
|
||||||
msgid "ProjectsNew|Please enter a valid personal access token."
|
msgid "ProjectsNew|Please enter a valid personal access token."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|Please enter a valid project name."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|Please enter a valid project slug."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|Please upload a valid GitLab project export file."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectsNew|Project Configuration"
|
msgid "ProjectsNew|Project Configuration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -46126,6 +46150,9 @@ msgstr ""
|
||||||
msgid "ProjectsNew|Project name"
|
msgid "ProjectsNew|Project name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|Project slug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectsNew|Projects"
|
msgid "ProjectsNew|Projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -46144,6 +46171,9 @@ msgstr ""
|
||||||
msgid "ProjectsNew|Select a template"
|
msgid "ProjectsNew|Select a template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectsNew|Unable to suggest a path. Please refresh and try again."
|
msgid "ProjectsNew|Unable to suggest a path. Please refresh and try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -46168,6 +46198,9 @@ msgstr ""
|
||||||
msgid "ProjectsNew|https://mycompany.fogbugz.com"
|
msgid "ProjectsNew|https://mycompany.fogbugz.com"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectsNew|my-awesome-project"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Projects|An error occurred deleting the project. Please refresh the page to try again."
|
msgid "Projects|An error occurred deleting the project. Please refresh the page to try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -54438,9 +54471,6 @@ msgstr ""
|
||||||
msgid "Show all activity"
|
msgid "Show all activity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Show all breadcrumbs"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Show all comments"
|
msgid "Show all comments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -63740,6 +63770,9 @@ msgstr ""
|
||||||
msgid "Visit new homepage"
|
msgid "Visit new homepage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Visual Studio Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Visual Studio Code (HTTPS)"
|
msgid "Visual Studio Code (HTTPS)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -536,9 +536,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when requesting triggered job JSON' do
|
context 'when requesting triggered job JSON' do
|
||||||
let(:trigger) { create(:ci_trigger, project: project) }
|
let_it_be(:trigger) { create(:ci_trigger, project: project) }
|
||||||
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
|
let_it_be(:pipeline) { create(:ci_pipeline, project: project, trigger: trigger) }
|
||||||
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
|
let_it_be(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
|
||||||
|
let_it_be(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
|
||||||
let(:user) { developer }
|
let(:user) { developer }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ FactoryBot.define do
|
||||||
status { :created }
|
status { :created }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :triggered do
|
||||||
|
trigger { association :ci_trigger, project_id: project_id }
|
||||||
|
end
|
||||||
|
|
||||||
factory :ci_pipeline do
|
factory :ci_pipeline do
|
||||||
trait :invalid do
|
trait :invalid do
|
||||||
status { :failed }
|
status { :failed }
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,11 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
where(:directory_code_dropdown_updates) do
|
||||||
|
[true, false]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
describe 'with sub-groups' do
|
describe 'with sub-groups' do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||||
|
|
@ -54,6 +59,7 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(vscode_web_ide: true)
|
stub_feature_flags(vscode_web_ide: true)
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||||
|
|
||||||
ide_visit(project)
|
ide_visit(project)
|
||||||
end
|
end
|
||||||
|
|
@ -64,10 +70,12 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id
|
||||||
describe 'with vscode feature flag off' do
|
describe 'with vscode feature flag off' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(vscode_web_ide: false)
|
stub_feature_flags(vscode_web_ide: false)
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||||
|
|
||||||
ide_visit(project)
|
ide_visit(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'legacy Web IDE'
|
it_behaves_like 'legacy Web IDE'
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,8 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
||||||
|
|
||||||
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
|
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
|
||||||
click_link('.gitignore')
|
click_link('.gitignore')
|
||||||
edit_in_web_ide
|
click_button 'Edit'
|
||||||
|
click_link_or_button 'Web IDE'
|
||||||
|
|
||||||
expect_fork_prompt
|
expect_fork_prompt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -443,7 +443,9 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Variables' do
|
describe 'Variables' do
|
||||||
let(:trigger_request) { create(:ci_trigger_request, project_id: project.id) }
|
let(:trigger) { create(:ci_trigger, project: project) }
|
||||||
|
let(:pipeline) { create(:ci_pipeline, trigger: trigger, project: project, sha: project.commit('HEAD').sha) }
|
||||||
|
let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
|
||||||
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
|
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
|
||||||
|
|
||||||
context 'when user is a maintainer' do
|
context 'when user is a maintainer' do
|
||||||
|
|
@ -459,6 +461,11 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when variables are stored in trigger_request' do
|
context 'when variables are stored in trigger_request' do
|
||||||
before do
|
before do
|
||||||
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
||||||
|
|
@ -468,6 +475,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
||||||
|
|
||||||
it_behaves_like 'no reveal button variables behavior'
|
it_behaves_like 'no reveal button variables behavior'
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when variables are stored in pipeline_variables' do
|
context 'when variables are stored in pipeline_variables' do
|
||||||
before do
|
before do
|
||||||
|
|
@ -504,6 +512,11 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when variables are stored in trigger_request' do
|
context 'when variables are stored in trigger_request' do
|
||||||
before do
|
before do
|
||||||
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
||||||
|
|
@ -513,6 +526,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
||||||
|
|
||||||
it_behaves_like 'reveal button variables behavior'
|
it_behaves_like 'reveal button variables behavior'
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when variables are stored in pipeline_variables' do
|
context 'when variables are stored in pipeline_variables' do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ require 'spec_helper'
|
||||||
RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :groups_and_projects do
|
RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :groups_and_projects do
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
let_it_be(:project) { create(:project, :repository, :public) }
|
let_it_be(:project1) { create(:project, :repository, :public) }
|
||||||
|
let_it_be(:project2) { create(:project, :repository, :public) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
@ -17,13 +18,18 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with developer user' do
|
context 'with developer user' do
|
||||||
|
context 'when directory_code_dropdown_updates is true' do
|
||||||
before_all do
|
before_all do
|
||||||
|
project1.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
stub_feature_flags(blob_overflow_menu: false)
|
stub_feature_flags(blob_overflow_menu: false)
|
||||||
project.add_developer(user)
|
stub_feature_flags(directory_code_dropdown_updates: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows all the expected links' do
|
it 'shows all the expected links' do
|
||||||
visit project_path(project)
|
visit project_path(project1)
|
||||||
|
|
||||||
# The navigation bar
|
# The navigation bar
|
||||||
within_testid('super-sidebar') do
|
within_testid('super-sidebar') do
|
||||||
|
|
@ -32,7 +38,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
aggregate_failures 'dropdown links in the navigation bar' do
|
aggregate_failures 'dropdown links in the navigation bar' do
|
||||||
expect(page).to have_link('New issue')
|
expect(page).to have_link('New issue')
|
||||||
expect(page).to have_link('New merge request')
|
expect(page).to have_link('New merge request')
|
||||||
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
|
expect(page).to have_link('New snippet', href: new_project_snippet_path(project1))
|
||||||
end
|
end
|
||||||
|
|
||||||
find_new_menu_toggle.click
|
find_new_menu_toggle.click
|
||||||
|
|
@ -52,14 +58,16 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
end
|
end
|
||||||
|
|
||||||
# The Web IDE
|
# The Web IDE
|
||||||
click_button 'Edit'
|
within_testid('code-dropdown') do
|
||||||
expect(page).to have_button('Web IDE')
|
click_button 'Code'
|
||||||
|
end
|
||||||
|
expect(page).to have_link('Web IDE')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'hides the links when the project is archived' do
|
it 'hides the links when the project is archived' do
|
||||||
project.update!(archived: true)
|
project1.update!(archived: true)
|
||||||
|
|
||||||
visit project_path(project)
|
visit project_path(project1)
|
||||||
|
|
||||||
within_testid('super-sidebar') do
|
within_testid('super-sidebar') do
|
||||||
find_new_menu_toggle.click
|
find_new_menu_toggle.click
|
||||||
|
|
@ -67,7 +75,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
aggregate_failures 'dropdown links' do
|
aggregate_failures 'dropdown links' do
|
||||||
expect(page).not_to have_link('New issue')
|
expect(page).not_to have_link('New issue')
|
||||||
expect(page).not_to have_link('New merge request')
|
expect(page).not_to have_link('New merge request')
|
||||||
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
|
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project1))
|
||||||
end
|
end
|
||||||
|
|
||||||
find_new_menu_toggle.click
|
find_new_menu_toggle.click
|
||||||
|
|
@ -75,7 +83,78 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
|
|
||||||
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
|
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
|
||||||
|
|
||||||
|
within_testid('code-dropdown') do
|
||||||
|
click_button('Code')
|
||||||
expect(page).not_to have_button('Edit')
|
expect(page).not_to have_button('Edit')
|
||||||
|
expect(page).not_to have_link('Web IDE')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when directory_code_dropdown_updates is false' do
|
||||||
|
before_all do
|
||||||
|
project2.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(blob_overflow_menu: false)
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows all the expected links' do
|
||||||
|
visit project_path(project2)
|
||||||
|
|
||||||
|
# The navigation bar
|
||||||
|
within_testid('super-sidebar') do
|
||||||
|
find_new_menu_toggle.click
|
||||||
|
|
||||||
|
aggregate_failures 'dropdown links in the navigation bar' do
|
||||||
|
expect(page).to have_link('New issue')
|
||||||
|
expect(page).to have_link('New merge request')
|
||||||
|
expect(page).to have_link('New snippet', href: new_project_snippet_path(project2))
|
||||||
|
end
|
||||||
|
|
||||||
|
find_new_menu_toggle.click
|
||||||
|
end
|
||||||
|
|
||||||
|
# The dropdown above the tree
|
||||||
|
page.within('.repo-breadcrumb') do
|
||||||
|
find_by_testid('add-to-tree').click
|
||||||
|
|
||||||
|
aggregate_failures 'dropdown links above the repo tree' do
|
||||||
|
expect(page).to have_link('New file')
|
||||||
|
expect(page).to have_button('Upload file')
|
||||||
|
expect(page).to have_button('New directory')
|
||||||
|
expect(page).to have_link('New branch')
|
||||||
|
expect(page).to have_link('New tag')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The Web IDE
|
||||||
|
click_button 'Edit'
|
||||||
|
expect(page).to have_button('Web IDE')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'hides the links when the project is archived' do
|
||||||
|
project2.update!(archived: true)
|
||||||
|
|
||||||
|
visit project_path(project2)
|
||||||
|
|
||||||
|
within_testid('super-sidebar') do
|
||||||
|
find_new_menu_toggle.click
|
||||||
|
|
||||||
|
aggregate_failures 'dropdown links' do
|
||||||
|
expect(page).not_to have_link('New issue')
|
||||||
|
expect(page).not_to have_link('New merge request')
|
||||||
|
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project2))
|
||||||
|
end
|
||||||
|
|
||||||
|
find_new_menu_toggle.click
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
|
||||||
|
expect(page).not_to have_button('Edit')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -90,10 +169,28 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
context 'when directory_code_dropdown_updates is true' do
|
||||||
before do
|
before do
|
||||||
project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
|
stub_feature_flags(directory_code_dropdown_updates: true)
|
||||||
project.add_member(user, user_level)
|
project1.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
|
||||||
visit project_path(project)
|
project1.add_member(user, user_level)
|
||||||
|
visit project_path(project1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates Web IDE link" do
|
||||||
|
within_testid('code-dropdown') do
|
||||||
|
click_button 'Code'
|
||||||
|
end
|
||||||
|
expect(page.has_link?('Web IDE')).to be(expect_ide_link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when directory_code_dropdown_updates is false' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: false)
|
||||||
|
project2.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
|
||||||
|
project2.add_member(user, user_level)
|
||||||
|
visit project_path(project2)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "updates Web IDE link" do
|
it "updates Web IDE link" do
|
||||||
|
|
@ -101,4 +198,5 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,14 @@ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_id
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { create(:project, :repository) }
|
let(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
|
where(:directory_code_dropdown_updates) do
|
||||||
|
[true, false]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(vscode_web_ide: false)
|
stub_feature_flags(vscode_web_ide: false)
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||||
|
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
@ -73,4 +79,5 @@ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_id
|
||||||
|
|
||||||
expect(page).to have_content('folder name')
|
expect(page).to have_content('folder name')
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,14 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { create(:project, :repository) }
|
let(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
|
where(:directory_code_dropdown_updates) do
|
||||||
|
[true, false]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(vscode_web_ide: false)
|
stub_feature_flags(vscode_web_ide: false)
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||||
|
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
@ -26,7 +32,8 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates file in current directory' do
|
it 'creates file in current directory' do
|
||||||
wait_for_requests
|
wait_for_all_requests
|
||||||
|
|
||||||
first('.ide-tree-actions button').click
|
first('.ide-tree-actions button').click
|
||||||
|
|
||||||
page.within('.modal') do
|
page.within('.modal') do
|
||||||
|
|
@ -60,4 +67,5 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do
|
||||||
|
|
||||||
expect(page).to have_content('file name')
|
expect(page).to have_content('file name')
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,10 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
|
||||||
stub_feature_flags(vscode_web_ide: false)
|
stub_feature_flags(vscode_web_ide: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when directory_code_dropdown_updates is enabled' do
|
||||||
it 'opens folder in IDE' do
|
it 'opens folder in IDE' do
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: true)
|
||||||
|
|
||||||
visit project_tree_path(project, File.join('master', 'bar'))
|
visit project_tree_path(project, File.join('master', 'bar'))
|
||||||
ide_visit_from_link
|
ide_visit_from_link
|
||||||
|
|
||||||
|
|
@ -151,6 +154,21 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when directory_code_dropdown_updates is disabled' do
|
||||||
|
it 'opens folder in IDE' do
|
||||||
|
stub_feature_flags(directory_code_dropdown_updates: false)
|
||||||
|
|
||||||
|
visit project_tree_path(project, File.join('master', 'bar'))
|
||||||
|
ide_visit_from_link
|
||||||
|
|
||||||
|
wait_for_all_requests
|
||||||
|
find('.ide-file-list')
|
||||||
|
wait_for_requests
|
||||||
|
expect(page).to have_selector('.is-open', text: 'bar')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'for subgroups' do
|
context 'for subgroups' do
|
||||||
let(:group) { create(:group) }
|
let(:group) { create(:group) }
|
||||||
let(:subgroup) { create(:group, parent: group) }
|
let(:subgroup) { create(:group, parent: group) }
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,6 @@ describe('~/access_tokens/components/access_token_table_app', () => {
|
||||||
initialActiveAccessTokens: defaultActiveAccessTokens,
|
initialActiveAccessTokens: defaultActiveAccessTokens,
|
||||||
noActiveTokensMessage,
|
noActiveTokensMessage,
|
||||||
showRole,
|
showRole,
|
||||||
glFeatures: {
|
|
||||||
patIp: true,
|
|
||||||
},
|
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import { GlAnimatedUploadIcon, GlFormInput } from '@gitlab/ui';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import importFromGitlabExportApp from '~/import/gitlab_project/import_from_gitlab_export_app.vue';
|
||||||
|
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
|
||||||
|
|
||||||
|
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||||
|
|
||||||
|
describe('Import from GitLab export file app', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const createComponent = () => {
|
||||||
|
wrapper = shallowMountExtended(importFromGitlabExportApp, {
|
||||||
|
propsData: {
|
||||||
|
backButtonPath: '/projects/new#import_project',
|
||||||
|
namespaceFullPath: 'root',
|
||||||
|
namespaceId: '1',
|
||||||
|
rootPath: '/',
|
||||||
|
importGitlabProjectPath: 'import/path',
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
GlFormInput,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate);
|
||||||
|
const findForm = () => wrapper.find('form');
|
||||||
|
const findProjectNameInput = () => wrapper.findByTestId('project-name');
|
||||||
|
const findProjectSlugInput = () => wrapper.findByTestId('project-slug');
|
||||||
|
const findDropzoneButton = () => wrapper.findByTestId('dropzone-button');
|
||||||
|
const findDropzoneInput = () => wrapper.findByTestId('dropzone-input');
|
||||||
|
const findAnimatedUploadIcon = () => wrapper.findComponent(GlAnimatedUploadIcon);
|
||||||
|
const findBackButton = () => wrapper.findByTestId('back-button');
|
||||||
|
const findNextButton = () => wrapper.findByTestId('next-button');
|
||||||
|
|
||||||
|
const setProjectName = async (projectName) => {
|
||||||
|
await findProjectNameInput().setValue(projectName);
|
||||||
|
await findProjectNameInput().trigger('blur');
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async () => {
|
||||||
|
const file = new File(['foo'], 'foo.gz', { type: 'application/gzip', size: 1024 });
|
||||||
|
Object.defineProperty(findDropzoneInput().element, 'files', { value: [file] });
|
||||||
|
findDropzoneInput().trigger('change');
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('form', () => {
|
||||||
|
it('renders the multi step form correctly', () => {
|
||||||
|
expect(findMultiStepForm().props()).toMatchObject({
|
||||||
|
currentStep: 3,
|
||||||
|
stepsTotal: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the form element correctly', () => {
|
||||||
|
const form = findForm();
|
||||||
|
|
||||||
|
expect(form.attributes('action')).toBe('import/path');
|
||||||
|
expect(form.find('input[type=hidden][name=authenticity_token]').attributes('value')).toBe(
|
||||||
|
'mock-csrf-token',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not submit the form without requried fields', () => {
|
||||||
|
const submitSpy = jest.spyOn(findForm().element, 'submit');
|
||||||
|
|
||||||
|
findForm().trigger('submit');
|
||||||
|
expect(submitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits the form with valid form data', async () => {
|
||||||
|
const submitSpy = jest.spyOn(findForm().element, 'submit');
|
||||||
|
|
||||||
|
await setProjectName('test project');
|
||||||
|
uploadFile();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
findForm().trigger('submit');
|
||||||
|
|
||||||
|
expect(submitSpy).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validation', () => {
|
||||||
|
it('shows an error message when project name is cleared', async () => {
|
||||||
|
await setProjectName('');
|
||||||
|
|
||||||
|
const formGroup = wrapper.findByTestId('project-name-form-group');
|
||||||
|
expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter a valid project name.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error message when project name starts with invalid characters', async () => {
|
||||||
|
await setProjectName('#test');
|
||||||
|
|
||||||
|
const formGroup = wrapper.findByTestId('project-name-form-group');
|
||||||
|
expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
|
||||||
|
'Name must start with a letter, digit, emoji, or underscore.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error message when project name contains invalid characters', async () => {
|
||||||
|
await setProjectName('test?');
|
||||||
|
|
||||||
|
const formGroup = wrapper.findByTestId('project-name-form-group');
|
||||||
|
expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
|
||||||
|
'Name can contain only lowercase or uppercase letters, digits, emoji, spaces, dots, underscores, dashes, or pluses.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error message when there are no file uploaded', async () => {
|
||||||
|
findForm().trigger('submit');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const formGroup = wrapper.findByTestId('project-file-form-group');
|
||||||
|
expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
|
||||||
|
'Please upload a valid GitLab project export file.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('project slug', () => {
|
||||||
|
it('updates the project slug appropriately when updating project name', async () => {
|
||||||
|
await setProjectName('test project');
|
||||||
|
|
||||||
|
expect(findProjectSlugInput().props('value')).toBe('test-project');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('drop zone', () => {
|
||||||
|
it('renders a drop zone', () => {
|
||||||
|
expect(findDropzoneInput().exists()).toBe(true);
|
||||||
|
expect(findDropzoneButton().text()).toBe('Drop or upload file to attach');
|
||||||
|
expect(findAnimatedUploadIcon().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uploads a file', async () => {
|
||||||
|
await uploadFile();
|
||||||
|
|
||||||
|
expect(findDropzoneButton().text()).toContain('foo.gz');
|
||||||
|
expect(findAnimatedUploadIcon().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('back button', () => {
|
||||||
|
it('renders a back button', () => {
|
||||||
|
expect(findBackButton().attributes('href')).toBe('/projects/new#import_project');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('next button', () => {
|
||||||
|
it('renders a next button', () => {
|
||||||
|
expect(findNextButton().attributes('type')).toBe('submit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
|
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||||
import CodeDropdownIdeItem from '~/repository/components/code_dropdown/code_dropdown_ide_item.vue';
|
import CodeDropdownIdeItem from '~/repository/components/code_dropdown/code_dropdown_ide_item.vue';
|
||||||
|
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
|
||||||
|
|
||||||
|
jest.mock('~/behaviors/shortcuts/shortcuts_toggle', () => ({
|
||||||
|
shouldDisableShortcuts: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
describe('CodeDropdownIdeItem', () => {
|
describe('CodeDropdownIdeItem', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
const { bindInternalEventDocument } = useMockInternalEventsTracking();
|
||||||
|
|
||||||
const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
|
const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
|
||||||
const findAllGlButtons = () => wrapper.findAllComponents(GlButton);
|
const findAllGlButtons = () => wrapper.findAllComponents(GlButton);
|
||||||
const findGlButtonAtIndex = (index) => findAllGlButtons().at(index);
|
const findGlButtonAtIndex = (index) => findAllGlButtons().at(index);
|
||||||
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
|
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
|
||||||
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
|
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
|
||||||
|
const findKbd = () => wrapper.find('kbd');
|
||||||
|
|
||||||
const createComponent = (props = {}) => {
|
const createComponent = (props = {}) => {
|
||||||
wrapper = shallowMount(CodeDropdownIdeItem, {
|
wrapper = shallowMount(CodeDropdownIdeItem, {
|
||||||
|
|
@ -56,6 +64,11 @@ describe('CodeDropdownIdeItem', () => {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: 'button 1',
|
text: 'button 1',
|
||||||
href: '/link 1',
|
href: '/link 1',
|
||||||
|
shortcut: '.',
|
||||||
|
tracking: {
|
||||||
|
action: 'click_consolidated_edit',
|
||||||
|
label: 'web_ide',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -71,9 +84,27 @@ describe('CodeDropdownIdeItem', () => {
|
||||||
expect(dropdownItem.props('item')).toStrictEqual(mockButtonItem);
|
expect(dropdownItem.props('item')).toStrictEqual(mockButtonItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders shortcut if passed in', () => {
|
||||||
|
expect(findKbd().exists()).toBe(true);
|
||||||
|
expect(findKbd().text()).toBe(mockButtonItem.shortcut);
|
||||||
|
});
|
||||||
|
|
||||||
it('closes the dropdown on click', () => {
|
it('closes the dropdown on click', () => {
|
||||||
findDropdownItemAtIndex(0).vm.$emit('action');
|
findDropdownItemAtIndex(0).vm.$emit('action');
|
||||||
expect(wrapper.emitted('close-dropdown')).toStrictEqual([[]]);
|
expect(wrapper.emitted('close-dropdown')).toStrictEqual([[]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls to track events if passed in', () => {
|
||||||
|
const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
|
||||||
|
findDropdownItemAtIndex(0).vm.$emit('action');
|
||||||
|
expect(trackEventSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(trackEventSpy).toHaveBeenCalledWith(
|
||||||
|
'click_consolidated_edit',
|
||||||
|
{
|
||||||
|
label: 'web_ide',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ describe('Compact Code Dropdown coomponent', () => {
|
||||||
const httpUrl = 'http://foo.bar';
|
const httpUrl = 'http://foo.bar';
|
||||||
const httpsUrl = 'https://foo.bar';
|
const httpsUrl = 'https://foo.bar';
|
||||||
const xcodeUrl = 'xcode://foo.bar';
|
const xcodeUrl = 'xcode://foo.bar';
|
||||||
|
const webIdeUrl = 'webIdeUrl://foo.bar';
|
||||||
|
const gitpodUrl = 'gitpodUrl://foo.bar';
|
||||||
const currentPath = null;
|
const currentPath = null;
|
||||||
const directoryDownloadLinks = [
|
const directoryDownloadLinks = [
|
||||||
{ text: 'zip', path: `${httpUrl}/archive.zip` },
|
{ text: 'zip', path: `${httpUrl}/archive.zip` },
|
||||||
|
|
@ -28,6 +30,10 @@ describe('Compact Code Dropdown coomponent', () => {
|
||||||
sshUrl,
|
sshUrl,
|
||||||
httpUrl,
|
httpUrl,
|
||||||
xcodeUrl,
|
xcodeUrl,
|
||||||
|
webIdeUrl,
|
||||||
|
gitpodUrl,
|
||||||
|
showWebIdeButton: true,
|
||||||
|
showGitpodButton: true,
|
||||||
currentPath,
|
currentPath,
|
||||||
directoryDownloadLinks,
|
directoryDownloadLinks,
|
||||||
};
|
};
|
||||||
|
|
@ -116,13 +122,19 @@ describe('Compact Code Dropdown coomponent', () => {
|
||||||
|
|
||||||
describe('ideGroup', () => {
|
describe('ideGroup', () => {
|
||||||
it('should not render if ideGroup is empty', () => {
|
it('should not render if ideGroup is empty', () => {
|
||||||
createComponent({ xcodeUrl: undefined, sshUrl: undefined, httpUrl: undefined });
|
createComponent({
|
||||||
|
xcodeUrl: undefined,
|
||||||
|
sshUrl: undefined,
|
||||||
|
httpUrl: undefined,
|
||||||
|
showWebIdeButton: false,
|
||||||
|
showGitpodButton: false,
|
||||||
|
});
|
||||||
expect(findCodeDropdownIdeItems().exists()).toBe(false);
|
expect(findCodeDropdownIdeItems().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with correct props', () => {
|
it('renders with correct props', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
expect(findCodeDropdownIdeItems()).toHaveLength(3);
|
expect(findCodeDropdownIdeItems()).toHaveLength(5);
|
||||||
|
|
||||||
mockIdeItems.forEach((item, index) => {
|
mockIdeItems.forEach((item, index) => {
|
||||||
const ideItem = findCodeDropdownIdeItemAtIndex(index);
|
const ideItem = findCodeDropdownIdeItemAtIndex(index);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,27 @@
|
||||||
export const mockIdeItems = [
|
export const mockIdeItems = [
|
||||||
|
{
|
||||||
|
extraAttrs: {
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
href: 'webIdeUrl://foo.bar',
|
||||||
|
shortcut: '.',
|
||||||
|
text: 'Web IDE',
|
||||||
|
tracking: {
|
||||||
|
action: 'click_consolidated_edit',
|
||||||
|
label: 'web_ide',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extraAttrs: {
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
href: 'gitpodUrl://foo.bar',
|
||||||
|
text: 'GitPod',
|
||||||
|
tracking: {
|
||||||
|
action: 'click_consolidated_edit',
|
||||||
|
label: 'gitpod',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,10 @@ describe('HeaderArea', () => {
|
||||||
httpUrl: headerAppInjected.httpUrl,
|
httpUrl: headerAppInjected.httpUrl,
|
||||||
kerberosUrl: headerAppInjected.kerberosUrl,
|
kerberosUrl: headerAppInjected.kerberosUrl,
|
||||||
xcodeUrl: headerAppInjected.xcodeUrl,
|
xcodeUrl: headerAppInjected.xcodeUrl,
|
||||||
|
webIdeUrl: headerAppInjected.webIdeUrl,
|
||||||
|
gitpodUrl: headerAppInjected.gitpodUrl,
|
||||||
|
showWebIdeButton: headerAppInjected.showWebIdeButton,
|
||||||
|
showGitpodButton: headerAppInjected.showGitpodButton,
|
||||||
currentPath: defaultMockRoute.params.path,
|
currentPath: defaultMockRoute.params.path,
|
||||||
directoryDownloadLinks: headerAppInjected.downloadLinks,
|
directoryDownloadLinks: headerAppInjected.downloadLinks,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,10 @@ describe('vue_shared/components/web_ide_link', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let trackingSpy;
|
let trackingSpy;
|
||||||
|
|
||||||
function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
|
function createComponent(
|
||||||
|
props,
|
||||||
|
{ mountFn = shallowMountExtended, slots = {}, featureFlagValue = false } = {},
|
||||||
|
) {
|
||||||
const fakeApollo = createMockApollo([
|
const fakeApollo = createMockApollo([
|
||||||
[getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)],
|
[getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)],
|
||||||
]);
|
]);
|
||||||
|
|
@ -122,6 +125,11 @@ describe('vue_shared/components/web_ide_link', () => {
|
||||||
GlDisclosureDropdownItem,
|
GlDisclosureDropdownItem,
|
||||||
},
|
},
|
||||||
apolloProvider: fakeApollo,
|
apolloProvider: fakeApollo,
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
directoryCodeDropdownUpdates: featureFlagValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||||
|
|
@ -205,9 +213,33 @@ describe('vue_shared/components/web_ide_link', () => {
|
||||||
props: { showEditButton: false },
|
props: { showEditButton: false },
|
||||||
expectedActions: [ACTION_WEB_IDE],
|
expectedActions: [ACTION_WEB_IDE],
|
||||||
},
|
},
|
||||||
])('for a set of props', ({ props, expectedActions }) => {
|
{
|
||||||
|
props: {
|
||||||
|
showWebIdeButton: true,
|
||||||
|
showGitpodButton: true,
|
||||||
|
gitpodEnabled: true,
|
||||||
|
isBlob: true,
|
||||||
|
},
|
||||||
|
expectedActions: [
|
||||||
|
{ ...ACTION_WEB_IDE, text: 'Open in Web IDE' },
|
||||||
|
ACTION_EDIT,
|
||||||
|
{ ...ACTION_GITPOD, text: 'Open in Gitpod' },
|
||||||
|
],
|
||||||
|
featureFlagValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: {
|
||||||
|
showWebIdeButton: true,
|
||||||
|
showGitpodButton: true,
|
||||||
|
gitpodEnabled: true,
|
||||||
|
isBlob: false,
|
||||||
|
},
|
||||||
|
expectedActions: [ACTION_EDIT],
|
||||||
|
featureFlagValue: true,
|
||||||
|
},
|
||||||
|
])('for a set of props', ({ props, expectedActions, featureFlagValue }) => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent(props);
|
createComponent(props, { featureFlagValue });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the appropiate actions', () => {
|
it('renders the appropiate actions', () => {
|
||||||
|
|
|
||||||
|
|
@ -89,4 +89,57 @@ RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do
|
||||||
is_expected.to eq("/#{project.full_path}/-/jobs/#{build.id}/artifacts/browse")
|
is_expected.to eq("/#{project.full_path}/-/jobs/#{build.id}/artifacts/browse")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#triggered' do
|
||||||
|
subject { resolve_field(:triggered, build, current_user: user, object_type: described_class) }
|
||||||
|
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
context 'when not triggered' do
|
||||||
|
let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
|
||||||
|
let_it_be(:build) { create(:ci_build, pipeline: pipeline, project: project, user: user) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(build.pipeline).to receive(:trigger_id).and_call_original
|
||||||
|
is_expected.to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(build).to receive(:trigger_request).and_call_original
|
||||||
|
is_expected.to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when triggered' do
|
||||||
|
let_it_be(:trigger) { create(:ci_trigger, project: project) }
|
||||||
|
let_it_be(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
|
||||||
|
let_it_be(:pipeline) { create(:ci_empty_pipeline, trigger: trigger, project: project) }
|
||||||
|
let_it_be(:build) do
|
||||||
|
create(:ci_build, trigger_request: trigger_request, pipeline: pipeline, project: project, user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(build.pipeline).to receive(:trigger_id).and_call_original
|
||||||
|
is_expected.to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(build).to receive(:trigger_request).and_call_original
|
||||||
|
is_expected.to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Types::WorkItems::Widgets::ErrorTracking::StackTraceContextType, feature_category: :team_planning do
|
||||||
|
it 'exposes the expected fields' do
|
||||||
|
expected_fields = %i[line_number line]
|
||||||
|
|
||||||
|
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Types::WorkItems::Widgets::ErrorTracking::StackTraceType, feature_category: :team_planning do
|
||||||
|
it 'exposes the expected fields' do
|
||||||
|
expected_fields = %i[filename absolute_path line_number column_number function context]
|
||||||
|
|
||||||
|
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe GitlabSchema.types['ErrorTrackingStatus'], feature_category: :team_planning do
|
||||||
|
specify { expect(described_class.graphql_name).to eq('ErrorTrackingStatus') }
|
||||||
|
|
||||||
|
describe 'enum values' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:field_name, :field_value) do
|
||||||
|
'SUCCESS' | :success
|
||||||
|
'ERROR' | :error
|
||||||
|
'NOT_FOUND' | :not_found
|
||||||
|
'RETRY' | :retry
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it 'exposes correct available fields' do
|
||||||
|
expect(described_class.values[field_name].value).to eq(field_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Types::WorkItems::Widgets::ErrorTrackingType, feature_category: :team_planning do
|
RSpec.describe Types::WorkItems::Widgets::ErrorTrackingType, feature_category: :team_planning do
|
||||||
it 'exposes the expected fields' do
|
it 'exposes the expected fields' do
|
||||||
expected_fields = %i[type identifier]
|
expected_fields = %i[type identifier stack_trace status]
|
||||||
|
|
||||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -82,45 +82,30 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#group_title' do
|
describe '#push_group_breadcrumbs' do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:nested_group) { create(:group, parent: group) }
|
let_it_be(:nested_group) { create(:group, parent: group) }
|
||||||
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
|
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
|
||||||
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
|
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
|
||||||
|
|
||||||
subject { helper.group_title(very_deep_nested_group) }
|
subject { helper.push_group_breadcrumbs(very_deep_nested_group) }
|
||||||
|
|
||||||
context 'traversal queries' do
|
it 'enqueues the elements in the breadcrumb schema list in the correct order' do
|
||||||
shared_examples 'correct ancestor order' do
|
expect(helper).to receive(:push_to_schema_breadcrumb).with(group.name, group_path(group), nil).ordered
|
||||||
it 'outputs the groups in the correct order' do
|
expect(helper).to receive(:push_to_schema_breadcrumb).with(nested_group.name, group_path(nested_group), nil).ordered
|
||||||
expect(subject)
|
expect(helper).to receive(:push_to_schema_breadcrumb).with(deep_nested_group.name, group_path(deep_nested_group), nil).ordered
|
||||||
.to match(%r{<li.*><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
|
expect(helper).to receive(:push_to_schema_breadcrumb).with(very_deep_nested_group.name, group_path(very_deep_nested_group), nil).ordered
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
very_deep_nested_group.reload # make sure traversal_ids are reloaded
|
|
||||||
end
|
|
||||||
|
|
||||||
include_examples 'correct ancestor order'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'enqueues the elements in the breadcrumb schema list' do
|
|
||||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(group.name, group_path(group), nil)
|
|
||||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(nested_group.name, group_path(nested_group), nil)
|
|
||||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(deep_nested_group.name, group_path(deep_nested_group), nil)
|
|
||||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(very_deep_nested_group.name, group_path(very_deep_nested_group), nil)
|
|
||||||
|
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'avoids N+1 queries' do
|
it 'avoids N+1 queries' do
|
||||||
control = ActiveRecord::QueryRecorder.new do
|
control = ActiveRecord::QueryRecorder.new do
|
||||||
helper.group_title(nested_group)
|
helper.push_group_breadcrumbs(nested_group)
|
||||||
end
|
end
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
helper.group_title(very_deep_nested_group)
|
helper.push_group_breadcrumbs(very_deep_nested_group)
|
||||||
end.not_to exceed_query_limit(control)
|
end.not_to exceed_query_limit(control)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -947,23 +947,28 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#project_title' do
|
describe '#push_project_breadcrumbs' do
|
||||||
subject { helper.project_title(project) }
|
subject { helper.push_project_breadcrumbs(project) }
|
||||||
|
|
||||||
it 'enqueues the elements in the breadcrumb schema list' do
|
it 'enqueues the elements in the breadcrumb schema list in the correct order' do
|
||||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.namespace.name, user_path(project.owner))
|
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.namespace.name, user_path(project.owner)).ordered
|
||||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.name, project_path(project), nil)
|
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.name, project_path(project), nil).ordered
|
||||||
|
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with malicious owner name' do
|
context 'with malicious owner name' do
|
||||||
|
let(:malicious_owner_name) { 'a<a class="fixed-top" href=/api/v4' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(User).to receive(:name).and_return('a<a class="fixed-top" href=/api/v4')
|
allow_any_instance_of(User).to receive(:name).and_return(malicious_owner_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'escapes the malicious owner name' do
|
it 'escapes the malicious owner name' do
|
||||||
expect(subject).not_to include('<a class="fixed-top" href="/api/v4"></a>')
|
expect(helper).not_to receive(:push_to_schema_breadcrumb).with(malicious_owner_name, user_path(project.owner))
|
||||||
|
expect(helper).to receive(:push_to_schema_breadcrumb).with('a', user_path(project.owner))
|
||||||
|
|
||||||
|
subject
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,31 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates member only once when source_xid and entity_type are the same' do
|
||||||
|
member = extracted_data(
|
||||||
|
email: member_user1.email,
|
||||||
|
id: member_user1.id
|
||||||
|
)
|
||||||
|
|
||||||
|
extracted = BulkImports::Pipeline::ExtractedData.new(
|
||||||
|
data: member.data,
|
||||||
|
page_info: { 'has_next_page' => false }
|
||||||
|
)
|
||||||
|
|
||||||
|
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
|
||||||
|
allow(extractor).to receive(:extract).and_return(extracted)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect { pipeline.run }.to change(portable.members, :count).by(1)
|
||||||
|
|
||||||
|
# Run again with exact same configuration
|
||||||
|
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
|
||||||
|
allow(extractor).to receive(:extract).and_return(extracted)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect { pipeline.run }.not_to change(portable.members, :count)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when importer_user_mapping is enabled' do
|
context 'when importer_user_mapping is enabled' do
|
||||||
let!(:import_source_user) do
|
let!(:import_source_user) do
|
||||||
create(:import_source_user,
|
create(:import_source_user,
|
||||||
|
|
@ -202,6 +227,15 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
|
||||||
subject.load(context, member_data)
|
subject.load(context, member_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'removes source_xid and entity_type from data before creating member' do
|
||||||
|
data = member_data.merge('source_xid' => '123', 'entity_type' => 'group')
|
||||||
|
|
||||||
|
expect { pipeline.load(context, data) }.to change(portable.members, :count).by(1)
|
||||||
|
|
||||||
|
created_member = portable.members.last
|
||||||
|
expect(created_member.attributes).not_to include('source_xid', 'entity_type')
|
||||||
|
end
|
||||||
|
|
||||||
context 'when user_id is current user id' do
|
context 'when user_id is current user id' do
|
||||||
it 'does not create new membership' do
|
it 'does not create new membership' do
|
||||||
data = { user_id: user.id }
|
data = { user_id: user.id }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
|
||||||
let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
|
let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:job) { create(:ci_build, user: user) }
|
let_it_be(:job) { create(:ci_build, user: user) }
|
||||||
|
let(:cell_id) { 1 }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(Gitlab::CurrentSettings)
|
allow(Gitlab::CurrentSettings)
|
||||||
|
|
@ -61,11 +62,38 @@ RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
|
||||||
|
|
||||||
subject(:decoded_token) { described_class.decode(encoded_token) }
|
subject(:decoded_token) { described_class.decode(encoded_token) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
|
||||||
|
end
|
||||||
|
|
||||||
context 'with a valid token' do
|
context 'with a valid token' do
|
||||||
|
let(:decoded_payload) { decoded_token.instance_variable_get(:@jwt).payload }
|
||||||
|
let(:expected_payload) do
|
||||||
|
{
|
||||||
|
"c" => cell_id.to_s(36),
|
||||||
|
"o" => job.project.organization_id.to_s(36),
|
||||||
|
"u" => user.id.to_s(36),
|
||||||
|
"p" => job.project_id.to_s(36)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
it 'successfully decodes the token with subject' do
|
it 'successfully decodes the token with subject' do
|
||||||
expect(decoded_token).to be_present
|
expect(decoded_token).to be_present
|
||||||
expect(decoded_token.job).to eq(job)
|
expect(decoded_token.job).to eq(job)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'successfully decodes the token with routable payload' do
|
||||||
|
expect(decoded_payload).to match(a_hash_including(expected_payload))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project belongs to a group' do
|
||||||
|
let_it_be(:job) { create(:ci_build, user: user, project: create(:project, :in_group)) }
|
||||||
|
|
||||||
|
it 'includes group id in routable payload' do
|
||||||
|
expect(decoded_payload)
|
||||||
|
.to match(a_hash_including(expected_payload.merge("g" => job.project.group.id.to_s(36))))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when signing key is not available' do
|
context 'when signing key is not available' do
|
||||||
|
|
@ -184,17 +212,58 @@ RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
|
||||||
let(:encoded_token) { described_class.encode(job) }
|
let(:encoded_token) { described_class.encode(job) }
|
||||||
let(:decoded_token) { described_class.decode(encoded_token) }
|
let(:decoded_token) { described_class.decode(encoded_token) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
|
||||||
|
end
|
||||||
|
|
||||||
it 'encodes the cell_id in the JWT payload' do
|
it 'encodes the cell_id in the JWT payload' do
|
||||||
expect(decoded_token.cell_id).to eq(Gitlab.config.cell.id)
|
expect(decoded_token.cell_id).to eq(cell_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#organization' do
|
describe '#organization_id' do
|
||||||
let(:encoded_token) { described_class.encode(job) }
|
let(:encoded_token) { described_class.encode(job) }
|
||||||
let(:decoded_token) { described_class.decode(encoded_token) }
|
let(:decoded_token) { described_class.decode(encoded_token) }
|
||||||
|
|
||||||
it 'encodes the organization in the JWT payload' do
|
it 'encodes the organization_id in the JWT payload' do
|
||||||
expect(decoded_token.organization).to eq(job.project.organization)
|
expect(decoded_token.organization_id).to eq(job.project.organization_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#project_id' do
|
||||||
|
let(:encoded_token) { described_class.encode(job) }
|
||||||
|
let(:decoded_token) { described_class.decode(encoded_token) }
|
||||||
|
|
||||||
|
it 'encodes the project_id in the JWT payload' do
|
||||||
|
expect(decoded_token.project_id).to eq(job.project_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#user_id' do
|
||||||
|
let(:encoded_token) { described_class.encode(job) }
|
||||||
|
let(:decoded_token) { described_class.decode(encoded_token) }
|
||||||
|
|
||||||
|
it 'encodes the user_id in the JWT payload' do
|
||||||
|
expect(decoded_token.user_id).to eq(job.user_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#group_id' do
|
||||||
|
let(:encoded_token) { described_class.encode(job) }
|
||||||
|
let(:decoded_token) { described_class.decode(encoded_token) }
|
||||||
|
|
||||||
|
context 'when project belongs to a group' do
|
||||||
|
let_it_be(:job) { create(:ci_build, user: user, project: create(:project, :in_group)) }
|
||||||
|
|
||||||
|
it 'encodes the group_id in the JWT payload' do
|
||||||
|
expect(decoded_token.group_id).to eq(job.project.group.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project belongs to a personal namespace' do
|
||||||
|
it 'does not encode the group_id in the JWT payload' do
|
||||||
|
expect(decoded_token.group_id).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,4 +93,13 @@ RSpec.describe Gitlab::Import::UsernameMentionRewriter, feature_category: :impor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the text contains username in the new line' do
|
||||||
|
let(:original_text) { "Hello,\n@username is mentioned here.\nThis is the next line." }
|
||||||
|
let(:expected_text) { "Hello,\n`@username` is mentioned here.\nThis is the next line." }
|
||||||
|
|
||||||
|
it 'wraps the username in backticks and it should be properly formatted in the new line' do
|
||||||
|
expect(instance.wrap_mentions_in_backticks(original_text)).to eq(expected_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -327,8 +327,8 @@ ci_pipelines: &pipeline_definition
|
||||||
- bridges
|
- bridges
|
||||||
- processables
|
- processables
|
||||||
- generic_commit_statuses
|
- generic_commit_statuses
|
||||||
- trigger_requests
|
|
||||||
- trigger
|
- trigger
|
||||||
|
- trigger_requests
|
||||||
- variables
|
- variables
|
||||||
- auto_canceled_by
|
- auto_canceled_by
|
||||||
- auto_canceled_pipelines
|
- auto_canceled_pipelines
|
||||||
|
|
@ -420,6 +420,7 @@ builds:
|
||||||
- resource_group
|
- resource_group
|
||||||
- metadata
|
- metadata
|
||||||
- runner
|
- runner
|
||||||
|
- trigger
|
||||||
- trigger_request
|
- trigger_request
|
||||||
- erased_by
|
- erased_by
|
||||||
- deployment
|
- deployment
|
||||||
|
|
@ -495,6 +496,7 @@ bridges:
|
||||||
- deployment
|
- deployment
|
||||||
- resource_group
|
- resource_group
|
||||||
- metadata
|
- metadata
|
||||||
|
- trigger
|
||||||
- trigger_request
|
- trigger_request
|
||||||
- downstream_pipeline
|
- downstream_pipeline
|
||||||
- upstream_pipeline
|
- upstream_pipeline
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
||||||
let_it_be_with_refind(:pipeline) { create(:ci_pipeline, project: project) }
|
let_it_be_with_refind(:pipeline) { create(:ci_pipeline, project: project) }
|
||||||
|
|
||||||
describe 'associations' do
|
describe 'associations' do
|
||||||
|
it { is_expected.to have_one(:trigger).through(:pipeline) }
|
||||||
it { is_expected.to belong_to(:trigger_request) }
|
it { is_expected.to belong_to(:trigger_request) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -16,7 +17,6 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
||||||
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
|
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
|
||||||
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
|
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
|
||||||
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
|
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
|
||||||
it { is_expected.to delegate_method(:trigger_short_token).to(:trigger_request) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#clone' do
|
describe '#clone' do
|
||||||
|
|
@ -89,7 +89,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
||||||
let(:ignore_accessors) do
|
let(:ignore_accessors) do
|
||||||
%i[type namespace lock_version target_url base_tags trace_sections
|
%i[type namespace lock_version target_url base_tags trace_sections
|
||||||
commit_id deployment erased_by_id project_id project_mirror
|
commit_id deployment erased_by_id project_id project_mirror
|
||||||
runner_id taggings tags trigger_request_id
|
runner_id taggings tags trigger_request_id trigger trigger_id
|
||||||
user_id auto_canceled_by_id retried failure_reason
|
user_id auto_canceled_by_id retried failure_reason
|
||||||
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
|
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
|
||||||
metadata runner_manager_build runner_manager runner_session trace_chunks
|
metadata runner_manager_build runner_manager runner_session trace_chunks
|
||||||
|
|
@ -195,7 +195,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
||||||
Ci::Build.attribute_names.map(&:to_sym) +
|
Ci::Build.attribute_names.map(&:to_sym) +
|
||||||
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
|
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
|
||||||
Ci::Build.reflect_on_all_associations.map(&:name) +
|
Ci::Build.reflect_on_all_associations.map(&:name) +
|
||||||
[:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens, :interruptible]
|
[:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens, :interruptible, :trigger]
|
||||||
|
|
||||||
current_accessors.uniq!
|
current_accessors.uniq!
|
||||||
|
|
||||||
|
|
@ -678,4 +678,26 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#trigger_short_token' do
|
||||||
|
let_it_be(:pipeline) { create(:ci_pipeline, :triggered, project: project) }
|
||||||
|
let_it_be(:stage) { create(:ci_stage, project: project, pipeline: pipeline, name: 'test') }
|
||||||
|
let_it_be(:processable) { create(:ci_build, :triggered, stage_id: stage.id, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it 'delegates to trigger' do
|
||||||
|
expect(processable.trigger).to receive(:short_token)
|
||||||
|
processable.trigger_short_token
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'delegates to trigger_request' do
|
||||||
|
expect(processable.trigger_request).to receive(:trigger_short_token)
|
||||||
|
processable.trigger_short_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,50 @@ RSpec.describe Ci::Trigger, feature_category: :continuous_integration do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#last_used' do
|
||||||
|
let_it_be(:project) { create :project }
|
||||||
|
let_it_be_with_refind(:trigger) { create(:ci_trigger, project: project) }
|
||||||
|
|
||||||
|
subject { trigger.last_used }
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
|
||||||
|
context 'when there is one pipeline' do
|
||||||
|
let_it_be(:pipeline1) { create(:ci_empty_pipeline, trigger: trigger, project: project, created_at: '2025-02-13') }
|
||||||
|
let_it_be(:build1) { create(:ci_build, pipeline: pipeline1, trigger_request: trigger_request1) }
|
||||||
|
let_it_be(:trigger_request1) { create(:ci_trigger_request, trigger: trigger, created_at: '2025-02-12') }
|
||||||
|
|
||||||
|
it { is_expected.to eq(pipeline1.reload.created_at) }
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq(trigger_request1.reload.created_at) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are two pipelines' do
|
||||||
|
let_it_be(:pipeline2) do
|
||||||
|
create(:ci_empty_pipeline, trigger: trigger, project: project, created_at: '2025-02-11')
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:build2) { create(:ci_build, pipeline: pipeline2, trigger_request: trigger_request2) }
|
||||||
|
let_it_be(:trigger_request2) { create(:ci_trigger_request, trigger: trigger, created_at: '2025-02-10') }
|
||||||
|
|
||||||
|
it { is_expected.to eq(pipeline2.reload.created_at) }
|
||||||
|
|
||||||
|
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq(trigger_request2.reload.created_at) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#short_token' do
|
describe '#short_token' do
|
||||||
let(:trigger) { create(:ci_trigger) }
|
let(:trigger) { create(:ci_trigger) }
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue