Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fe2f83b699
commit
addf13e0d0
|
|
@ -1,45 +1,4 @@
|
|||
.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:
|
||||
- .default-retry
|
||||
- .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
|
||||
DOCS_BRANCH: main
|
||||
environment:
|
||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
|
||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
|
||||
auto_stop_in: 2 weeks
|
||||
url: https://new.docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
|
||||
on_stop: review-docs-hugo-cleanup
|
||||
url: https://docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
|
||||
on_stop: review-docs-cleanup
|
||||
before_script:
|
||||
- source ./scripts/utils.sh
|
||||
- install_gitlab_gem
|
||||
|
||||
# Deploy documentation review app by using GitLab Docs Hugo project (gitlab-org/technical-writing/docs-gitlab-com)
|
||||
review-docs-hugo-deploy:
|
||||
extends: .review-docs-hugo
|
||||
# Deploy documentation review app by using GitLab Docs project (gitlab-org/technical-writing/docs-gitlab-com)
|
||||
review-docs-deploy:
|
||||
extends: .review-docs
|
||||
script:
|
||||
- ./scripts/trigger-build.rb docs-hugo deploy
|
||||
|
||||
# Cleanup remote environment of gitlab-org/technical-writing/docs-gitlab-com
|
||||
review-docs-hugo-cleanup:
|
||||
extends: .review-docs-hugo
|
||||
review-docs-cleanup:
|
||||
extends: .review-docs
|
||||
environment:
|
||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
|
||||
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
|
||||
action: stop
|
||||
script:
|
||||
- ./scripts/trigger-build.rb docs-hugo cleanup
|
||||
|
|
|
|||
|
|
@ -1125,12 +1125,9 @@ Gitlab/BoundedContexts:
|
|||
- 'app/models/preloaders/commit_status_preloader.rb'
|
||||
- 'app/models/preloaders/environments/deployment_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/merge_request_diff_preloader.rb'
|
||||
- 'app/models/preloaders/namespace_root_ancestor_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/runner_manager_policy_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/unfold_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/group_variable_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/build_action_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/ci/codequality_mr_diff_report_serializer_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 HelpIcon from '~/vue_shared/components/help_icon/help_icon.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 { EVENT_SUCCESS, FIELDS, FORM_SELECTOR, INITIAL_PAGE, PAGE_SIZE } from './constants';
|
||||
|
||||
|
|
@ -41,7 +40,6 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
|
||||
anchor: 'view-token-usage-information',
|
||||
}),
|
||||
|
|
@ -117,10 +115,6 @@ export default {
|
|||
ignoredFields.push('role');
|
||||
}
|
||||
|
||||
if (!this.glFeatures.patIp) {
|
||||
ignoredFields.push('lastUsedIps');
|
||||
}
|
||||
|
||||
const fields = FIELDS.filter(({ key }) => !ignoredFields.includes(key));
|
||||
|
||||
// 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 { initNewProjectUrlSelect } from '~/projects/new';
|
||||
import { initGitLabImportProjectForm } from '~/import/gitlab_project';
|
||||
|
||||
initNewProjectUrlSelect();
|
||||
initGitLabImportProject();
|
||||
initGitLabImportProjectForm();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
<script>
|
||||
import { GlButton, GlTruncate, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
|
||||
import { PATH_SEPARATOR } from '~/lib/utils/url_utility';
|
||||
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||
import { joinPaths, PATH_SEPARATOR } from '~/lib/utils/url_utility';
|
||||
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import Tracking from '~/tracking';
|
||||
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import { __, s__, n__ } from '~/locale';
|
||||
import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlTruncate,
|
||||
GlCollapsibleListbox,
|
||||
GlIcon,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
apollo: {
|
||||
|
|
@ -49,6 +47,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
toggleAriaLabelledBy: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
groupsOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -155,6 +158,19 @@ export default {
|
|||
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) {
|
||||
this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
|
||||
this.setNamespace({ id, fullPath });
|
||||
|
|
@ -187,31 +203,34 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-collapsible-listbox
|
||||
searchable
|
||||
fluid-width
|
||||
:searching="loading"
|
||||
:items="items"
|
||||
:toggle-text="dropdownText"
|
||||
:no-results-text="$options.i18n.emptySearchResult"
|
||||
class="gl-w-full"
|
||||
@show="trackDropdownShow"
|
||||
@shown="handleDropdownShown"
|
||||
@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>
|
||||
{{ searchSummary }}
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
<div>
|
||||
<gl-collapsible-listbox
|
||||
searchable
|
||||
fluid-width
|
||||
:searching="loading"
|
||||
:items="items"
|
||||
:toggle-text="dropdownText"
|
||||
toggle-class="gl-w-full"
|
||||
:toggle-aria-labelled-by="toggleAriaLabelledBy"
|
||||
:no-results-text="$options.i18n.emptySearchResult"
|
||||
class="project-destination-select gl-w-full gl-max-w-full"
|
||||
@show="trackDropdownShow"
|
||||
@shown="handleDropdownShown"
|
||||
@select="handleDropdownItemClick"
|
||||
@search="onSearch"
|
||||
>
|
||||
<template #search-summary-sr-only>
|
||||
{{ searchSummary }}
|
||||
</template>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -25,20 +25,22 @@ export default () => {
|
|||
const $projectPath = document.querySelector('.js-path-name');
|
||||
const { name, path } = prepareParameters();
|
||||
|
||||
// get the project name from the URL and set it as input value
|
||||
$projectName.value = name;
|
||||
if ($projectName || $projectPath) {
|
||||
// get the project name from the URL and set it as input value
|
||||
$projectName.value = name;
|
||||
|
||||
// get the path url and append it in the input
|
||||
$projectPath.value = path;
|
||||
// get the path url and append it in the input
|
||||
$projectPath.value = path;
|
||||
|
||||
// generate slug when project name changes
|
||||
$projectName.addEventListener('keyup', () => {
|
||||
projectNew.onProjectNameChange($projectName, $projectPath);
|
||||
hasUserDefinedProjectName = $projectName.value.trim().length > 0;
|
||||
});
|
||||
// generate slug when project name changes
|
||||
$projectName.addEventListener('keyup', () => {
|
||||
projectNew.onProjectNameChange($projectName, $projectPath);
|
||||
hasUserDefinedProjectName = $projectName.value.trim().length > 0;
|
||||
});
|
||||
|
||||
// generate project name from the slug if one isn't set
|
||||
$projectPath.addEventListener('keyup', () =>
|
||||
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
|
||||
);
|
||||
// generate project name from the slug if one isn't set
|
||||
$projectPath.addEventListener('keyup', () =>
|
||||
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script>
|
||||
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -7,16 +9,26 @@ export default {
|
|||
GlButtonGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
},
|
||||
mixins: [InternalEvents.mixin()],
|
||||
props: {
|
||||
ideItem: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shortcutsDisabled() {
|
||||
return shouldDisableShortcuts();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDropdown() {
|
||||
this.$emit('close-dropdown');
|
||||
},
|
||||
trackAndClose({ action, label }) {
|
||||
this.trackEvent(action, { label });
|
||||
this.closeDropdown();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -41,5 +53,16 @@ export default {
|
|||
</gl-button>
|
||||
</gl-button-group>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
|
||||
import { getHTTPProtocol } from '~/lib/utils/url_utility';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { GO_TO_PROJECT_WEBIDE, keysFor } from '~/behaviors/shortcuts/keybindings';
|
||||
import CodeDropdownCloneItem from './code_dropdown_clone_item.vue';
|
||||
import CodeDropdownDownloadItems from './code_dropdown_download_items.vue';
|
||||
import CodeDropdownIdeItem from './code_dropdown_ide_item.vue';
|
||||
|
|
@ -36,6 +37,16 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
webIdeUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
gitpodUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
currentPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -46,6 +57,16 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
showWebIdeButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
showGitpodButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
httpLabel() {
|
||||
|
|
@ -58,22 +79,52 @@ export default {
|
|||
httpUrlEncoded() {
|
||||
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() {
|
||||
const groups = [
|
||||
/* eslint-disable-next-line @gitlab/require-i18n-strings */
|
||||
this.createIdeGroup('Visual Studio Code', VSCODE_BASE_URL),
|
||||
this.createIdeGroup('IntelliJ IDEA', JETBRAINS_BASE_URL),
|
||||
];
|
||||
const actions = [];
|
||||
|
||||
if (this.xcodeUrl) {
|
||||
groups.push({
|
||||
/* eslint-disable-next-line @gitlab/require-i18n-strings */
|
||||
text: 'Xcode',
|
||||
href: this.xcodeUrl,
|
||||
});
|
||||
if (this.showWebIdeButton) actions.push(this.webIdeAction);
|
||||
if (this.showGitpodButton) actions.push(this.gitPodAction);
|
||||
|
||||
if (this.httpUrl || this.sshUrl) {
|
||||
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() {
|
||||
return this.directoryDownloadLinks.map((link) => ({
|
||||
|
|
@ -107,7 +158,7 @@ export default {
|
|||
...(this.sshUrl
|
||||
? [
|
||||
{
|
||||
text: 'SSH',
|
||||
text: __('SSH'),
|
||||
href: `${baseUrl}${this.sshUrlEncoded}`,
|
||||
},
|
||||
]
|
||||
|
|
@ -115,7 +166,7 @@ export default {
|
|||
...(this.httpUrl
|
||||
? [
|
||||
{
|
||||
text: 'HTTPS',
|
||||
text: __('HTTPS'),
|
||||
href: `${baseUrl}${this.httpUrlEncoded}`,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -323,8 +323,12 @@ export default {
|
|||
:http-url="httpUrl"
|
||||
:kerberos-url="kerberosUrl"
|
||||
:xcode-url="xcodeUrl"
|
||||
:web-ide-url="webIDEUrl"
|
||||
:gitpod-url="gitpodUrl"
|
||||
:current-path="currentPath"
|
||||
:directory-download-links="downloadLinks"
|
||||
:show-web-ide-button="showWebIdeButton"
|
||||
:show-gitpod-button="showGitpodButton"
|
||||
/>
|
||||
<repository-overflow-menu v-if="comparePath" />
|
||||
</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 SOURCE_TYPE_GROUP = 'group';
|
||||
export const SOURCE_TYPE_PROJECT = 'project';
|
||||
export const SOURCE_TYPE_FILE = 'file';
|
||||
|
||||
export const IMPORT_HISTORY_TABLE_STATUS = {
|
||||
inProgress: 'started',
|
||||
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 { keysFor, GO_TO_PROJECT_WEBIDE } from '~/behaviors/shortcuts/keybindings';
|
||||
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';
|
||||
|
||||
export const i18n = {
|
||||
|
|
@ -32,7 +33,7 @@ export default {
|
|||
ConfirmForkModal,
|
||||
},
|
||||
i18n,
|
||||
mixins: [Tracking.mixin()],
|
||||
mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
|
||||
props: {
|
||||
isFork: {
|
||||
type: Boolean,
|
||||
|
|
@ -141,13 +142,15 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
hideIDEActionsInDirectoryView() {
|
||||
return this.glFeatures.directoryCodeDropdownUpdates && !this.isBlob;
|
||||
},
|
||||
actions() {
|
||||
return [
|
||||
this.pipelineEditorAction,
|
||||
this.webIdeAction,
|
||||
this.editAction,
|
||||
this.gitpodAction,
|
||||
].filter((action) => action);
|
||||
return this.hideIDEActionsInDirectoryView
|
||||
? [this.pipelineEditorAction, this.editAction].filter(Boolean)
|
||||
: [this.pipelineEditorAction, this.webIdeAction, this.editAction, this.gitpodAction].filter(
|
||||
Boolean,
|
||||
);
|
||||
},
|
||||
hasActions() {
|
||||
return this.actions.length > 0;
|
||||
|
|
|
|||
|
|
@ -557,3 +557,8 @@
|
|||
@apply gl-line-clamp-2 gl-whitespace-normal;
|
||||
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
|
||||
|
||||
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) }
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ module Resolvers
|
|||
private
|
||||
|
||||
def unconditional_includes
|
||||
[:trigger_requests]
|
||||
[:trigger_requests, :pipelines]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -268,7 +268,11 @@ module Types
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# rubocop:enable Graphql/AuthorizeTypes
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ module BreadcrumbsHelper
|
|||
@breadcrumb_title = title
|
||||
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)
|
||||
@breadcrumb_collapsed_links ||= {}
|
||||
@breadcrumb_collapsed_links[location] ||= []
|
||||
|
|
|
|||
|
|
@ -41,29 +41,12 @@ module GroupsHelper
|
|||
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
|
||||
end
|
||||
|
||||
def group_title(group)
|
||||
@has_group_title = true
|
||||
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
|
||||
|
||||
def push_group_breadcrumbs(group)
|
||||
sorted_ancestors(group).with_route.reverse_each do |parent|
|
||||
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent), parent.try(:avatar_url))
|
||||
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))
|
||||
|
||||
full_title.join.html_safe
|
||||
end
|
||||
|
||||
def projects_lfs_status(group)
|
||||
|
|
|
|||
|
|
@ -88,14 +88,10 @@ module PageLayoutHelper
|
|||
end
|
||||
|
||||
def header_title(title = nil, title_url = nil)
|
||||
if title
|
||||
@header_title = title
|
||||
@header_title_url = title_url
|
||||
else
|
||||
return @header_title unless @header_title_url
|
||||
return @header_title unless title
|
||||
|
||||
breadcrumb_list_item(link_to(@header_title, @header_title_url))
|
||||
end
|
||||
@header_title = title
|
||||
@header_title_url = title_url
|
||||
end
|
||||
|
||||
def sidebar(name = nil)
|
||||
|
|
|
|||
|
|
@ -103,14 +103,18 @@ module ProjectsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def project_title(project)
|
||||
namespace_link = build_namespace_breadcrumb_link(project)
|
||||
project_link = build_project_breadcrumb_link(project)
|
||||
def push_project_breadcrumbs(project)
|
||||
if project.group
|
||||
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
|
||||
project_link = breadcrumb_list_item project_link
|
||||
push_to_schema_breadcrumb(name, url)
|
||||
end
|
||||
|
||||
"#{namespace_link} #{project_link}".html_safe
|
||||
push_to_schema_breadcrumb(simple_sanitize(project.name), project_path(project), project.try(:avatar_url))
|
||||
end
|
||||
|
||||
def remove_project_message(project)
|
||||
|
|
@ -1071,38 +1075,6 @@ module ProjectsHelper
|
|||
}
|
||||
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?
|
||||
strong_memoize(:delete_inactive_projects_setting) do
|
||||
::Gitlab::CurrentSettings.delete_inactive_projects?
|
||||
|
|
|
|||
|
|
@ -170,20 +170,6 @@ module Ci
|
|||
)
|
||||
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
|
||||
joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
|
||||
.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 :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 :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
|
||||
|
||||
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
|
||||
|
||||
accepts_nested_attributes_for :needs
|
||||
|
||||
scope :preload_needs, -> { preload(:needs) }
|
||||
|
|
@ -268,6 +267,14 @@ module Ci
|
|||
options[:manual_confirmation] if manual_job?
|
||||
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
|
||||
|
||||
def dependencies
|
||||
|
|
|
|||
|
|
@ -37,12 +37,12 @@ module Ci
|
|||
self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
|
||||
end
|
||||
|
||||
def last_trigger_request
|
||||
trigger_requests.last
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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,13 +13,19 @@ module Ci
|
|||
end
|
||||
|
||||
def trigger_variables
|
||||
return [] unless trigger_request
|
||||
|
||||
@trigger_variables ||=
|
||||
if pipeline.variables.any?
|
||||
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
|
||||
return [] if pipeline.trigger_id.blank?
|
||||
|
||||
pipeline.variables.map(&:to_hash_variable)
|
||||
else
|
||||
trigger_request.user_variables
|
||||
return [] unless trigger_request
|
||||
|
||||
if pipeline.variables.any?
|
||||
pipeline.variables.map(&:to_hash_variable)
|
||||
else
|
||||
trigger_request.user_variables
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ class BuildDetailsEntity < Ci::JobEntity
|
|||
raw_project_job_path(project, build)
|
||||
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_variables, as: :variables, using: TriggerVariableEntity
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class PipelineSerializer < BaseSerializer
|
|||
:cancelable_statuses,
|
||||
:retryable_builds,
|
||||
:stages,
|
||||
:trigger,
|
||||
:trigger_requests,
|
||||
:user,
|
||||
(:latest_statuses if preload_statuses),
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ module PersonalAccessTokens
|
|||
end
|
||||
|
||||
def last_used_ip_needs_update?
|
||||
return false unless Feature.enabled?(:pat_ip, @personal_access_token.user)
|
||||
return false unless Gitlab::IpAddressState.current
|
||||
return true if @personal_access_token.last_used_at.nil?
|
||||
|
||||
|
|
|
|||
|
|
@ -2,27 +2,40 @@
|
|||
- header_title _("New project"), new_project_path
|
||||
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
|
||||
|
||||
= render ::Layouts::PageHeadingComponent.new('') do |c|
|
||||
- c.with_heading do
|
||||
%span.gl-inline-flex.gl-items-center.gl-gap-3
|
||||
= sprite_icon('tanuki', size: 32)
|
||||
= _('Import an exported GitLab project')
|
||||
- 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
|
||||
%span.gl-inline-flex.gl-items-center.gl-gap-3
|
||||
= sprite_icon('tanuki', size: 32)
|
||||
= _('Import an exported GitLab project')
|
||||
|
||||
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
|
||||
= render 'import/shared/new_project_form'
|
||||
|
||||
.row
|
||||
.form-group.col-md-12
|
||||
= _("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.")
|
||||
.row
|
||||
.form-group.col-sm-12
|
||||
= label_tag :file, _('GitLab project export'), class: 'label-bold'
|
||||
.form-group
|
||||
= file_field_tag :file, class: ''
|
||||
.row
|
||||
.col-sm-12.gl-mt-5
|
||||
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
|
||||
= _('Import project')
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_path) do
|
||||
= _('Cancel')
|
||||
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
|
||||
= render 'import/shared/new_project_form'
|
||||
|
||||
.row
|
||||
.form-group.col-md-12
|
||||
= _("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.")
|
||||
.row
|
||||
.form-group.col-sm-12
|
||||
= label_tag :file, _('GitLab project export'), class: 'label-bold'
|
||||
.form-group
|
||||
= file_field_tag :file, class: ''
|
||||
.row
|
||||
.col-sm-12.gl-mt-5
|
||||
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
|
||||
= _('Import project')
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_path) do
|
||||
= _('Cancel')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- page_title @group.name
|
||||
- page_description @group.description_html unless page_description
|
||||
- header_title group_title(@group) unless header_title
|
||||
- push_group_breadcrumbs(@group)
|
||||
- nav "group"
|
||||
- display_subscription_banner!
|
||||
- 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_description @project.description_html unless page_description
|
||||
- header_title project_title(@project) unless header_title
|
||||
- push_project_breadcrumbs(@project)
|
||||
- nav "project"
|
||||
- page_itemtype 'http://schema.org/SoftwareSourceCode'
|
||||
- display_subscription_banner!
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
description: "Selects an editor in the Edit dropdown menu"
|
||||
category: default
|
||||
internal_events: true
|
||||
action: click_consolidated_edit
|
||||
extra_properties:
|
||||
identifiers:
|
||||
|
|
@ -14,4 +14,3 @@ tiers:
|
|||
additional_properties:
|
||||
label:
|
||||
description: "The editor selected in the Edit dropdown menu"
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
name: pat_ip
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428577
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161076
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428577
|
||||
milestone: '17.8'
|
||||
group: group::authentication
|
||||
type: beta
|
||||
default_enabled: true
|
||||
name: ci_read_trigger_from_ci_pipeline
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502767
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180728
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/508601
|
||||
milestone: '17.10'
|
||||
group: group::ci platform
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -5,6 +5,5 @@ feature_category: continuous_integration
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163429
|
||||
milestone: '17.4'
|
||||
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'
|
||||
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 >}}
|
||||
|
||||
- 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
|
||||
|
||||
{{< /details >}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ title: Configure GitLab to access GitLab Duo Self-Hosted
|
|||
|
||||
{{< 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
|
||||
|
||||
{{< /details >}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ title: Enable logging for self-hosted models
|
|||
|
||||
{{< 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
|
||||
|
||||
{{< /details >}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ title: GitLab Duo Self-Hosted supported platforms
|
|||
|
||||
{{< 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
|
||||
|
||||
{{< /details >}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ title: Supported GitLab Duo Self-Hosted models and hardware requirements
|
|||
|
||||
{{< 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
|
||||
|
||||
{{< /details >}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ title: Troubleshooting GitLab Duo Self-Hosted
|
|||
|
||||
{{< 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
|
||||
|
||||
{{< /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="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`
|
||||
|
||||
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="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`
|
||||
|
||||
Represents an escalation policy.
|
||||
|
|
@ -39651,9 +39690,22 @@ Represents the error tracking widget.
|
|||
|
||||
| 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. |
|
||||
|
||||
### `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`
|
||||
|
||||
Represents a health status widget.
|
||||
|
|
@ -41402,6 +41454,17 @@ Epic ID wildcard values.
|
|||
| <a id="epicwildcardidany"></a>`ANY` | Any 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`
|
||||
|
||||
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).
|
||||
|
||||
In the following example, the API request retrieves the list of all projects on GitLab host
|
||||
`example.com`:
|
||||
`gitlab.example.com`:
|
||||
|
||||
```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
|
||||
|
|
@ -69,14 +69,14 @@ send the payload body:
|
|||
- Query string:
|
||||
|
||||
```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):
|
||||
|
||||
```shell
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ For example, this script uses a colon:
|
|||
```yaml
|
||||
job:
|
||||
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
|
||||
|
|
@ -41,7 +41,7 @@ if possible:
|
|||
```yaml
|
||||
job:
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ Dashboards support the following filters:
|
|||
|
||||
- **Date range**: Date selector to filter data by date.
|
||||
- **Anonymous users**: Toggle to include or exclude anonymous users from the dataset.
|
||||
- **Project**: Dropdown list to filter data by project.
|
||||
|
||||
#### Dashboard status
|
||||
|
||||
|
|
@ -132,6 +133,8 @@ To create a built-in analytics dashboard:
|
|||
enabled: true
|
||||
dateRange:
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ and thumbs-ups. React with emoji on:
|
|||
|
||||
- [Issues](project/issues/_index.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).
|
||||
- [Objectives and key results](okrs.md).
|
||||
- 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.
|
||||
- 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 made [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/513302) in GitLab 17.10. Feature flag `pat_ip` removed.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ module API
|
|||
authenticate!
|
||||
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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ module API
|
|||
group_projects = projects_for_group_preload(projects_relation)
|
||||
groups = group_projects.map(&:namespace)
|
||||
|
||||
Preloaders::GroupRootAncestorPreloader.new(groups).execute
|
||||
::Namespaces::Preloaders::GroupRootAncestorPreloader.new(groups).execute
|
||||
|
||||
group_projects.each do |project|
|
||||
project.group = project.namespace
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ module BulkImports
|
|||
module Pipelines
|
||||
class MembersPipeline
|
||||
include Pipeline
|
||||
include HexdigestCacheStrategy
|
||||
|
||||
GROUP_MEMBER_RELATIONS = %i[direct inherited shared_from_groups].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
|
||||
|
||||
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
|
||||
|
||||
def load(_context, 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]
|
||||
create_placeholder_membership(data)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module BulkImports
|
|||
|
||||
attr_reader :tracker
|
||||
|
||||
delegate :source_xid, :entity_type, to: :entity
|
||||
|
||||
def initialize(tracker, extra = {})
|
||||
@tracker = tracker
|
||||
@extra = extra
|
||||
|
|
|
|||
|
|
@ -49,15 +49,19 @@ module Ci
|
|||
end
|
||||
|
||||
def build_payload(job)
|
||||
base_payload = { cell_id: Gitlab.config.cell.id }
|
||||
base_payload.merge(extra_payload(job)).compact_blank
|
||||
base_payload = { scoped_user_id: job.scoped_user&.id }.compact_blank
|
||||
base_payload.merge(routable_payload(job))
|
||||
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,
|
||||
organization_id: job.project.organization_id
|
||||
}
|
||||
c: Gitlab.config.cell.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
|
||||
|
||||
def token_prefix
|
||||
|
|
@ -101,14 +105,30 @@ module Ci
|
|||
strong_memoize_attr :scoped_user
|
||||
|
||||
def cell_id
|
||||
@jwt.payload['cell_id']
|
||||
decode(@jwt.payload['c'])
|
||||
end
|
||||
strong_memoize_attr :cell_id
|
||||
|
||||
def organization
|
||||
job&.project&.organization
|
||||
def organization_id
|
||||
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
|
||||
strong_memoize_attr :organization
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,26 +48,26 @@ module Gitlab
|
|||
'de' => 97,
|
||||
'en' => 100,
|
||||
'eo' => 0,
|
||||
'es' => 38,
|
||||
'es' => 40,
|
||||
'fil_PH' => 0,
|
||||
'fr' => 98,
|
||||
'fr' => 97,
|
||||
'gl_ES' => 0,
|
||||
'id_ID' => 0,
|
||||
'it' => 84,
|
||||
'ja' => 99,
|
||||
'ko' => 30,
|
||||
'it' => 85,
|
||||
'ja' => 96,
|
||||
'ko' => 47,
|
||||
'nb_NO' => 16,
|
||||
'nl_NL' => 0,
|
||||
'pl_PL' => 2,
|
||||
'pt_BR' => 92,
|
||||
'ro_RO' => 50,
|
||||
'ru' => 15,
|
||||
'pt_BR' => 93,
|
||||
'ro_RO' => 49,
|
||||
'ru' => 54,
|
||||
'si_LK' => 9,
|
||||
'tr_TR' => 6,
|
||||
'uk' => 38,
|
||||
'zh_CN' => 89,
|
||||
'uk' => 37,
|
||||
'zh_CN' => 86,
|
||||
'zh_HK' => 1,
|
||||
'zh_TW' => 85
|
||||
'zh_TW' => 81
|
||||
}.freeze
|
||||
private_constant :TRANSLATION_LEVELS
|
||||
|
||||
|
|
|
|||
|
|
@ -25,22 +25,29 @@ module Gitlab
|
|||
def wrap_mentions_in_backticks(text)
|
||||
return text unless text.present?
|
||||
|
||||
if MENTION_REGEX.match?(text)
|
||||
text = MENTION_REGEX.replace_gsub(text) do |match|
|
||||
case match[0]
|
||||
when /^`/
|
||||
match[0]
|
||||
when /^ /
|
||||
" `#{match[0].lstrip}`"
|
||||
when /^\(/
|
||||
"(`#{match[0].sub(/^./, '')}`"
|
||||
else
|
||||
"`#{match[0]}`"
|
||||
resultant_array = []
|
||||
|
||||
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]
|
||||
when /^`/
|
||||
match[0]
|
||||
when /^ /
|
||||
" `#{match[0].lstrip}`"
|
||||
when /^\(/
|
||||
"(`#{match[0].sub(/^./, '')}`"
|
||||
else
|
||||
"`#{match[0]}`"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resultant_array << line
|
||||
end
|
||||
|
||||
text
|
||||
resultant_array.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,7 @@ excluded_attributes:
|
|||
- :pipeline_schedule_id
|
||||
- :merge_request_id
|
||||
- :external_pull_request_id
|
||||
- :trigger_id
|
||||
- :ci_ref_id
|
||||
- :locked
|
||||
pipeline_metadata:
|
||||
|
|
|
|||
|
|
@ -21399,6 +21399,9 @@ msgstr ""
|
|||
msgid "Drop or %{linkStart}upload%{linkEnd} files to attach"
|
||||
msgstr ""
|
||||
|
||||
msgid "Drop or upload file to attach"
|
||||
msgstr ""
|
||||
|
||||
msgid "Drop your designs to start your upload."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26441,6 +26444,9 @@ msgstr ""
|
|||
msgid "GitLabPages|Your project is configured for GitLab Pages and the pipeline is running..."
|
||||
msgstr ""
|
||||
|
||||
msgid "GitPod"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gitaly servers"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30991,6 +30997,9 @@ msgstr ""
|
|||
msgid "Integration|Branches for which notifications are to be sent"
|
||||
msgstr ""
|
||||
|
||||
msgid "IntelliJ IDEA"
|
||||
msgstr ""
|
||||
|
||||
msgid "IntelliJ IDEA (HTTPS)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46060,6 +46069,9 @@ msgstr ""
|
|||
msgid "ProjectsNew|Get started with one of our popular project templates."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|GitLab project export"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Gitea host URL"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|My awesome project"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|New project"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46117,6 +46132,15 @@ msgstr ""
|
|||
msgid "ProjectsNew|Please enter a valid personal access token."
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46126,6 +46150,9 @@ msgstr ""
|
|||
msgid "ProjectsNew|Project name"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Project slug"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Projects"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46144,6 +46171,9 @@ msgstr ""
|
|||
msgid "ProjectsNew|Select a template"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46168,6 +46198,9 @@ msgstr ""
|
|||
msgid "ProjectsNew|https://mycompany.fogbugz.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|my-awesome-project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Projects|An error occurred deleting the project. Please refresh the page to try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54438,9 +54471,6 @@ msgstr ""
|
|||
msgid "Show all activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all breadcrumbs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all comments"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63740,6 +63770,9 @@ msgstr ""
|
|||
msgid "Visit new homepage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Visual Studio Code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Visual Studio Code (HTTPS)"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -536,9 +536,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
|
|||
end
|
||||
|
||||
context 'when requesting triggered job JSON' do
|
||||
let(:trigger) { create(:ci_trigger, project: project) }
|
||||
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
|
||||
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
|
||||
let_it_be(:trigger) { create(:ci_trigger, project: project) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project, trigger: trigger) }
|
||||
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 }
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ FactoryBot.define do
|
|||
status { :created }
|
||||
end
|
||||
|
||||
trait :triggered do
|
||||
trigger { association :ci_trigger, project_id: project_id }
|
||||
end
|
||||
|
||||
factory :ci_pipeline do
|
||||
trait :invalid do
|
||||
status { :failed }
|
||||
|
|
|
|||
|
|
@ -45,29 +45,37 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id
|
|||
end
|
||||
end
|
||||
|
||||
describe 'with sub-groups' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
|
||||
|
||||
let(:project) { subgroup_project }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: true)
|
||||
|
||||
ide_visit(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'new Web IDE'
|
||||
where(:directory_code_dropdown_updates) do
|
||||
[true, false]
|
||||
end
|
||||
|
||||
describe 'with vscode feature flag off' do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
with_them do
|
||||
describe 'with sub-groups' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
|
||||
|
||||
ide_visit(project)
|
||||
let(:project) { subgroup_project }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: true)
|
||||
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||
|
||||
ide_visit(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'new Web IDE'
|
||||
end
|
||||
|
||||
it_behaves_like 'legacy Web IDE'
|
||||
describe 'with vscode feature flag off' do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||
|
||||
ide_visit(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'legacy Web IDE'
|
||||
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
|
||||
click_link('.gitignore')
|
||||
edit_in_web_ide
|
||||
click_button 'Edit'
|
||||
click_link_or_button 'Web IDE'
|
||||
|
||||
expect_fork_prompt
|
||||
|
||||
|
|
|
|||
|
|
@ -443,7 +443,9 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
|||
end
|
||||
|
||||
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) }
|
||||
|
||||
context 'when user is a maintainer' do
|
||||
|
|
@ -459,14 +461,20 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
|||
end
|
||||
end
|
||||
|
||||
context 'when variables are stored in trigger_request' do
|
||||
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||
before do
|
||||
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
||||
|
||||
visit project_job_path(project, job)
|
||||
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'no reveal button variables behavior'
|
||||
context 'when variables are stored in trigger_request' do
|
||||
before do
|
||||
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
||||
|
||||
visit project_job_path(project, job)
|
||||
end
|
||||
|
||||
it_behaves_like 'no reveal button variables behavior'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when variables are stored in pipeline_variables' do
|
||||
|
|
@ -504,14 +512,20 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
|
|||
end
|
||||
end
|
||||
|
||||
context 'when variables are stored in trigger_request' do
|
||||
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
|
||||
before do
|
||||
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
||||
|
||||
visit project_job_path(project, job)
|
||||
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'reveal button variables behavior'
|
||||
context 'when variables are stored in trigger_request' do
|
||||
before do
|
||||
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
|
||||
|
||||
visit project_job_path(project, job)
|
||||
end
|
||||
|
||||
it_behaves_like 'reveal button variables behavior'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when variables are stored in pipeline_variables' do
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :groups_and_projects do
|
||||
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) }
|
||||
|
||||
before do
|
||||
|
|
@ -17,65 +18,143 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
|||
end
|
||||
|
||||
context 'with developer user' do
|
||||
before_all do
|
||||
stub_feature_flags(blob_overflow_menu: false)
|
||||
project.add_developer(user)
|
||||
context 'when directory_code_dropdown_updates is true' do
|
||||
before_all do
|
||||
project1.add_developer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(blob_overflow_menu: false)
|
||||
stub_feature_flags(directory_code_dropdown_updates: true)
|
||||
end
|
||||
|
||||
it 'shows all the expected links' do
|
||||
visit project_path(project1)
|
||||
|
||||
# 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(project1))
|
||||
end
|
||||
|
||||
find_new_menu_toggle.click
|
||||
end
|
||||
|
||||
# The dropdown above the tree
|
||||
page.within('.tree-controls') do
|
||||
find('.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
|
||||
within_testid('code-dropdown') do
|
||||
click_button 'Code'
|
||||
end
|
||||
expect(page).to have_link('Web IDE')
|
||||
end
|
||||
|
||||
it 'hides the links when the project is archived' do
|
||||
project1.update!(archived: true)
|
||||
|
||||
visit project_path(project1)
|
||||
|
||||
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(project1))
|
||||
end
|
||||
|
||||
find_new_menu_toggle.click
|
||||
end
|
||||
|
||||
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_link('Web IDE')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows all the expected links' do
|
||||
visit project_path(project)
|
||||
|
||||
# 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(project))
|
||||
end
|
||||
|
||||
find_new_menu_toggle.click
|
||||
context 'when directory_code_dropdown_updates is false' do
|
||||
before_all do
|
||||
project2.add_developer(user)
|
||||
end
|
||||
|
||||
# The dropdown above the tree
|
||||
page.within('.tree-controls') do
|
||||
find('.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
|
||||
before do
|
||||
stub_feature_flags(blob_overflow_menu: false)
|
||||
stub_feature_flags(directory_code_dropdown_updates: false)
|
||||
end
|
||||
|
||||
# The Web IDE
|
||||
click_button 'Edit'
|
||||
expect(page).to have_button('Web IDE')
|
||||
end
|
||||
it 'shows all the expected links' do
|
||||
visit project_path(project2)
|
||||
|
||||
it 'hides the links when the project is archived' do
|
||||
project.update!(archived: true)
|
||||
# The navigation bar
|
||||
within_testid('super-sidebar') do
|
||||
find_new_menu_toggle.click
|
||||
|
||||
visit project_path(project)
|
||||
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
|
||||
|
||||
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(project))
|
||||
find_new_menu_toggle.click
|
||||
end
|
||||
|
||||
find_new_menu_toggle.click
|
||||
# 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
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
|
||||
it 'hides the links when the project is archived' do
|
||||
project2.update!(archived: true)
|
||||
|
||||
expect(page).not_to have_button('Edit')
|
||||
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
|
||||
|
||||
|
|
@ -90,14 +169,33 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
|
|||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
|
||||
project.add_member(user, user_level)
|
||||
visit project_path(project)
|
||||
context 'when directory_code_dropdown_updates is true' do
|
||||
before do
|
||||
stub_feature_flags(directory_code_dropdown_updates: true)
|
||||
project1.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
|
||||
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
|
||||
|
||||
it "updates Web IDE link" do
|
||||
expect(page.has_button?('Edit')).to be(expect_ide_link)
|
||||
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
|
||||
|
||||
it "updates Web IDE link" do
|
||||
expect(page.has_button?('Edit')).to be(expect_ide_link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,69 +8,76 @@ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_id
|
|||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit project_tree_path(project, :master)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
ide_visit_from_link
|
||||
where(:directory_code_dropdown_updates) do
|
||||
[true, false]
|
||||
end
|
||||
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||
|
||||
it 'creates directory in current directory' do
|
||||
wait_for_all_requests
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
all('.ide-tree-actions button').last.click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('folder name')
|
||||
|
||||
click_button('Create directory')
|
||||
end
|
||||
|
||||
expect(page).to have_content('folder name')
|
||||
|
||||
first('.ide-tree-actions button').click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('folder name/file name')
|
||||
|
||||
click_button('Create file')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.js-ide-commit-mode').click
|
||||
|
||||
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
|
||||
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
|
||||
# taller (as it is by default with chrome headless) then the button will not exist.
|
||||
if page.has_css?('[data-testid="begin-commit-button"]')
|
||||
find_by_testid('begin-commit-button').click
|
||||
end
|
||||
|
||||
fill_in('commit-message', with: 'commit message ide')
|
||||
|
||||
find(:css, ".js-ide-commit-new-mr input").set(false)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within '.multi-file-commit-form' do
|
||||
click_button('Commit')
|
||||
visit project_tree_path(project, :master)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
ide_visit_from_link
|
||||
end
|
||||
|
||||
find('.js-ide-edit-mode').click
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
|
||||
expect(page).to have_content('folder name')
|
||||
it 'creates directory in current directory' do
|
||||
wait_for_all_requests
|
||||
|
||||
all('.ide-tree-actions button').last.click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('folder name')
|
||||
|
||||
click_button('Create directory')
|
||||
end
|
||||
|
||||
expect(page).to have_content('folder name')
|
||||
|
||||
first('.ide-tree-actions button').click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('folder name/file name')
|
||||
|
||||
click_button('Create file')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.js-ide-commit-mode').click
|
||||
|
||||
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
|
||||
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
|
||||
# taller (as it is by default with chrome headless) then the button will not exist.
|
||||
if page.has_css?('[data-testid="begin-commit-button"]')
|
||||
find_by_testid('begin-commit-button').click
|
||||
end
|
||||
|
||||
fill_in('commit-message', with: 'commit message ide')
|
||||
|
||||
find(:css, ".js-ide-commit-new-mr input").set(false)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within '.multi-file-commit-form' do
|
||||
click_button('Commit')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
find('.js-ide-edit-mode').click
|
||||
|
||||
expect(page).to have_content('folder name')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,56 +8,64 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do
|
|||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit project_path(project)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
ide_visit_from_link
|
||||
where(:directory_code_dropdown_updates) do
|
||||
[true, false]
|
||||
end
|
||||
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(vscode_web_ide: false)
|
||||
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
|
||||
|
||||
it 'creates file in current directory' do
|
||||
wait_for_requests
|
||||
first('.ide-tree-actions button').click
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('file name')
|
||||
|
||||
click_button('Create file')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.js-ide-commit-mode').click
|
||||
|
||||
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
|
||||
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
|
||||
# taller (as it is by default with chrome headless) then the button will not exist.
|
||||
if page.has_css?('[data-testid="begin-commit-button"]')
|
||||
find_by_testid('begin-commit-button').click
|
||||
end
|
||||
|
||||
fill_in('commit-message', with: 'commit message ide')
|
||||
|
||||
find(:css, ".js-ide-commit-new-mr input").set(false)
|
||||
|
||||
page.within '.multi-file-commit-form' do
|
||||
click_button('Commit')
|
||||
visit project_path(project)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
ide_visit_from_link
|
||||
end
|
||||
|
||||
find('.js-ide-edit-mode').click
|
||||
after do
|
||||
set_cookie('new_repo', 'false')
|
||||
end
|
||||
|
||||
expect(page).to have_content('file name')
|
||||
it 'creates file in current directory' do
|
||||
wait_for_all_requests
|
||||
|
||||
first('.ide-tree-actions button').click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.form-control').set('file name')
|
||||
|
||||
click_button('Create file')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.js-ide-commit-mode').click
|
||||
|
||||
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
|
||||
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
|
||||
# taller (as it is by default with chrome headless) then the button will not exist.
|
||||
if page.has_css?('[data-testid="begin-commit-button"]')
|
||||
find_by_testid('begin-commit-button').click
|
||||
end
|
||||
|
||||
fill_in('commit-message', with: 'commit message ide')
|
||||
|
||||
find(:css, ".js-ide-commit-new-mr input").set(false)
|
||||
|
||||
page.within '.multi-file-commit-form' do
|
||||
click_button('Commit')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
find('.js-ide-edit-mode').click
|
||||
|
||||
expect(page).to have_content('file name')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -140,14 +140,32 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
|
|||
stub_feature_flags(vscode_web_ide: false)
|
||||
end
|
||||
|
||||
it 'opens folder in IDE' do
|
||||
visit project_tree_path(project, File.join('master', 'bar'))
|
||||
ide_visit_from_link
|
||||
context 'when directory_code_dropdown_updates is enabled' do
|
||||
it 'opens folder in IDE' do
|
||||
stub_feature_flags(directory_code_dropdown_updates: true)
|
||||
|
||||
wait_for_all_requests
|
||||
find('.ide-file-list')
|
||||
wait_for_requests
|
||||
expect(page).to have_selector('.is-open', text: 'bar')
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -62,9 +62,6 @@ describe('~/access_tokens/components/access_token_table_app', () => {
|
|||
initialActiveAccessTokens: defaultActiveAccessTokens,
|
||||
noActiveTokensMessage,
|
||||
showRole,
|
||||
glFeatures: {
|
||||
patIp: true,
|
||||
},
|
||||
...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 { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
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', () => {
|
||||
let wrapper;
|
||||
|
||||
const { bindInternalEventDocument } = useMockInternalEventsTracking();
|
||||
|
||||
const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
|
||||
const findAllGlButtons = () => wrapper.findAllComponents(GlButton);
|
||||
const findGlButtonAtIndex = (index) => findAllGlButtons().at(index);
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
|
||||
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
|
||||
const findKbd = () => wrapper.find('kbd');
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(CodeDropdownIdeItem, {
|
||||
|
|
@ -56,6 +64,11 @@ describe('CodeDropdownIdeItem', () => {
|
|||
type: 'button',
|
||||
text: 'button 1',
|
||||
href: '/link 1',
|
||||
shortcut: '.',
|
||||
tracking: {
|
||||
action: 'click_consolidated_edit',
|
||||
label: 'web_ide',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -71,9 +84,27 @@ describe('CodeDropdownIdeItem', () => {
|
|||
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', () => {
|
||||
findDropdownItemAtIndex(0).vm.$emit('action');
|
||||
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 httpsUrl = 'https://foo.bar';
|
||||
const xcodeUrl = 'xcode://foo.bar';
|
||||
const webIdeUrl = 'webIdeUrl://foo.bar';
|
||||
const gitpodUrl = 'gitpodUrl://foo.bar';
|
||||
const currentPath = null;
|
||||
const directoryDownloadLinks = [
|
||||
{ text: 'zip', path: `${httpUrl}/archive.zip` },
|
||||
|
|
@ -28,6 +30,10 @@ describe('Compact Code Dropdown coomponent', () => {
|
|||
sshUrl,
|
||||
httpUrl,
|
||||
xcodeUrl,
|
||||
webIdeUrl,
|
||||
gitpodUrl,
|
||||
showWebIdeButton: true,
|
||||
showGitpodButton: true,
|
||||
currentPath,
|
||||
directoryDownloadLinks,
|
||||
};
|
||||
|
|
@ -116,13 +122,19 @@ describe('Compact Code Dropdown coomponent', () => {
|
|||
|
||||
describe('ideGroup', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('renders with correct props', () => {
|
||||
createComponent();
|
||||
expect(findCodeDropdownIdeItems()).toHaveLength(3);
|
||||
expect(findCodeDropdownIdeItems()).toHaveLength(5);
|
||||
|
||||
mockIdeItems.forEach((item, index) => {
|
||||
const ideItem = findCodeDropdownIdeItemAtIndex(index);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,27 @@
|
|||
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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -183,6 +183,10 @@ describe('HeaderArea', () => {
|
|||
httpUrl: headerAppInjected.httpUrl,
|
||||
kerberosUrl: headerAppInjected.kerberosUrl,
|
||||
xcodeUrl: headerAppInjected.xcodeUrl,
|
||||
webIdeUrl: headerAppInjected.webIdeUrl,
|
||||
gitpodUrl: headerAppInjected.gitpodUrl,
|
||||
showWebIdeButton: headerAppInjected.showWebIdeButton,
|
||||
showGitpodButton: headerAppInjected.showGitpodButton,
|
||||
currentPath: defaultMockRoute.params.path,
|
||||
directoryDownloadLinks: headerAppInjected.downloadLinks,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
||||
function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
|
||||
function createComponent(
|
||||
props,
|
||||
{ mountFn = shallowMountExtended, slots = {}, featureFlagValue = false } = {},
|
||||
) {
|
||||
const fakeApollo = createMockApollo([
|
||||
[getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)],
|
||||
]);
|
||||
|
|
@ -122,6 +125,11 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
GlDisclosureDropdownItem,
|
||||
},
|
||||
apolloProvider: fakeApollo,
|
||||
provide: {
|
||||
glFeatures: {
|
||||
directoryCodeDropdownUpdates: featureFlagValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
|
|
@ -205,9 +213,33 @@ describe('vue_shared/components/web_ide_link', () => {
|
|||
props: { showEditButton: false },
|
||||
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(() => {
|
||||
createComponent(props);
|
||||
createComponent(props, { featureFlagValue });
|
||||
});
|
||||
|
||||
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")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -82,45 +82,30 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#group_title' do
|
||||
describe '#push_group_breadcrumbs' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:nested_group) { create(:group, parent: 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) }
|
||||
|
||||
subject { helper.group_title(very_deep_nested_group) }
|
||||
subject { helper.push_group_breadcrumbs(very_deep_nested_group) }
|
||||
|
||||
context 'traversal queries' do
|
||||
shared_examples 'correct ancestor order' do
|
||||
it 'outputs the groups in the correct order' do
|
||||
expect(subject)
|
||||
.to match(%r{<li.*><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
|
||||
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)
|
||||
it 'enqueues the elements in the breadcrumb schema list in the correct order' do
|
||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(group.name, group_path(group), nil).ordered
|
||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(nested_group.name, group_path(nested_group), nil).ordered
|
||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(deep_nested_group.name, group_path(deep_nested_group), nil).ordered
|
||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(very_deep_nested_group.name, group_path(very_deep_nested_group), nil).ordered
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'avoids N+1 queries' do
|
||||
control = ActiveRecord::QueryRecorder.new do
|
||||
helper.group_title(nested_group)
|
||||
helper.push_group_breadcrumbs(nested_group)
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -947,23 +947,28 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#project_title' do
|
||||
subject { helper.project_title(project) }
|
||||
describe '#push_project_breadcrumbs' do
|
||||
subject { helper.push_project_breadcrumbs(project) }
|
||||
|
||||
it 'enqueues the elements in the breadcrumb schema list' 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.name, project_path(project), nil)
|
||||
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)).ordered
|
||||
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.name, project_path(project), nil).ordered
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'with malicious owner name' do
|
||||
let(:malicious_owner_name) { 'a<a class="fixed-top" href=/api/v4' }
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -75,6 +75,31 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
|
|||
)
|
||||
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
|
||||
let!(:import_source_user) do
|
||||
create(:import_source_user,
|
||||
|
|
@ -202,6 +227,15 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
|
|||
subject.load(context, member_data)
|
||||
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
|
||||
it 'does not create new membership' do
|
||||
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(:user) { create(:user) }
|
||||
let_it_be(:job) { create(:ci_build, user: user) }
|
||||
let(:cell_id) { 1 }
|
||||
|
||||
before do
|
||||
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) }
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
|
||||
end
|
||||
|
||||
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
|
||||
expect(decoded_token).to be_present
|
||||
expect(decoded_token.job).to eq(job)
|
||||
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
|
||||
|
||||
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(: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
|
||||
expect(decoded_token.cell_id).to eq(Gitlab.config.cell.id)
|
||||
expect(decoded_token.cell_id).to eq(cell_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#organization' do
|
||||
describe '#organization_id' do
|
||||
let(:encoded_token) { described_class.encode(job) }
|
||||
let(:decoded_token) { described_class.decode(encoded_token) }
|
||||
|
||||
it 'encodes the organization in the JWT payload' do
|
||||
expect(decoded_token.organization).to eq(job.project.organization)
|
||||
it 'encodes the organization_id in the JWT payload' do
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -93,4 +93,13 @@ RSpec.describe Gitlab::Import::UsernameMentionRewriter, feature_category: :impor
|
|||
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
|
||||
|
|
|
|||
|
|
@ -327,8 +327,8 @@ ci_pipelines: &pipeline_definition
|
|||
- bridges
|
||||
- processables
|
||||
- generic_commit_statuses
|
||||
- trigger_requests
|
||||
- trigger
|
||||
- trigger_requests
|
||||
- variables
|
||||
- auto_canceled_by
|
||||
- auto_canceled_pipelines
|
||||
|
|
@ -420,6 +420,7 @@ builds:
|
|||
- resource_group
|
||||
- metadata
|
||||
- runner
|
||||
- trigger
|
||||
- trigger_request
|
||||
- erased_by
|
||||
- deployment
|
||||
|
|
@ -495,6 +496,7 @@ bridges:
|
|||
- deployment
|
||||
- resource_group
|
||||
- metadata
|
||||
- trigger
|
||||
- trigger_request
|
||||
- downstream_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) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to have_one(:trigger).through(:pipeline) }
|
||||
it { is_expected.to belong_to(:trigger_request) }
|
||||
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_ref?).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
|
||||
|
||||
describe '#clone' do
|
||||
|
|
@ -89,7 +89,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
|||
let(:ignore_accessors) do
|
||||
%i[type namespace lock_version target_url base_tags trace_sections
|
||||
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
|
||||
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
|
||||
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_aliases.keys.map(&:to_sym) +
|
||||
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!
|
||||
|
||||
|
|
@ -678,4 +678,26 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,50 @@ RSpec.describe Ci::Trigger, feature_category: :continuous_integration do
|
|||
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
|
||||
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