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> | ||||
|   <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="gl-w-full" | ||||
|       class="project-destination-select gl-w-full gl-max-w-full" | ||||
|       @show="trackDropdownShow" | ||||
|       @shown="handleDropdownShown" | ||||
|       @select="handleDropdownItemClick" | ||||
|       @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> | ||||
| 
 | ||||
|     <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,6 +25,7 @@ export default () => { | |||
|   const $projectPath = document.querySelector('.js-path-name'); | ||||
|   const { name, path } = prepareParameters(); | ||||
| 
 | ||||
|   if ($projectName || $projectPath) { | ||||
|     // get the project name from the URL and set it as input value
 | ||||
|     $projectName.value = name; | ||||
| 
 | ||||
|  | @ -41,4 +42,5 @@ export default () => { | |||
|     $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 | ||||
|     return @header_title unless title | ||||
| 
 | ||||
|     @header_title     = title | ||||
|     @header_title_url = title_url | ||||
|     else | ||||
|       return @header_title unless @header_title_url | ||||
| 
 | ||||
|       breadcrumb_list_item(link_to(@header_title, @header_title_url)) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   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,15 +13,21 @@ module Ci | |||
|     end | ||||
| 
 | ||||
|     def trigger_variables | ||||
|       @trigger_variables ||= | ||||
|         if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project) | ||||
|           return [] if pipeline.trigger_id.blank? | ||||
| 
 | ||||
|           pipeline.variables.map(&:to_hash_variable) | ||||
|         else | ||||
|           return [] unless trigger_request | ||||
| 
 | ||||
|       @trigger_variables ||= | ||||
|           if pipeline.variables.any? | ||||
|             pipeline.variables.map(&:to_hash_variable) | ||||
|           else | ||||
|             trigger_request.user_variables | ||||
|           end | ||||
|         end | ||||
|     end | ||||
| 
 | ||||
|     def execute_in | ||||
|       scheduled? && scheduled_at && [0, scheduled_at - Time.now].max | ||||
|  |  | |||
|  | @ -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,13 +2,27 @@ | |||
| - 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| | ||||
| - 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 | ||||
|   = form_tag import_gitlab_project_path, class: 'new_project', multipart: true do | ||||
|     = render 'import/shared/new_project_form' | ||||
| 
 | ||||
|     .row | ||||
|  | @ -25,4 +39,3 @@ | |||
|           = _('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,8 +25,12 @@ 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| | ||||
|         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] | ||||
|  | @ -40,7 +44,10 @@ module Gitlab | |||
|             end | ||||
|           end | ||||
| 
 | ||||
|         text | ||||
|           resultant_array << line | ||||
|         end | ||||
| 
 | ||||
|         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,6 +45,11 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   where(:directory_code_dropdown_updates) do | ||||
|     [true, false] | ||||
|   end | ||||
| 
 | ||||
|   with_them do | ||||
|     describe 'with sub-groups' do | ||||
|       let_it_be(:group) { create(:group) } | ||||
|       let_it_be(:subgroup) { create(:group, parent: group) } | ||||
|  | @ -54,6 +59,7 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id | |||
| 
 | ||||
|       before do | ||||
|         stub_feature_flags(vscode_web_ide: true) | ||||
|         stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates) | ||||
| 
 | ||||
|         ide_visit(project) | ||||
|       end | ||||
|  | @ -64,10 +70,12 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id | |||
|     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,6 +461,11 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do | ||||
|           before do | ||||
|             stub_feature_flags(ci_read_trigger_from_ci_pipeline: false) | ||||
|           end | ||||
| 
 | ||||
|           context 'when variables are stored in trigger_request' do | ||||
|             before do | ||||
|               trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' }) | ||||
|  | @ -468,6 +475,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou | |||
| 
 | ||||
|             it_behaves_like 'no reveal button variables behavior' | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when variables are stored in pipeline_variables' do | ||||
|           before do | ||||
|  | @ -504,6 +512,11 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do | ||||
|           before do | ||||
|             stub_feature_flags(ci_read_trigger_from_ci_pipeline: false) | ||||
|           end | ||||
| 
 | ||||
|           context 'when variables are stored in trigger_request' do | ||||
|             before do | ||||
|               trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' }) | ||||
|  | @ -513,6 +526,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou | |||
| 
 | ||||
|             it_behaves_like 'reveal button variables behavior' | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when variables are stored in pipeline_variables' do | ||||
|           before 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,13 +18,18 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
|   end | ||||
| 
 | ||||
|   context 'with developer user' do | ||||
|     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) | ||||
|       project.add_developer(user) | ||||
|         stub_feature_flags(directory_code_dropdown_updates: true) | ||||
|       end | ||||
| 
 | ||||
|       it 'shows all the expected links' do | ||||
|       visit project_path(project) | ||||
|         visit project_path(project1) | ||||
| 
 | ||||
|         # The navigation bar | ||||
|         within_testid('super-sidebar') do | ||||
|  | @ -32,7 +38,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
|           aggregate_failures 'dropdown links in the navigation bar' do | ||||
|             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)) | ||||
|             expect(page).to have_link('New snippet', href: new_project_snippet_path(project1)) | ||||
|           end | ||||
| 
 | ||||
|           find_new_menu_toggle.click | ||||
|  | @ -52,14 +58,16 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
|         end | ||||
| 
 | ||||
|         # The Web IDE | ||||
|       click_button 'Edit' | ||||
|       expect(page).to have_button('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 | ||||
|       project.update!(archived: true) | ||||
|         project1.update!(archived: true) | ||||
| 
 | ||||
|       visit project_path(project) | ||||
|         visit project_path(project1) | ||||
| 
 | ||||
|         within_testid('super-sidebar') do | ||||
|           find_new_menu_toggle.click | ||||
|  | @ -67,7 +75,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
|           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)) | ||||
|             expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project1)) | ||||
|           end | ||||
| 
 | ||||
|           find_new_menu_toggle.click | ||||
|  | @ -75,7 +83,78 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
| 
 | ||||
|         expect(page).not_to have_selector('[data-testid="add-to-tree"]') | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|     context 'when directory_code_dropdown_updates is false' do | ||||
|       before_all do | ||||
|         project2.add_developer(user) | ||||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         stub_feature_flags(blob_overflow_menu: false) | ||||
|         stub_feature_flags(directory_code_dropdown_updates: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'shows all the expected links' do | ||||
|         visit project_path(project2) | ||||
| 
 | ||||
|         # The navigation bar | ||||
|         within_testid('super-sidebar') do | ||||
|           find_new_menu_toggle.click | ||||
| 
 | ||||
|           aggregate_failures 'dropdown links in the navigation bar' do | ||||
|             expect(page).to have_link('New issue') | ||||
|             expect(page).to have_link('New merge request') | ||||
|             expect(page).to have_link('New snippet', href: new_project_snippet_path(project2)) | ||||
|           end | ||||
| 
 | ||||
|           find_new_menu_toggle.click | ||||
|         end | ||||
| 
 | ||||
|         # The dropdown above the tree | ||||
|         page.within('.repo-breadcrumb') do | ||||
|           find_by_testid('add-to-tree').click | ||||
| 
 | ||||
|           aggregate_failures 'dropdown links above the repo tree' do | ||||
|             expect(page).to have_link('New file') | ||||
|             expect(page).to have_button('Upload file') | ||||
|             expect(page).to have_button('New directory') | ||||
|             expect(page).to have_link('New branch') | ||||
|             expect(page).to have_link('New tag') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         # The Web IDE | ||||
|         click_button 'Edit' | ||||
|         expect(page).to have_button('Web IDE') | ||||
|       end | ||||
| 
 | ||||
|       it 'hides the links when the project is archived' do | ||||
|         project2.update!(archived: true) | ||||
| 
 | ||||
|         visit project_path(project2) | ||||
| 
 | ||||
|         within_testid('super-sidebar') do | ||||
|           find_new_menu_toggle.click | ||||
| 
 | ||||
|           aggregate_failures 'dropdown links' do | ||||
|             expect(page).not_to have_link('New issue') | ||||
|             expect(page).not_to have_link('New merge request') | ||||
|             expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project2)) | ||||
|           end | ||||
| 
 | ||||
|           find_new_menu_toggle.click | ||||
|         end | ||||
| 
 | ||||
|         expect(page).not_to have_selector('[data-testid="add-to-tree"]') | ||||
|         expect(page).not_to have_button('Edit') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -90,10 +169,28 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
|     end | ||||
| 
 | ||||
|     with_them do | ||||
|       context 'when directory_code_dropdown_updates is true' 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) | ||||
|           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 | ||||
| 
 | ||||
|       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 | ||||
|  | @ -101,4 +198,5 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: : | |||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -8,8 +8,14 @@ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_id | |||
|   let(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :repository) } | ||||
| 
 | ||||
|   where(:directory_code_dropdown_updates) do | ||||
|     [true, 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) | ||||
| 
 | ||||
|       project.add_maintainer(user) | ||||
|       sign_in(user) | ||||
|  | @ -73,4 +79,5 @@ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_id | |||
| 
 | ||||
|       expect(page).to have_content('folder name') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -8,8 +8,14 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do | |||
|   let(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :repository) } | ||||
| 
 | ||||
|   where(:directory_code_dropdown_updates) do | ||||
|     [true, 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) | ||||
| 
 | ||||
|       project.add_maintainer(user) | ||||
|       sign_in(user) | ||||
|  | @ -26,7 +32,8 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do | |||
|     end | ||||
| 
 | ||||
|     it 'creates file in current directory' do | ||||
|     wait_for_requests | ||||
|       wait_for_all_requests | ||||
| 
 | ||||
|       first('.ide-tree-actions button').click | ||||
| 
 | ||||
|       page.within('.modal') do | ||||
|  | @ -60,4 +67,5 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do | |||
| 
 | ||||
|       expect(page).to have_content('file name') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -140,7 +140,10 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do | |||
|       stub_feature_flags(vscode_web_ide: false) | ||||
|     end | ||||
| 
 | ||||
|     context 'when directory_code_dropdown_updates is enabled' do | ||||
|       it 'opens folder in IDE' do | ||||
|         stub_feature_flags(directory_code_dropdown_updates: true) | ||||
| 
 | ||||
|         visit project_tree_path(project, File.join('master', 'bar')) | ||||
|         ide_visit_from_link | ||||
| 
 | ||||
|  | @ -151,6 +154,21 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when directory_code_dropdown_updates is disabled' do | ||||
|       it 'opens folder in IDE' do | ||||
|         stub_feature_flags(directory_code_dropdown_updates: false) | ||||
| 
 | ||||
|         visit project_tree_path(project, File.join('master', 'bar')) | ||||
|         ide_visit_from_link | ||||
| 
 | ||||
|         wait_for_all_requests | ||||
|         find('.ide-file-list') | ||||
|         wait_for_requests | ||||
|         expect(page).to have_selector('.is-open', text: 'bar') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'for subgroups' do | ||||
|     let(:group) { create(:group) } | ||||
|     let(:subgroup) { create(:group, parent: group) } | ||||
|  |  | |||
|  | @ -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