Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									e7e44c0e4c
								
							
						
					
					
						commit
						524e972622
					
				|  | @ -1,10 +1,51 @@ | |||
| cloud-native-image-env: | ||||
|   extends: | ||||
|     - .default-retry | ||||
|     - .cng:rules | ||||
|   image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13 | ||||
|   stage: post-test | ||||
|   before_script: | ||||
|     - source ./scripts/utils.sh | ||||
|     - install_gitlab_gem | ||||
|   script: | ||||
|     - 'ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > build.env' | ||||
|     - cat build.env | ||||
|   artifacts: | ||||
|     reports: | ||||
|       dotenv: build.env | ||||
|     paths: | ||||
|       - build.env | ||||
|     expire_in: 7 days | ||||
|     when: always | ||||
| 
 | ||||
| cloud-native-image: | ||||
|   extends: .cng:rules | ||||
|   image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine | ||||
|   dependencies: [] | ||||
|   stage: post-test | ||||
|   needs: ["cloud-native-image-env"] | ||||
|   inherit: | ||||
|     variables: false | ||||
|   variables: | ||||
|     GIT_DEPTH: "1" | ||||
|   script: | ||||
|     - install_gitlab_gem | ||||
|     - ./scripts/trigger-build cng | ||||
|     TOP_UPSTREAM_SOURCE_PROJECT: "${TOP_UPSTREAM_SOURCE_PROJECT}" | ||||
|     TOP_UPSTREAM_SOURCE_REF: "${TOP_UPSTREAM_SOURCE_REF}" | ||||
|     TOP_UPSTREAM_SOURCE_JOB: "${TOP_UPSTREAM_SOURCE_JOB}" | ||||
|     TOP_UPSTREAM_SOURCE_SHA: "${TOP_UPSTREAM_SOURCE_SHA}" | ||||
|     TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID: "${TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID}" | ||||
|     TOP_UPSTREAM_MERGE_REQUEST_IID: "${TOP_UPSTREAM_MERGE_REQUEST_IID}" | ||||
|     GITLAB_REF_SLUG: "${GITLAB_REF_SLUG}" | ||||
|     # CNG pipeline specific variables | ||||
|     GITLAB_VERSION: "${GITLAB_VERSION}" | ||||
|     GITLAB_TAG: "${GITLAB_TAG}" | ||||
|     GITLAB_ASSETS_TAG: "${GITLAB_ASSETS_TAG}" | ||||
|     FORCE_RAILS_IMAGE_BUILDS: "${FORCE_RAILS_IMAGE_BUILDS}" | ||||
|     CE_PIPELINE: "${CE_PIPELINE}"  # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$CE_PIPELINE'` will evaluate to `false` when this variable is empty | ||||
|     EE_PIPELINE: "${EE_PIPELINE}"  # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$EE_PIPELINE'` will evaluate to `false` when this variable is empty | ||||
|     GITLAB_SHELL_VERSION: "${GITLAB_SHELL_VERSION}" | ||||
|     GITLAB_ELASTICSEARCH_INDEXER_VERSION: "${GITLAB_ELASTICSEARCH_INDEXER_VERSION}" | ||||
|     GITLAB_KAS_VERSION: "${GITLAB_KAS_VERSION}" | ||||
|     GITLAB_WORKHORSE_VERSION: "${GITLAB_WORKHORSE_VERSION}" | ||||
|     GITLAB_PAGES_VERSION: "${GITLAB_PAGES_VERSION}" | ||||
|     GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}" | ||||
|   trigger: | ||||
|     project: gitlab-org/build/CNG | ||||
|     branch: $TRIGGER_BRANCH | ||||
|     strategy: depend | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ | |||
| review-docs-deploy: | ||||
|   extends: .review-docs | ||||
|   script: | ||||
|     - ./scripts/trigger-build docs deploy | ||||
|     - ./scripts/trigger-build.rb docs deploy | ||||
| 
 | ||||
| # Cleanup remote environment of gitlab-docs | ||||
| review-docs-cleanup: | ||||
|  | @ -37,7 +37,7 @@ review-docs-cleanup: | |||
|     name: review-docs/mr-${CI_MERGE_REQUEST_IID} | ||||
|     action: stop | ||||
|   script: | ||||
|     - ./scripts/trigger-build docs cleanup | ||||
|     - ./scripts/trigger-build.rb docs cleanup | ||||
| 
 | ||||
| docs-lint markdown: | ||||
|   extends: | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ update-qa-cache: | |||
|     - echo $exit_code | ||||
|     - | | ||||
|       if [ $exit_code -eq 0 ]; then | ||||
|         ./scripts/trigger-build omnibus | ||||
|         ./scripts/trigger-build.rb omnibus | ||||
|       elif [ $exit_code -eq 1 ]; then | ||||
|         exit 1 | ||||
|       else | ||||
|  | @ -108,7 +108,7 @@ update-qa-cache: | |||
|       if [[ $feature_flags ]]; then | ||||
|         export GITLAB_QA_OPTIONS="--set-feature-flags $feature_flags" | ||||
|         echo $GITLAB_QA_OPTIONS | ||||
|         ./scripts/trigger-build omnibus | ||||
|         ./scripts/trigger-build.rb omnibus | ||||
|       else | ||||
|         echo "No changed feature flag found to test. The tests are skipped if the flag was removed." | ||||
|       fi | ||||
|  |  | |||
|  | @ -438,7 +438,7 @@ db:gitlabcom-database-testing: | |||
|   script: | ||||
|     - source scripts/utils.sh | ||||
|     - install_gitlab_gem | ||||
|     - ./scripts/trigger-build gitlab-com-database-testing | ||||
|     - ./scripts/trigger-build.rb gitlab-com-database-testing | ||||
| 
 | ||||
| gitlab:setup: | ||||
|   extends: .db-job-base | ||||
|  |  | |||
|  | @ -16,20 +16,58 @@ include: | |||
|   - source ./scripts/review_apps/review-apps.sh | ||||
|   - install_api_client_dependencies_with_apk | ||||
| 
 | ||||
| review-build-cng: | ||||
| review-build-cng-env: | ||||
|   extends: | ||||
|     - .default-retry | ||||
|     - .review:rules:review-build-cng | ||||
|   image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13 | ||||
|   stage: prepare | ||||
|   variables: | ||||
|     CNG_PROJECT_ACCESS_TOKEN: "${CNG_MIRROR_PROJECT_ACCESS_TOKEN}"  # "Multi-pipeline (from 'gitlab-org/gitlab' 'review-build-cng' job)" at https://gitlab.com/gitlab-org/build/CNG-mirror/-/settings/access_tokens | ||||
|     CNG_PROJECT_PATH: "gitlab-org/build/CNG-mirror" | ||||
|   needs: [] | ||||
|   before_script: | ||||
|     - source ./scripts/utils.sh | ||||
|     - install_gitlab_gem | ||||
|   script: | ||||
|     - ./scripts/trigger-build cng | ||||
|     - 'ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > build.env' | ||||
|     - cat build.env | ||||
|   artifacts: | ||||
|     reports: | ||||
|       dotenv: build.env | ||||
|     paths: | ||||
|       - build.env | ||||
|     expire_in: 7 days | ||||
|     when: always | ||||
| 
 | ||||
| review-build-cng: | ||||
|   extends: .review:rules:review-build-cng | ||||
|   stage: prepare | ||||
|   needs: ["review-build-cng-env"] | ||||
|   inherit: | ||||
|     variables: false | ||||
|   variables: | ||||
|     TOP_UPSTREAM_SOURCE_PROJECT: "${TOP_UPSTREAM_SOURCE_PROJECT}" | ||||
|     TOP_UPSTREAM_SOURCE_REF: "${TOP_UPSTREAM_SOURCE_REF}" | ||||
|     TOP_UPSTREAM_SOURCE_JOB: "${TOP_UPSTREAM_SOURCE_JOB}" | ||||
|     TOP_UPSTREAM_SOURCE_SHA: "${TOP_UPSTREAM_SOURCE_SHA}" | ||||
|     TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID: "${TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID}" | ||||
|     TOP_UPSTREAM_MERGE_REQUEST_IID: "${TOP_UPSTREAM_MERGE_REQUEST_IID}" | ||||
|     GITLAB_REF_SLUG: "${GITLAB_REF_SLUG}" | ||||
|     # CNG pipeline specific variables | ||||
|     GITLAB_VERSION: "${GITLAB_VERSION}" | ||||
|     GITLAB_TAG: "${GITLAB_TAG}" | ||||
|     GITLAB_ASSETS_TAG: "${GITLAB_ASSETS_TAG}" | ||||
|     FORCE_RAILS_IMAGE_BUILDS: "${FORCE_RAILS_IMAGE_BUILDS}" | ||||
|     CE_PIPELINE: "${CE_PIPELINE}"  # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$CE_PIPELINE'` will evaluate to `false` when this variable is empty | ||||
|     EE_PIPELINE: "${EE_PIPELINE}"  # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$EE_PIPELINE'` will evaluate to `false` when this variable is empty | ||||
|     GITLAB_SHELL_VERSION: "${GITLAB_SHELL_VERSION}" | ||||
|     GITLAB_ELASTICSEARCH_INDEXER_VERSION: "${GITLAB_ELASTICSEARCH_INDEXER_VERSION}" | ||||
|     GITLAB_KAS_VERSION: "${GITLAB_KAS_VERSION}" | ||||
|     GITLAB_WORKHORSE_VERSION: "${GITLAB_WORKHORSE_VERSION}" | ||||
|     GITLAB_PAGES_VERSION: "${GITLAB_PAGES_VERSION}" | ||||
|     GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}" | ||||
|   trigger: | ||||
|     project: gitlab-org/build/CNG-mirror | ||||
|     branch: $TRIGGER_BRANCH | ||||
|     strategy: depend | ||||
| 
 | ||||
| .review-workflow-base: | ||||
|   extends: | ||||
|  |  | |||
|  | @ -141,7 +141,7 @@ | |||
|   - ".gitlab/ci/review-apps/**/*" | ||||
|   - "scripts/review_apps/base-config.yaml" | ||||
|   - "scripts/review_apps/review-apps.sh" | ||||
|   - "scripts/trigger-build" | ||||
|   - "scripts/trigger-build.rb" | ||||
|   - "{,ee/,jh/}{bin,config}/**/*.rb" | ||||
| 
 | ||||
| .ci-qa-patterns: &ci-qa-patterns | ||||
|  |  | |||
|  | @ -24,7 +24,6 @@ Database/MultipleDatabases: | |||
|   - lib/gitlab/import_export/group/relation_tree_restorer.rb | ||||
|   - lib/gitlab/legacy_github_import/importer.rb | ||||
|   - lib/gitlab/seeder.rb | ||||
|   - lib/system_check/orphans/repository_check.rb | ||||
|   - spec/db/schema_spec.rb | ||||
|   - spec/initializers/database_config_spec.rb | ||||
|   - spec/lib/backup/manager_spec.rb | ||||
|  |  | |||
							
								
								
									
										11
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										11
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -2,6 +2,17 @@ | |||
| documentation](doc/development/changelog.md) for instructions on adding your own | ||||
| entry. | ||||
| 
 | ||||
| ## 14.7.3 (2022-02-15) | ||||
| 
 | ||||
| ### Fixed (2 changes) | ||||
| 
 | ||||
| - [Update GitHub PRs Importer to force update repository](gitlab-org/gitlab@33f12736b070362cb89e9bbb4b3aa7d86fc373c3) ([merge request](gitlab-org/gitlab!80595)) | ||||
| - [Fix Geo checksummable check failing when file is nil](gitlab-org/gitlab@f49e3ea3e4d4ca7a64607687f9aaa974801b6bf9) ([merge request](gitlab-org/gitlab!80595)) **GitLab Enterprise Edition** | ||||
| 
 | ||||
| ### Changed (1 change) | ||||
| 
 | ||||
| - [Properly exclude pending_destruction packages when creating one](gitlab-org/gitlab@9fb9f1ca8a2342225b7017c211f85175a4ef56dd) ([merge request](gitlab-org/gitlab!80595)) | ||||
| 
 | ||||
| ## 14.7.2 (2022-02-08) | ||||
| 
 | ||||
| ### Added (1 change) | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| d3ab199f7923a9d75516b8d1f1ea2f84b03190b1 | ||||
| a67a6fdd96ba690d57c919f9a042dceebab2832e | ||||
|  |  | |||
|  | @ -44,6 +44,9 @@ export const typePolicies = { | |||
|   PipelinePermissions: { | ||||
|     merge: true, | ||||
|   }, | ||||
|   DesignCollection: { | ||||
|     merge: true, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export const stripWhitespaceFromQuery = (url, path) => { | ||||
|  |  | |||
|  | @ -2,31 +2,14 @@ | |||
| import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; | ||||
| import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; | ||||
| import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | ||||
| import { formatNumber, __, s__ } from '~/locale'; | ||||
| import { __, s__ } from '~/locale'; | ||||
| import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; | ||||
| import { RUNNER_JOB_COUNT_LIMIT } from '../constants'; | ||||
| import { formatJobCount, tableField } from '../utils'; | ||||
| import RunnerActionsCell from './cells/runner_actions_cell.vue'; | ||||
| import RunnerSummaryCell from './cells/runner_summary_cell.vue'; | ||||
| import RunnerStatusCell from './cells/runner_status_cell.vue'; | ||||
| import RunnerTags from './runner_tags.vue'; | ||||
| 
 | ||||
| const tableField = ({ key, label = '', thClasses = [] }) => { | ||||
|   return { | ||||
|     key, | ||||
|     label, | ||||
|     thClass: [ | ||||
|       'gl-bg-transparent!', | ||||
|       'gl-border-b-solid!', | ||||
|       'gl-border-b-gray-100!', | ||||
|       'gl-border-b-1!', | ||||
|       ...thClasses, | ||||
|     ], | ||||
|     tdAttr: { | ||||
|       'data-testid': `td-${key}`, | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     GlTable, | ||||
|  | @ -54,10 +37,7 @@ export default { | |||
|   }, | ||||
|   methods: { | ||||
|     formatJobCount(jobCount) { | ||||
|       if (jobCount > RUNNER_JOB_COUNT_LIMIT) { | ||||
|         return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`; | ||||
|       } | ||||
|       return formatNumber(jobCount); | ||||
|       return formatJobCount(jobCount); | ||||
|     }, | ||||
|     runnerTrAttr(runner) { | ||||
|       if (runner) { | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { | |||
|   I18N_FETCH_ERROR, | ||||
|   RUNNER_DETAILS_PROJECTS_PAGE_SIZE, | ||||
| } from '../constants'; | ||||
| import { getPaginationVariables } from '../utils'; | ||||
| import { captureException } from '../sentry_utils'; | ||||
| import RunnerAssignedItem from './runner_assigned_item.vue'; | ||||
| import RunnerPagination from './runner_pagination.vue'; | ||||
|  | @ -62,19 +63,9 @@ export default { | |||
|   computed: { | ||||
|     variables() { | ||||
|       const { id } = this.runner; | ||||
|       const { before, after } = this.pagination; | ||||
| 
 | ||||
|       if (before) { | ||||
|       return { | ||||
|         id, | ||||
|           before, | ||||
|           last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, | ||||
|         }; | ||||
|       } | ||||
|       return { | ||||
|         id, | ||||
|         after, | ||||
|         first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, | ||||
|         ...getPaginationVariables(this.pagination, RUNNER_DETAILS_PROJECTS_PAGE_SIZE), | ||||
|       }; | ||||
|     }, | ||||
|     loading() { | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ import { | |||
|   RUNNER_PAGE_SIZE, | ||||
|   STATUS_NEVER_CONTACTED, | ||||
| } from './constants'; | ||||
| import { getPaginationVariables } from './utils'; | ||||
| 
 | ||||
| /** | ||||
|  * The filters and sorting of the runners are built around | ||||
|  | @ -184,30 +185,27 @@ export const fromSearchToVariables = ({ | |||
|   sort = null, | ||||
|   pagination = {}, | ||||
| } = {}) => { | ||||
|   const variables = {}; | ||||
|   const filterVariables = {}; | ||||
| 
 | ||||
|   const queryObj = filterToQueryObject(processFilters(filters), { | ||||
|     filteredSearchTermKey: PARAM_KEY_SEARCH, | ||||
|   }); | ||||
| 
 | ||||
|   [variables.status] = queryObj[PARAM_KEY_STATUS] || []; | ||||
|   variables.search = queryObj[PARAM_KEY_SEARCH]; | ||||
|   variables.tagList = queryObj[PARAM_KEY_TAG]; | ||||
|   [filterVariables.status] = queryObj[PARAM_KEY_STATUS] || []; | ||||
|   filterVariables.search = queryObj[PARAM_KEY_SEARCH]; | ||||
|   filterVariables.tagList = queryObj[PARAM_KEY_TAG]; | ||||
| 
 | ||||
|   if (runnerType) { | ||||
|     variables.type = runnerType; | ||||
|     filterVariables.type = runnerType; | ||||
|   } | ||||
|   if (sort) { | ||||
|     variables.sort = sort; | ||||
|     filterVariables.sort = sort; | ||||
|   } | ||||
| 
 | ||||
|   if (pagination.before) { | ||||
|     variables.before = pagination.before; | ||||
|     variables.last = RUNNER_PAGE_SIZE; | ||||
|   } else { | ||||
|     variables.after = pagination.after; | ||||
|     variables.first = RUNNER_PAGE_SIZE; | ||||
|   } | ||||
|   const paginationVariables = getPaginationVariables(pagination, RUNNER_PAGE_SIZE); | ||||
| 
 | ||||
|   return variables; | ||||
|   return { | ||||
|     ...filterVariables, | ||||
|     ...paginationVariables, | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -0,0 +1,72 @@ | |||
| import { formatNumber } from '~/locale'; | ||||
| import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants'; | ||||
| import { RUNNER_JOB_COUNT_LIMIT } from './constants'; | ||||
| 
 | ||||
| /** | ||||
|  * Formats a job count, limited to a max number | ||||
|  * | ||||
|  * @param {Number} jobCount | ||||
|  * @returns Formatted string | ||||
|  */ | ||||
| export const formatJobCount = (jobCount) => { | ||||
|   if (typeof jobCount !== 'number') { | ||||
|     return ''; | ||||
|   } | ||||
|   if (jobCount > RUNNER_JOB_COUNT_LIMIT) { | ||||
|     return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`; | ||||
|   } | ||||
|   return formatNumber(jobCount); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Returns a GlTable fields with a given key and label | ||||
|  * | ||||
|  * @param {Object} options | ||||
|  * @returns Field object to add to GlTable fields | ||||
|  */ | ||||
| export const tableField = ({ key, label = '', thClasses = [] }) => { | ||||
|   return { | ||||
|     key, | ||||
|     label, | ||||
|     thClass: [DEFAULT_TH_CLASSES, ...thClasses], | ||||
|     tdAttr: { | ||||
|       'data-testid': `td-${key}`, | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Returns variables for a GraphQL query that uses keyset | ||||
|  * pagination. | ||||
|  * | ||||
|  * https://docs.gitlab.com/ee/development/graphql_guide/pagination.html#keyset-pagination
 | ||||
|  * | ||||
|  * @param {Object} pagination - Contains before, after, page | ||||
|  * @param {Number} pageSize | ||||
|  * @returns Variables | ||||
|  */ | ||||
| export const getPaginationVariables = (pagination, pageSize = 10) => { | ||||
|   const { before, after } = pagination; | ||||
| 
 | ||||
|   // first + after: Next page
 | ||||
|   // Get the first N items after item X
 | ||||
|   if (after) { | ||||
|     return { | ||||
|       after, | ||||
|       first: pageSize, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   // last + before: Prev page
 | ||||
|   // Get the first N items before item X, when you click on Prev
 | ||||
|   if (before) { | ||||
|     return { | ||||
|       before, | ||||
|       last: pageSize, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   // first page
 | ||||
|   // Get the first N items
 | ||||
|   return { first: pageSize }; | ||||
| }; | ||||
|  | @ -10,6 +10,7 @@ import { | |||
|   GlIcon, | ||||
|   GlTooltipDirective, | ||||
| } from '@gitlab/ui'; | ||||
| import { kebabCase, snakeCase } from 'lodash'; | ||||
| import createFlash from '~/flash'; | ||||
| import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | ||||
| import { IssuableType } from '~/issues/constants'; | ||||
|  | @ -221,6 +222,12 @@ export default { | |||
|       // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 | ||||
|       return this.issuableAttribute === IssuableType.Epic; | ||||
|     }, | ||||
|     formatIssuableAttribute() { | ||||
|       return { | ||||
|         kebab: kebabCase(this.issuableAttribute), | ||||
|         snake: snakeCase(this.issuableAttribute), | ||||
|       }; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     updateAttribute(attributeId) { | ||||
|  | @ -300,26 +307,28 @@ export default { | |||
|   <sidebar-editable-item | ||||
|     ref="editable" | ||||
|     :title="attributeTypeTitle" | ||||
|     :data-testid="`${issuableAttribute}-edit`" | ||||
|     :data-testid="`${formatIssuableAttribute.kebab}-edit`" | ||||
|     :tracking="tracking" | ||||
|     :loading="updating || loading" | ||||
|     @open="handleOpen" | ||||
|     @close="handleClose" | ||||
|   > | ||||
|     <template #collapsed> | ||||
|       <slot name="value-collapsed" :current-attribute="currentAttribute"> | ||||
|         <div | ||||
|           v-if="isClassicSidebar" | ||||
|           v-gl-tooltip.left.viewport | ||||
|           :title="attributeTypeTitle" | ||||
|           class="sidebar-collapsed-icon" | ||||
|         > | ||||
|         <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="attributeTypeIcon" /> | ||||
|           <gl-icon :aria-label="attributeTypeTitle" :name="attributeTypeIcon" /> | ||||
|           <span class="collapse-truncated-title"> | ||||
|             {{ attributeTitle }} | ||||
|           </span> | ||||
|         </div> | ||||
|       </slot> | ||||
|       <div | ||||
|         :data-testid="`select-${issuableAttribute}`" | ||||
|         :data-testid="`select-${formatIssuableAttribute.kebab}`" | ||||
|         :class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'" | ||||
|       > | ||||
|         <span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span> | ||||
|  | @ -337,7 +346,7 @@ export default { | |||
|             v-gl-tooltip="tooltipText" | ||||
|             class="gl-text-gray-900! gl-font-weight-bold" | ||||
|             :href="attributeUrl" | ||||
|             :data-qa-selector="`${issuableAttribute}_link`" | ||||
|             :data-qa-selector="`${formatIssuableAttribute.snake}_link`" | ||||
|           > | ||||
|             {{ attributeTitle }} | ||||
|             <span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span> | ||||
|  | @ -359,7 +368,7 @@ export default { | |||
|       > | ||||
|         <gl-search-box-by-type ref="search" v-model="searchTerm" /> | ||||
|         <gl-dropdown-item | ||||
|           :data-testid="`no-${issuableAttribute}-item`" | ||||
|           :data-testid="`no-${formatIssuableAttribute.kebab}-item`" | ||||
|           :is-check-item="true" | ||||
|           :is-checked="isAttributeChecked($options.noAttributeId)" | ||||
|           @click="updateAttribute($options.noAttributeId)" | ||||
|  | @ -389,7 +398,7 @@ export default { | |||
|               :key="attrItem.id" | ||||
|               :is-check-item="true" | ||||
|               :is-checked="isAttributeChecked(attrItem.id)" | ||||
|               :data-testid="`${issuableAttribute}-items`" | ||||
|               :data-testid="`${formatIssuableAttribute.kebab}-items`" | ||||
|               @click="updateAttribute(attrItem.id)" | ||||
|             > | ||||
|               {{ attrItem.title }} | ||||
|  |  | |||
|  | @ -38,7 +38,10 @@ export default { | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div data-testid="helpPane" class="time-tracking-help-state"> | ||||
|   <div | ||||
|     data-testid="helpPane" | ||||
|     class="sidebar-help-state gl-bg-white gl-border-gray-100 gl-border-t-solid gl-border-b-solid gl-border-1" | ||||
|   > | ||||
|     <div class="time-tracking-info"> | ||||
|       <h4>{{ __('Track time with quick actions') }}</h4> | ||||
|       <p>{{ __('Quick actions can be used in description and comment boxes.') }}</p> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
| import { GlIcon, GlLink, GlModal, GlModalDirective, GlLoadingIcon } from '@gitlab/ui'; | ||||
| import { GlIcon, GlLink, GlModal, GlButton, GlModalDirective, GlLoadingIcon } from '@gitlab/ui'; | ||||
| import { IssuableType } from '~/issues/constants'; | ||||
| import { s__, __ } from '~/locale'; | ||||
| import { timeTrackingQueries } from '~/sidebar/constants'; | ||||
|  | @ -21,6 +21,7 @@ export default { | |||
|     GlIcon, | ||||
|     GlLink, | ||||
|     GlModal, | ||||
|     GlButton, | ||||
|     GlLoadingIcon, | ||||
|     TimeTrackingCollapsedState, | ||||
|     TimeTrackingSpentOnlyPane, | ||||
|  | @ -187,7 +188,11 @@ export default { | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div v-cloak class="time-tracker time-tracking-component-wrap" data-testid="time-tracker"> | ||||
|   <div | ||||
|     v-cloak | ||||
|     class="time-tracker time-tracking-component-wrap sidebar-help-wrap" | ||||
|     data-testid="time-tracker" | ||||
|   > | ||||
|     <time-tracking-collapsed-state | ||||
|       v-if="showCollapsed" | ||||
|       :show-comparison-state="showComparisonState" | ||||
|  | @ -198,25 +203,21 @@ export default { | |||
|       :time-spent-human-readable="humanTotalTimeSpent" | ||||
|       :time-estimate-human-readable="humanTimeEstimate" | ||||
|     /> | ||||
|     <div class="hide-collapsed gl-line-height-20 gl-text-gray-900"> | ||||
|     <div | ||||
|       class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center" | ||||
|     > | ||||
|       {{ __('Time tracking') }} | ||||
|       <gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" inline /> | ||||
|       <div | ||||
|         v-if="!showHelpState" | ||||
|         data-testid="helpButton" | ||||
|         class="help-button float-right" | ||||
|         @click="toggleHelpState(true)" | ||||
|       <gl-button | ||||
|         :data-testid="showHelpState ? 'closeHelpButton' : 'helpButton'" | ||||
|         category="tertiary" | ||||
|         size="small" | ||||
|         variant="link" | ||||
|         class="gl-ml-auto" | ||||
|         @click="toggleHelpState(!showHelpState)" | ||||
|       > | ||||
|         <gl-icon name="question-o" /> | ||||
|       </div> | ||||
|       <div | ||||
|         v-else | ||||
|         data-testid="closeHelpButton" | ||||
|         class="close-help-button float-right" | ||||
|         @click="toggleHelpState(false)" | ||||
|       > | ||||
|         <gl-icon name="close" /> | ||||
|       </div> | ||||
|         <gl-icon :name="showHelpState ? 'close' : 'question-o'" class="gl-text-gray-900!" /> | ||||
|       </gl-button> | ||||
|     </div> | ||||
|     <div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed"> | ||||
|       <div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane"> | ||||
|  |  | |||
|  | @ -0,0 +1,65 @@ | |||
| import { kebabCase } from 'lodash'; | ||||
| import Vue from 'vue'; | ||||
| import { GlToggle } from '@gitlab/ui'; | ||||
| import { parseBoolean } from '~/lib/utils/common_utils'; | ||||
| 
 | ||||
| export const initToggle = (el) => { | ||||
|   if (!el) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   const { | ||||
|     name, | ||||
|     isChecked, | ||||
|     disabled, | ||||
|     isLoading, | ||||
|     label, | ||||
|     help, | ||||
|     labelPosition, | ||||
|     ...dataset | ||||
|   } = el.dataset; | ||||
| 
 | ||||
|   return new Vue({ | ||||
|     el, | ||||
|     props: { | ||||
|       disabled: { | ||||
|         type: Boolean, | ||||
|         required: false, | ||||
|         default: parseBoolean(disabled), | ||||
|       }, | ||||
|       isLoading: { | ||||
|         type: Boolean, | ||||
|         required: false, | ||||
|         default: parseBoolean(isLoading), | ||||
|       }, | ||||
|     }, | ||||
|     data() { | ||||
|       return { | ||||
|         value: parseBoolean(isChecked), | ||||
|       }; | ||||
|     }, | ||||
|     render(h) { | ||||
|       return h(GlToggle, { | ||||
|         props: { | ||||
|           name, | ||||
|           value: this.value, | ||||
|           disabled: this.disabled, | ||||
|           isLoading: this.isLoading, | ||||
|           label, | ||||
|           help, | ||||
|           labelPosition, | ||||
|         }, | ||||
|         class: el.className, | ||||
|         attrs: Object.fromEntries( | ||||
|           Object.entries(dataset).map(([key, value]) => [`data-${kebabCase(key)}`, value]), | ||||
|         ), | ||||
|         on: { | ||||
|           change: (newValue) => { | ||||
|             this.value = newValue; | ||||
|             this.$emit('change', newValue); | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | @ -742,6 +742,26 @@ | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .sidebar-help-wrap { | ||||
|   .sidebar-help-state { | ||||
|     margin: 16px -20px -20px; | ||||
|     padding: 16px 20px; | ||||
|   } | ||||
| 
 | ||||
|   .help-state-toggle-enter-active { | ||||
|     transition: all 0.8s ease; | ||||
|   } | ||||
| 
 | ||||
|   .help-state-toggle-leave-active { | ||||
|     transition: all 0.5s ease; | ||||
|   } | ||||
| 
 | ||||
|   .help-state-toggle-enter, | ||||
|   .help-state-toggle-leave-active { | ||||
|     opacity: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .time-tracker { | ||||
|   .sidebar-collapsed-icon { | ||||
|     > .stopwatch-svg { | ||||
|  | @ -759,11 +779,6 @@ | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .help-button, | ||||
|   .close-help-button { | ||||
|     cursor: pointer; | ||||
|   } | ||||
| 
 | ||||
|   .compare-meter { | ||||
|     &.over_estimate { | ||||
|       .time-remaining, | ||||
|  | @ -776,31 +791,6 @@ | |||
|   .compare-display-container { | ||||
|     font-size: 13px; | ||||
|   } | ||||
| 
 | ||||
|   .time-tracking-help-state { | ||||
|     background: $white; | ||||
|     margin: 16px -20px -20px; | ||||
|     padding: 16px 20px; | ||||
|     border-top: 1px solid $border-gray-light; | ||||
|     border-bottom: 1px solid $border-gray-light; | ||||
| 
 | ||||
|     a:hover { | ||||
|       color: $btn-white-active; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .help-state-toggle-enter-active { | ||||
|     transition: all 0.8s ease; | ||||
|   } | ||||
| 
 | ||||
|   .help-state-toggle-leave-active { | ||||
|     transition: all 0.5s ease; | ||||
|   } | ||||
| 
 | ||||
|   .help-state-toggle-enter, | ||||
|   .help-state-toggle-leave-active { | ||||
|     opacity: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .issuable-todo-btn { | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module AlertManagement | ||||
|     module HttpIntegration | ||||
|       class Create < HttpIntegrationBase | ||||
|         include FindsProject | ||||
| 
 | ||||
|         graphql_name 'HttpIntegrationCreate' | ||||
| 
 | ||||
|         include FindsProject | ||||
| 
 | ||||
|         argument :project_path, GraphQL::Types::ID, | ||||
|                  required: true, | ||||
|                  description: 'Project to create the integration in.' | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module AlertManagement | ||||
|     module PrometheusIntegration | ||||
|       class Create < PrometheusIntegrationBase | ||||
|         include FindsProject | ||||
| 
 | ||||
|         graphql_name 'PrometheusIntegrationCreate' | ||||
| 
 | ||||
|         include FindsProject | ||||
| 
 | ||||
|         argument :project_path, GraphQL::Types::ID, | ||||
|                  required: true, | ||||
|                  description: 'Project to create the integration in.' | ||||
|  |  | |||
|  | @ -3,10 +3,9 @@ | |||
| module Mutations | ||||
|   module Boards | ||||
|     class Create < ::Mutations::BaseMutation | ||||
|       include Mutations::ResolvesResourceParent | ||||
| 
 | ||||
|       graphql_name 'CreateBoard' | ||||
| 
 | ||||
|       include Mutations::ResolvesResourceParent | ||||
|       include Mutations::Boards::CommonMutationArguments | ||||
| 
 | ||||
|       field :board, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module Branches | ||||
|     class Create < BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'CreateBranch' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       argument :project_path, GraphQL::Types::ID, | ||||
|                required: true, | ||||
|                description: 'Project full path the branch is associated with.' | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module Ci | ||||
|     class CiCdSettingsUpdate < BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'CiCdSettingsUpdate' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       authorize :admin_project | ||||
| 
 | ||||
|       argument :full_path, GraphQL::Types::ID, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module Ci | ||||
|     module JobTokenScope | ||||
|       class AddProject < BaseMutation | ||||
|         include FindsProject | ||||
| 
 | ||||
|         graphql_name 'CiJobTokenScopeAddProject' | ||||
| 
 | ||||
|         include FindsProject | ||||
| 
 | ||||
|         authorize :admin_project | ||||
| 
 | ||||
|         argument :project_path, GraphQL::Types::ID, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module Ci | ||||
|     module JobTokenScope | ||||
|       class RemoveProject < BaseMutation | ||||
|         include FindsProject | ||||
| 
 | ||||
|         graphql_name 'CiJobTokenScopeRemoveProject' | ||||
| 
 | ||||
|         include FindsProject | ||||
| 
 | ||||
|         authorize :admin_project | ||||
| 
 | ||||
|         argument :project_path, GraphQL::Types::ID, | ||||
|  |  | |||
|  | @ -4,12 +4,12 @@ module Mutations | |||
|   module Clusters | ||||
|     module Agents | ||||
|       class Create < BaseMutation | ||||
|         graphql_name 'CreateClusterAgent' | ||||
| 
 | ||||
|         include FindsProject | ||||
| 
 | ||||
|         authorize :create_cluster | ||||
| 
 | ||||
|         graphql_name 'CreateClusterAgent' | ||||
| 
 | ||||
|         argument :project_path, GraphQL::Types::ID, | ||||
|                  required: true, | ||||
|                  description: 'Full path of the associated project for this cluster agent.' | ||||
|  |  | |||
|  | @ -3,6 +3,8 @@ | |||
| module Mutations | ||||
|   module Commits | ||||
|     class Create < BaseMutation | ||||
|       graphql_name 'CommitCreate' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       class UrlHelpers | ||||
|  | @ -10,8 +12,6 @@ module Mutations | |||
|         include Gitlab::Routing | ||||
|       end | ||||
| 
 | ||||
|       graphql_name 'CommitCreate' | ||||
| 
 | ||||
|       argument :project_path, GraphQL::Types::ID, | ||||
|                required: true, | ||||
|                description: 'Project full path the branch is associated with.' | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module ContainerExpirationPolicies | ||||
|     class Update < Mutations::BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'UpdateContainerExpirationPolicy' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       authorize :destroy_container_image | ||||
| 
 | ||||
|       argument :project_path, | ||||
|  |  | |||
|  | @ -3,12 +3,11 @@ | |||
| module Mutations | ||||
|   module ContainerRepositories | ||||
|     class DestroyTags < ::Mutations::ContainerRepositories::DestroyBase | ||||
|       LIMIT = 20 | ||||
| 
 | ||||
|       TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}" | ||||
| 
 | ||||
|       graphql_name 'DestroyContainerRepositoryTags' | ||||
| 
 | ||||
|       LIMIT = 20 | ||||
|       TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}" | ||||
| 
 | ||||
|       authorize :destroy_container_image | ||||
| 
 | ||||
|       argument :id, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module CustomEmoji | ||||
|     class Create < BaseMutation | ||||
|       include Mutations::ResolvesGroup | ||||
| 
 | ||||
|       graphql_name 'CreateCustomEmoji' | ||||
| 
 | ||||
|       include Mutations::ResolvesGroup | ||||
| 
 | ||||
|       authorize :create_custom_emoji | ||||
| 
 | ||||
|       field :custom_emoji, | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ module Mutations | |||
|   module CustomerRelations | ||||
|     module Contacts | ||||
|       class Create < BaseMutation | ||||
|         graphql_name 'CustomerRelationsContactCreate' | ||||
| 
 | ||||
|         include ResolvesIds | ||||
|         include Gitlab::Graphql::Authorize::AuthorizeResource | ||||
| 
 | ||||
|         graphql_name 'CustomerRelationsContactCreate' | ||||
| 
 | ||||
|         field :contact, | ||||
|               Types::CustomerRelations::ContactType, | ||||
|               null: true, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module CustomerRelations | ||||
|     module Contacts | ||||
|       class Update < Mutations::BaseMutation | ||||
|         include ResolvesIds | ||||
| 
 | ||||
|         graphql_name 'CustomerRelationsContactUpdate' | ||||
| 
 | ||||
|         include ResolvesIds | ||||
| 
 | ||||
|         authorize :admin_crm_contact | ||||
| 
 | ||||
|         field :contact, | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ module Mutations | |||
|   module CustomerRelations | ||||
|     module Organizations | ||||
|       class Create < BaseMutation | ||||
|         graphql_name 'CustomerRelationsOrganizationCreate' | ||||
| 
 | ||||
|         include ResolvesIds | ||||
|         include Gitlab::Graphql::Authorize::AuthorizeResource | ||||
| 
 | ||||
|         graphql_name 'CustomerRelationsOrganizationCreate' | ||||
| 
 | ||||
|         field :organization, | ||||
|               Types::CustomerRelations::OrganizationType, | ||||
|               null: true, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module CustomerRelations | ||||
|     module Organizations | ||||
|       class Update < Mutations::BaseMutation | ||||
|         include ResolvesIds | ||||
| 
 | ||||
|         graphql_name 'CustomerRelationsOrganizationUpdate' | ||||
| 
 | ||||
|         include ResolvesIds | ||||
| 
 | ||||
|         authorize :admin_crm_organization | ||||
| 
 | ||||
|         field :organization, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module DependencyProxy | ||||
|     module GroupSettings | ||||
|       class Update < Mutations::BaseMutation | ||||
|         include Mutations::ResolvesGroup | ||||
| 
 | ||||
|         graphql_name 'UpdateDependencyProxySettings' | ||||
| 
 | ||||
|         include Mutations::ResolvesGroup | ||||
| 
 | ||||
|         authorize :admin_dependency_proxy | ||||
| 
 | ||||
|         argument :group_path, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module DependencyProxy | ||||
|     module ImageTtlGroupPolicy | ||||
|       class Update < Mutations::BaseMutation | ||||
|         include Mutations::ResolvesGroup | ||||
| 
 | ||||
|         graphql_name 'UpdateDependencyProxyImageTtlGroupPolicy' | ||||
| 
 | ||||
|         include Mutations::ResolvesGroup | ||||
| 
 | ||||
|         authorize :admin_dependency_proxy | ||||
| 
 | ||||
|         argument :group_path, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module DesignManagement | ||||
|     class Delete < Base | ||||
|       Errors = ::Gitlab::Graphql::Errors | ||||
| 
 | ||||
|       graphql_name "DesignManagementDelete" | ||||
| 
 | ||||
|       Errors = ::Gitlab::Graphql::Errors | ||||
| 
 | ||||
|       argument :filenames, [GraphQL::Types::String], | ||||
|                required: true, | ||||
|                description: "Filenames of the designs to delete.", | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module Groups | ||||
|     class Update < Mutations::BaseMutation | ||||
|       include Mutations::ResolvesGroup | ||||
| 
 | ||||
|       graphql_name 'GroupUpdate' | ||||
| 
 | ||||
|       include Mutations::ResolvesGroup | ||||
| 
 | ||||
|       authorize :admin_group | ||||
| 
 | ||||
|       field :group, Types::GroupType, | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ | |||
| module Mutations | ||||
|   module Issues | ||||
|     class Create < BaseMutation | ||||
|       graphql_name 'CreateIssue' | ||||
| 
 | ||||
|       include Mutations::SpamProtection | ||||
|       include FindsProject | ||||
|       include CommonMutationArguments | ||||
| 
 | ||||
|       graphql_name 'CreateIssue' | ||||
| 
 | ||||
|       authorize :create_issue | ||||
| 
 | ||||
|       argument :project_path, GraphQL::Types::ID, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module Issues | ||||
|     class SetConfidential < Base | ||||
|       include Mutations::SpamProtection | ||||
| 
 | ||||
|       graphql_name 'IssueSetConfidential' | ||||
| 
 | ||||
|       include Mutations::SpamProtection | ||||
| 
 | ||||
|       argument :confidential, | ||||
|                GraphQL::Types::Boolean, | ||||
|                required: true, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module JiraImport | ||||
|     class ImportUsers < BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'JiraImportUsers' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       authorize :admin_project | ||||
| 
 | ||||
|       field :jira_users, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module JiraImport | ||||
|     class Start < BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'JiraImportStart' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       authorize :admin_project | ||||
| 
 | ||||
|       field :jira_import, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module Labels | ||||
|     class Create < BaseMutation | ||||
|       include Mutations::ResolvesResourceParent | ||||
| 
 | ||||
|       graphql_name 'LabelCreate' | ||||
| 
 | ||||
|       include Mutations::ResolvesResourceParent | ||||
| 
 | ||||
|       field :label, | ||||
|             Types::LabelType, | ||||
|             null: true, | ||||
|  |  | |||
|  | @ -3,12 +3,6 @@ | |||
| module Mutations | ||||
|   module MergeRequests | ||||
|     class Accept < Base | ||||
|       NOT_MERGEABLE = 'This branch cannot be merged' | ||||
|       HOOKS_VALIDATION_ERROR = 'Pre-merge hooks failed' | ||||
|       SHA_MISMATCH = 'The merge-head is not at the anticipated SHA' | ||||
|       MERGE_FAILED = 'The merge failed' | ||||
|       ALREADY_SCHEDULED = 'The merge request is already scheduled to be merged' | ||||
| 
 | ||||
|       graphql_name 'MergeRequestAccept' | ||||
|       authorize :accept_merge_request | ||||
|       description <<~DESC | ||||
|  | @ -17,6 +11,12 @@ module Mutations | |||
|         immediately if possible, or using one of the automatic merge strategies. | ||||
|       DESC | ||||
| 
 | ||||
|       NOT_MERGEABLE = 'This branch cannot be merged' | ||||
|       HOOKS_VALIDATION_ERROR = 'Pre-merge hooks failed' | ||||
|       SHA_MISMATCH = 'The merge-head is not at the anticipated SHA' | ||||
|       MERGE_FAILED = 'The merge failed' | ||||
|       ALREADY_SCHEDULED = 'The merge request is already scheduled to be merged' | ||||
| 
 | ||||
|       argument :strategy, | ||||
|                ::Types::MergeStrategyEnum, | ||||
|                required: false, | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Mutations | ||||
|   module MergeRequests | ||||
|     class Create < BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'MergeRequestCreate' | ||||
| 
 | ||||
|       include FindsProject | ||||
| 
 | ||||
|       argument :project_path, GraphQL::Types::ID, | ||||
|                required: true, | ||||
|                description: 'Project full path the merge request is associated with.' | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Mutations | |||
|   module Namespace | ||||
|     module PackageSettings | ||||
|       class Update < Mutations::BaseMutation | ||||
|         include Mutations::ResolvesNamespace | ||||
| 
 | ||||
|         graphql_name 'UpdateNamespacePackageSettings' | ||||
| 
 | ||||
|         include Mutations::ResolvesNamespace | ||||
| 
 | ||||
|         authorize :create_package_settings | ||||
| 
 | ||||
|         argument :namespace_path, | ||||
|  |  | |||
|  | @ -3,14 +3,13 @@ | |||
| module Mutations | ||||
|   module ReleaseAssetLinks | ||||
|     class Create < BaseMutation | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'ReleaseAssetLinkCreate' | ||||
| 
 | ||||
|       authorize :create_release | ||||
| 
 | ||||
|       include FindsProject | ||||
|       include Types::ReleaseAssetLinkSharedInputArguments | ||||
| 
 | ||||
|       authorize :create_release | ||||
| 
 | ||||
|       argument :project_path, GraphQL::Types::ID, | ||||
|                required: true, | ||||
|                description: 'Full path of the project the asset link is associated with.' | ||||
|  |  | |||
|  | @ -3,14 +3,14 @@ | |||
| module Mutations | ||||
|   module Snippets | ||||
|     class Create < BaseMutation | ||||
|       graphql_name 'CreateSnippet' | ||||
| 
 | ||||
|       include ServiceCompatibility | ||||
|       include CanMutateSpammable | ||||
|       include Mutations::SpamProtection | ||||
| 
 | ||||
|       authorize :create_snippet | ||||
| 
 | ||||
|       graphql_name 'CreateSnippet' | ||||
| 
 | ||||
|       field :snippet, | ||||
|             Types::SnippetType, | ||||
|             null: true, | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ | |||
| module Mutations | ||||
|   module Snippets | ||||
|     class Update < Base | ||||
|       graphql_name 'UpdateSnippet' | ||||
| 
 | ||||
|       include ServiceCompatibility | ||||
|       include CanMutateSpammable | ||||
|       include Mutations::SpamProtection | ||||
| 
 | ||||
|       graphql_name 'UpdateSnippet' | ||||
| 
 | ||||
|       argument :id, ::Types::GlobalIDType[::Snippet], | ||||
|                required: true, | ||||
|                description: 'Global ID of the snippet to update.' | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| module Mutations | ||||
|   module WorkItems | ||||
|     class Create < BaseMutation | ||||
|       graphql_name 'WorkItemCreate' | ||||
| 
 | ||||
|       include Mutations::SpamProtection | ||||
|       include FindsProject | ||||
| 
 | ||||
|       graphql_name 'WorkItemCreate' | ||||
| 
 | ||||
|       authorize :create_work_item | ||||
| 
 | ||||
|       argument :description, GraphQL::Types::String, | ||||
|  |  | |||
|  | @ -3,11 +3,10 @@ | |||
| module Mutations | ||||
|   module WorkItems | ||||
|     class Delete < BaseMutation | ||||
|       graphql_name 'WorkItemDelete' | ||||
|       description "Deletes a work item." \ | ||||
|                   " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice." | ||||
| 
 | ||||
|       graphql_name 'WorkItemDelete' | ||||
| 
 | ||||
|       authorize :delete_work_item | ||||
| 
 | ||||
|       argument :id, ::Types::GlobalIDType[::WorkItem], | ||||
|  |  | |||
|  | @ -3,12 +3,11 @@ | |||
| module Mutations | ||||
|   module WorkItems | ||||
|     class Update < BaseMutation | ||||
|       include Mutations::SpamProtection | ||||
| 
 | ||||
|       graphql_name 'WorkItemUpdate' | ||||
|       description "Updates a work item by Global ID." \ | ||||
|                   " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice." | ||||
| 
 | ||||
|       graphql_name 'WorkItemUpdate' | ||||
|       include Mutations::SpamProtection | ||||
| 
 | ||||
|       authorize :update_work_item | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,10 +5,11 @@ module Types | |||
|     module Analytics | ||||
|       module UsageTrends | ||||
|         class MeasurementType < BaseObject | ||||
|           include Gitlab::Graphql::Authorize::AuthorizeResource | ||||
|           graphql_name 'UsageTrendsMeasurement' | ||||
|           description 'Represents a recorded measurement (object count) for the Admins' | ||||
| 
 | ||||
|           include Gitlab::Graphql::Authorize::AuthorizeResource | ||||
| 
 | ||||
|           authorize :read_usage_trends_measurement | ||||
| 
 | ||||
|           field :recorded_at, Types::TimeType, null: true, | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| module Types | ||||
|   module AlertManagement | ||||
|     class PrometheusIntegrationType < ::Types::BaseObject | ||||
|       include ::Gitlab::Routing | ||||
| 
 | ||||
|       graphql_name 'AlertManagementPrometheusIntegration' | ||||
|       description 'An endpoint and credentials used to accept Prometheus alerts for a project' | ||||
| 
 | ||||
|       include ::Gitlab::Routing | ||||
| 
 | ||||
|       implements(Types::AlertManagement::IntegrationType) | ||||
| 
 | ||||
|       authorize :admin_project | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| module Types | ||||
|   # rubocop: disable Graphql/AuthorizeTypes | ||||
|   class BoardListType < BaseObject | ||||
|     include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|     graphql_name 'BoardList' | ||||
|     description 'Represents a list for an issue board' | ||||
| 
 | ||||
|     include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|     alias_method :list, :object | ||||
| 
 | ||||
|     field :id, GraphQL::Types::ID, | ||||
|  |  | |||
|  | @ -3,12 +3,13 @@ | |||
| module Types | ||||
|   module Ci | ||||
|     class RunnerType < BaseObject | ||||
|       graphql_name 'CiRunner' | ||||
| 
 | ||||
|       edge_type_class(RunnerWebUrlEdge) | ||||
|       connection_type_class(Types::CountableConnectionType) | ||||
|       graphql_name 'CiRunner' | ||||
| 
 | ||||
|       authorize :read_runner | ||||
|       present_using ::Ci::RunnerPresenter | ||||
| 
 | ||||
|       expose_permissions Types::PermissionTypes::Ci::Runner | ||||
| 
 | ||||
|       JOB_COUNT_LIMIT = 1000 | ||||
|  |  | |||
|  | @ -2,14 +2,14 @@ | |||
| 
 | ||||
| module Types | ||||
|   class GroupInvitationType < BaseObject | ||||
|     graphql_name 'GroupInvitation' | ||||
|     description 'Represents a Group Invitation' | ||||
| 
 | ||||
|     expose_permissions Types::PermissionTypes::Group | ||||
|     authorize :admin_group | ||||
| 
 | ||||
|     implements InvitationInterface | ||||
| 
 | ||||
|     graphql_name 'GroupInvitation' | ||||
|     description 'Represents a Group Invitation' | ||||
| 
 | ||||
|     field :group, Types::GroupType, null: true, | ||||
|           description: 'Group that a User is invited to.' | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,14 +2,14 @@ | |||
| 
 | ||||
| module Types | ||||
|   class GroupMemberType < BaseObject | ||||
|     graphql_name 'GroupMember' | ||||
|     description 'Represents a Group Membership' | ||||
| 
 | ||||
|     expose_permissions Types::PermissionTypes::Group | ||||
|     authorize :read_group | ||||
| 
 | ||||
|     implements MemberInterface | ||||
| 
 | ||||
|     graphql_name 'GroupMember' | ||||
|     description 'Represents a Group Membership' | ||||
| 
 | ||||
|     field :group, Types::GroupType, null: true, | ||||
|           description: 'Group that a User is a member of.' | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,11 +3,12 @@ | |||
| module Types | ||||
|   module MergeRequests | ||||
|     class AssigneeType < ::Types::UserType | ||||
|       graphql_name 'MergeRequestAssignee' | ||||
|       description 'A user assigned to a merge request.' | ||||
| 
 | ||||
|       include FindClosest | ||||
|       include ::Types::MergeRequests::InteractsWithMergeRequest | ||||
| 
 | ||||
|       graphql_name 'MergeRequestAssignee' | ||||
|       description 'A user assigned to a merge request.' | ||||
|       authorize :read_user | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -3,11 +3,12 @@ | |||
| module Types | ||||
|   module MergeRequests | ||||
|     class ReviewerType < ::Types::UserType | ||||
|       graphql_name 'MergeRequestReviewer' | ||||
|       description 'A user assigned to a merge request as a reviewer.' | ||||
| 
 | ||||
|       include FindClosest | ||||
|       include ::Types::MergeRequests::InteractsWithMergeRequest | ||||
| 
 | ||||
|       graphql_name 'MergeRequestReviewer' | ||||
|       description 'A user assigned to a merge request as a reviewer.' | ||||
|       authorize :read_user | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ module Types | |||
|   module Metrics | ||||
|     module Dashboards | ||||
|       class AnnotationType < ::Types::BaseObject | ||||
|         authorize :read_metrics_dashboard_annotation | ||||
|         graphql_name 'MetricsDashboardAnnotation' | ||||
|         authorize :read_metrics_dashboard_annotation | ||||
| 
 | ||||
|         field :description, GraphQL::Types::String, null: true, | ||||
|               description: 'Description of the annotation.' | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
| 
 | ||||
| module Types | ||||
|   class MutationType < BaseObject | ||||
|     include Gitlab::Graphql::MountMutation | ||||
| 
 | ||||
|     graphql_name 'Mutation' | ||||
| 
 | ||||
|     include Gitlab::Graphql::MountMutation | ||||
| 
 | ||||
|     mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs | ||||
|     mount_mutation Mutations::AlertManagement::CreateAlertIssue | ||||
|     mount_mutation Mutations::AlertManagement::UpdateAlertStatus | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Types | ||||
|   module Notes | ||||
|     class DiscussionType < BaseObject | ||||
|       DiscussionID = ::Types::GlobalIDType[::Discussion] | ||||
| 
 | ||||
|       graphql_name 'Discussion' | ||||
| 
 | ||||
|       DiscussionID = ::Types::GlobalIDType[::Discussion] | ||||
| 
 | ||||
|       authorize :read_note | ||||
| 
 | ||||
|       implements(Types::ResolvableInterface) | ||||
|  |  | |||
|  | @ -3,10 +3,11 @@ | |||
| module Types | ||||
|   module Packages | ||||
|     class PackageDetailsType < PackageType | ||||
|       include ::PackagesHelper | ||||
| 
 | ||||
|       graphql_name 'PackageDetailsType' | ||||
|       description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes' | ||||
| 
 | ||||
|       include ::PackagesHelper | ||||
| 
 | ||||
|       authorize :read_package | ||||
| 
 | ||||
|       field :versions, ::Types::Packages::PackageType.connection_type, null: true, | ||||
|  |  | |||
|  | @ -3,8 +3,8 @@ | |||
| module Types | ||||
|   module PermissionTypes | ||||
|     class Issue < BasePermissionType | ||||
|       description 'Check permissions for the current user on a issue' | ||||
|       graphql_name 'IssuePermissions' | ||||
|       description 'Check permissions for the current user on a issue' | ||||
| 
 | ||||
|       abilities :read_issue, :admin_issue, :update_issue, :reopen_issue, | ||||
|                 :read_design, :create_design, :destroy_design, | ||||
|  |  | |||
|  | @ -3,15 +3,16 @@ | |||
| module Types | ||||
|   module PermissionTypes | ||||
|     class MergeRequest < BasePermissionType | ||||
|       graphql_name 'MergeRequestPermissions' | ||||
|       description 'Check permissions for the current user on a merge request' | ||||
| 
 | ||||
|       present_using MergeRequestPresenter | ||||
| 
 | ||||
|       PERMISSION_FIELDS = %i[push_to_source_branch | ||||
|                              remove_source_branch | ||||
|                              cherry_pick_on_current_merge_request | ||||
|                              revert_on_current_merge_request].freeze | ||||
| 
 | ||||
|       present_using MergeRequestPresenter | ||||
|       description 'Check permissions for the current user on a merge request' | ||||
|       graphql_name 'MergeRequestPermissions' | ||||
| 
 | ||||
|       abilities :read_merge_request, :admin_merge_request, | ||||
|                 :update_merge_request, :create_note | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Types | ||||
|   # rubocop: disable Graphql/AuthorizeTypes | ||||
|   class QueryComplexityType < ::Types::BaseObject | ||||
|     ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity } | ||||
| 
 | ||||
|     graphql_name 'QueryComplexity' | ||||
| 
 | ||||
|     ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity } | ||||
| 
 | ||||
|     alias_method :query, :object | ||||
| 
 | ||||
|     field :limit, GraphQL::Types::Int, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Types | |||
|     # rubocop: disable Graphql/AuthorizeTypes | ||||
|     # This is presented through `Repository` that has its own authorization | ||||
|     class BlobType < BaseObject | ||||
|       present_using BlobPresenter | ||||
| 
 | ||||
|       graphql_name 'RepositoryBlob' | ||||
| 
 | ||||
|       present_using BlobPresenter | ||||
| 
 | ||||
|       field :id, GraphQL::Types::ID, null: false, | ||||
|             description: 'ID of the blob.' | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| module Types | ||||
|   module Terraform | ||||
|     class StateVersionType < BaseObject | ||||
|       include ::API::Helpers::RelatedResourcesHelpers | ||||
| 
 | ||||
|       graphql_name 'TerraformStateVersion' | ||||
| 
 | ||||
|       include ::API::Helpers::RelatedResourcesHelpers | ||||
| 
 | ||||
|       authorize :read_terraform_state | ||||
| 
 | ||||
|       field :id, GraphQL::Types::ID, | ||||
|  |  | |||
|  | @ -4,12 +4,11 @@ module Types | |||
|     # rubocop: disable Graphql/AuthorizeTypes | ||||
|     # This is presented through `Repository` that has its own authorization | ||||
|     class BlobType < BaseObject | ||||
|       implements Types::Tree::EntryType | ||||
| 
 | ||||
|       present_using BlobPresenter | ||||
| 
 | ||||
|       graphql_name 'Blob' | ||||
| 
 | ||||
|       implements Types::Tree::EntryType | ||||
|       present_using BlobPresenter | ||||
| 
 | ||||
|       field :web_url, GraphQL::Types::String, null: true, | ||||
|             description: 'Web URL of the blob.' | ||||
|       field :web_path, GraphQL::Types::String, null: true, | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ module Types | |||
|     # rubocop: disable Graphql/AuthorizeTypes | ||||
|     # This is presented through `Repository` that has its own authorization | ||||
|     class SubmoduleType < BaseObject | ||||
|       implements Types::Tree::EntryType | ||||
| 
 | ||||
|       graphql_name 'Submodule' | ||||
| 
 | ||||
|       implements Types::Tree::EntryType | ||||
| 
 | ||||
|       field :web_url, type: GraphQL::Types::String, null: true, | ||||
|             description: 'Web URL for the sub-module.' | ||||
|       field :tree_url, type: GraphQL::Types::String, null: true, | ||||
|  |  | |||
|  | @ -4,13 +4,12 @@ module Types | |||
|     # rubocop: disable Graphql/AuthorizeTypes | ||||
|     # This is presented through `Repository` that has its own authorization | ||||
|     class TreeEntryType < BaseObject | ||||
|       implements Types::Tree::EntryType | ||||
| 
 | ||||
|       present_using TreeEntryPresenter | ||||
| 
 | ||||
|       graphql_name 'TreeEntry' | ||||
|       description 'Represents a directory' | ||||
| 
 | ||||
|       implements Types::Tree::EntryType | ||||
|       present_using TreeEntryPresenter | ||||
| 
 | ||||
|       field :web_url, GraphQL::Types::String, null: true, | ||||
|             description: 'Web URL for the tree entry (directory).' | ||||
|       field :web_path, GraphQL::Types::String, null: true, | ||||
|  |  | |||
|  | @ -12,6 +12,8 @@ module Ci | |||
|       initial_branch = params[:branch_name] | ||||
|       latest_commit = project.repository.commit(initial_branch) || project.commit | ||||
|       commit_sha = latest_commit ? latest_commit.sha : '' | ||||
|       total_branches = project.repository_exists? ? project.repository.branch_count : 0 | ||||
| 
 | ||||
|       { | ||||
|         "ci-config-path": project.ci_config_path_or_default, | ||||
|         "ci-examples-help-page-path" => help_page_path('ci/examples/index'), | ||||
|  | @ -29,7 +31,7 @@ module Ci | |||
|         "project-full-path" => project.full_path, | ||||
|         "project-namespace" => project.namespace.full_path, | ||||
|         "runner-help-page-path" => help_page_path('ci/runners/index'), | ||||
|         "total-branches" => project.repository.branches.length, | ||||
|         "total-branches" => total_branches, | ||||
|         "yml-help-page-path" => help_page_path('ci/yaml/index') | ||||
|       } | ||||
|     end | ||||
|  |  | |||
|  | @ -0,0 +1,28 @@ | |||
| -# This partial renders a GlToggle root element. | ||||
| -# To actually initialize the component, make sure to call the initToggle helper from ~/toggles. | ||||
| 
 | ||||
| - classes = local_assigns.fetch(:classes) | ||||
| - name = local_assigns.fetch(:name, nil) | ||||
| - is_checked = local_assigns.fetch(:is_checked, false).to_s | ||||
| - disabled = local_assigns.fetch(:disabled, false).to_s | ||||
| - is_loading = local_assigns.fetch(:is_loading, false).to_s | ||||
| - label = local_assigns.fetch(:label, nil) | ||||
| - help = local_assigns.fetch(:help, nil) | ||||
| - label_position = local_assigns.fetch(:label_position, nil) | ||||
| - data = local_assigns.fetch(:data, {}) | ||||
| 
 | ||||
| %span{ class: classes, | ||||
|   data: { name: name, | ||||
|     is_checked: is_checked, | ||||
|     disabled: disabled, | ||||
|     is_loading: is_loading, | ||||
|     label: label, | ||||
|     help: help, | ||||
|     label_position: label_position, | ||||
|     **data } } | ||||
| 
 | ||||
| -# Leverage this block to render a rich help text. To render a plain text help text, | ||||
| -# prefer the `help` parameter. | ||||
| - if yield.present? | ||||
|   .gl-text-secondary.gl-mt-1 | ||||
|     = yield | ||||
|  | @ -0,0 +1,24 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class StartBackfillCiQueuingTables < Gitlab::Database::Migration[1.0] | ||||
|   MIGRATION = 'BackfillCiQueuingTables' | ||||
|   BATCH_SIZE = 500 | ||||
|   DELAY_INTERVAL = 2.minutes | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     return if Gitlab.com? | ||||
| 
 | ||||
|     queue_background_migration_jobs_by_range_at_intervals( | ||||
|       Gitlab::BackgroundMigration::BackfillCiQueuingTables::Ci::Build.pending, | ||||
|       MIGRATION, | ||||
|       DELAY_INTERVAL, | ||||
|       batch_size: BATCH_SIZE, | ||||
|       track_jobs: true) | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     # no-op | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| dbe6760198b8fa068c30871a439298e56802867044a178baa6b8b009f8da13e6 | ||||
|  | @ -528,7 +528,7 @@ You can use it either for personal or business websites, such as portfolios, doc | |||
| 
 | ||||
| #### GitLab Runner | ||||
| 
 | ||||
| - [Project page](https://gitlab.com/gitlab-org/gitlab-runner/blob/master/README.md) | ||||
| - [Project page](https://gitlab.com/gitlab-org/gitlab-runner/blob/main/README.md) | ||||
| - Configuration: | ||||
|   - [Omnibus](https://docs.gitlab.com/runner/) | ||||
|   - [Charts](https://docs.gitlab.com/runner/install/kubernetes.html) | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ the GitLab team to run the job. | |||
| If you want to know the in-depth details, here's what's really happening: | ||||
| 
 | ||||
| 1. You manually run the `review-docs-deploy` job in a merge request. | ||||
| 1. The job runs the [`scripts/trigger-build`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/trigger-build) | ||||
| 1. The job runs the [`scripts/trigger-build.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/trigger-build.rb) | ||||
|    script with the `docs deploy` flag, which triggers the "Triggered from `gitlab-org/gitlab` 'review-docs-deploy' job" | ||||
|    pipeline trigger in the `gitlab-org/gitlab-docs` project for the `$DOCS_BRANCH` (defaults to `main`). | ||||
| 1. The preview URL is shown both at the job output and in the merge request | ||||
|  |  | |||
|  | @ -99,7 +99,7 @@ The pipeline in the `gitlab-docs` project: | |||
| 
 | ||||
| Once a week on Mondays, a scheduled pipeline runs and rebuilds the Docker images | ||||
| used in various pipeline jobs, like `docs-lint`. The Docker image configuration files are | ||||
| located in the [Dockerfiles directory](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/dockerfiles). | ||||
| located in the [Dockerfiles directory](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/main/dockerfiles). | ||||
| 
 | ||||
| If you need to rebuild the Docker images immediately (must have maintainer level permissions): | ||||
| 
 | ||||
|  |  | |||
|  | @ -199,7 +199,7 @@ You can find Vale configuration in the following projects: | |||
| - [`gitlab-runner`](https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main/docs/.vale/gitlab) | ||||
| - [`omnibus-gitlab`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/tree/master/doc/.vale/gitlab) | ||||
| - [`charts`](https://gitlab.com/gitlab-org/charts/gitlab/-/tree/master/doc/.vale/gitlab) | ||||
| - [`gitlab-development-kit`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/master/doc/.vale/gitlab) | ||||
| - [`gitlab-development-kit`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/main/doc/.vale/gitlab) | ||||
| 
 | ||||
| This configuration is also used in build pipelines, where | ||||
| [error-level rules](#vale-result-types) are enforced. | ||||
|  |  | |||
|  | @ -1389,7 +1389,7 @@ The JSON report artifacts are not a public API of DAST and their format is expec | |||
| 
 | ||||
| The DAST tool always emits a JSON report file called `gl-dast-report.json` and | ||||
| sample reports can be found in the | ||||
| [DAST repository](https://gitlab.com/gitlab-org/security-products/dast/-/tree/master/test/end-to-end/expect). | ||||
| [DAST repository](https://gitlab.com/gitlab-org/security-products/dast/-/tree/main/test/end-to-end/expect). | ||||
| 
 | ||||
| ## Optimizing DAST | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ group: Product Planning | |||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments | ||||
| --- | ||||
| 
 | ||||
| # Planning hierarchies **(PREMIUM)** | ||||
| # Planning hierarchies **(FREE)** | ||||
| 
 | ||||
| Planning hierarchies are an integral part of breaking down your work in GitLab. | ||||
| To understand how you can use epics and issues together in hierarchies, remember the following: | ||||
|  | @ -22,7 +22,7 @@ portfolio management, see | |||
| 
 | ||||
| ## View planning hierarchies | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8 and is behind the feature flag `work_items_hierarchy`. | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8. | ||||
| 
 | ||||
| To view the planning hierarchy in a project: | ||||
| 
 | ||||
|  | @ -34,7 +34,7 @@ The work items outside your subscription plan show up below **Unavailable struct | |||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## Hierarchies with epics | ||||
| ## Hierarchies with epics **(PREMIUM)** | ||||
| 
 | ||||
| With epics, you can achieve the following hierarchy: | ||||
| 
 | ||||
|  | @ -68,14 +68,14 @@ Epic "1"*-- "0..*" Issue | |||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## View ancestry of an epic | ||||
| 
 | ||||
| In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## View ancestry of an issue | ||||
| 
 | ||||
| In an issue, you can view the parented epic above the issue in the right sidebar under **Epic**. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## View ancestry of an epic **(PREMIUM)** | ||||
| 
 | ||||
| In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**. | ||||
| 
 | ||||
|  | ||||
|  |  | |||
|  | @ -0,0 +1,153 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module BackgroundMigration | ||||
|     # Ensure queuing entries are present even if admins skip upgrades. | ||||
|     class BackfillCiQueuingTables | ||||
|       class Namespace < ActiveRecord::Base # rubocop:disable Style/Documentation | ||||
|         self.table_name = 'namespaces' | ||||
|         self.inheritance_column = :_type_disabled | ||||
|       end | ||||
| 
 | ||||
|       class Project < ActiveRecord::Base # rubocop:disable Style/Documentation | ||||
|         self.table_name = 'projects' | ||||
| 
 | ||||
|         belongs_to :namespace | ||||
|         has_one :ci_cd_settings, class_name: 'Gitlab::BackgroundMigration::BackfillCiQueuingTables::ProjectCiCdSetting' | ||||
| 
 | ||||
|         def group_runners_enabled? | ||||
|           return false unless ci_cd_settings | ||||
| 
 | ||||
|           ci_cd_settings.group_runners_enabled? | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       class ProjectCiCdSetting < ActiveRecord::Base # rubocop:disable Style/Documentation | ||||
|         self.table_name = 'project_ci_cd_settings' | ||||
|       end | ||||
| 
 | ||||
|       class Taggings < ActiveRecord::Base # rubocop:disable Style/Documentation | ||||
|         self.table_name = 'taggings' | ||||
|       end | ||||
| 
 | ||||
|       module Ci | ||||
|         class Build < ActiveRecord::Base # rubocop:disable Style/Documentation | ||||
|           include EachBatch | ||||
| 
 | ||||
|           self.table_name = 'ci_builds' | ||||
|           self.inheritance_column = :_type_disabled | ||||
| 
 | ||||
|           belongs_to :project | ||||
| 
 | ||||
|           scope :pending, -> do | ||||
|             where(status: :pending, type: 'Ci::Build', runner_id: nil) | ||||
|           end | ||||
| 
 | ||||
|           def self.each_batch(of: 1000, column: :id, order: { runner_id: :asc, id: :asc }, order_hint: nil) | ||||
|             start = except(:select).select(column).reorder(order) | ||||
|             start = start.take | ||||
|             return unless start | ||||
| 
 | ||||
|             start_id = start[column] | ||||
|             arel_table = self.arel_table | ||||
| 
 | ||||
|             1.step do |index| | ||||
|               start_cond = arel_table[column].gteq(start_id) | ||||
|               stop = except(:select).select(column).where(start_cond).reorder(order) | ||||
|               stop = stop.offset(of).limit(1).take | ||||
|               relation = where(start_cond) | ||||
| 
 | ||||
|               if stop | ||||
|                 stop_id = stop[column] | ||||
|                 start_id = stop_id | ||||
|                 stop_cond = arel_table[column].lt(stop_id) | ||||
|                 relation = relation.where(stop_cond) | ||||
|               end | ||||
| 
 | ||||
|               # Any ORDER BYs are useless for this relation and can lead to less | ||||
|               # efficient UPDATE queries, hence we get rid of it. | ||||
|               relation = relation.except(:order) | ||||
| 
 | ||||
|               # Using unscoped is necessary to prevent leaking the current scope used by | ||||
|               # ActiveRecord to chain `each_batch` method. | ||||
|               unscoped { yield relation, index } | ||||
| 
 | ||||
|               break unless stop | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           def tags_ids | ||||
|             BackfillCiQueuingTables::Taggings | ||||
|               .where(taggable_id: id, taggable_type: 'CommitStatus') | ||||
|               .pluck(:tag_id) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         class PendingBuild < ActiveRecord::Base # rubocop:disable Style/Documentation | ||||
|           self.table_name = 'ci_pending_builds' | ||||
| 
 | ||||
|           class << self | ||||
|             def upsert_from_build!(build) | ||||
|               entry = self.new(args_from_build(build)) | ||||
| 
 | ||||
|               self.upsert( | ||||
|                 entry.attributes.compact, | ||||
|                 returning: %w[build_id], | ||||
|                 unique_by: :build_id) | ||||
|             end | ||||
| 
 | ||||
|             def args_from_build(build) | ||||
|               project = build.project | ||||
| 
 | ||||
|               { | ||||
|                 build_id: build.id, | ||||
|                 project_id: build.project_id, | ||||
|                 protected: build.protected?, | ||||
|                 namespace_id: project.namespace_id, | ||||
|                 tag_ids: build.tags_ids, | ||||
|                 instance_runners_enabled: project.shared_runners_enabled?, | ||||
|                 namespace_traversal_ids: namespace_traversal_ids(project) | ||||
|               } | ||||
|             end | ||||
| 
 | ||||
|             def namespace_traversal_ids(project) | ||||
|               if project.group_runners_enabled? | ||||
|                 project.namespace.traversal_ids | ||||
|               else | ||||
|                 [] | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       BATCH_SIZE = 100 | ||||
| 
 | ||||
|       def perform(start_id, end_id) | ||||
|         scope = BackfillCiQueuingTables::Ci::Build.pending.where(id: start_id..end_id) | ||||
|         pending_builds_query = BackfillCiQueuingTables::Ci::PendingBuild | ||||
|           .where('ci_builds.id = ci_pending_builds.build_id') | ||||
|           .select(1) | ||||
| 
 | ||||
|         scope.each_batch(of: BATCH_SIZE) do |builds| | ||||
|           builds = builds.where('NOT EXISTS (?)', pending_builds_query) | ||||
|           builds = builds.includes(:project, project: [:namespace, :ci_cd_settings]) | ||||
| 
 | ||||
|           builds.each do |build| | ||||
|             BackfillCiQueuingTables::Ci::PendingBuild.upsert_from_build!(build) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         mark_job_as_succeeded(start_id, end_id) | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def mark_job_as_succeeded(*arguments) | ||||
|         Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded( | ||||
|           self.class.name.demodulize, | ||||
|            arguments) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -57,8 +57,8 @@ module SystemCheck | |||
|           WHERE (p.repository_storage LIKE ?) | ||||
|         " | ||||
| 
 | ||||
|         query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) # rubocop:disable GitlabSecurity/PublicSend | ||||
|         ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) || [] | ||||
|         query = ::Project.sanitize_sql_array([sql, storage_name]) | ||||
|         ::Project.connection.select_all(query).rows.try(:flatten!) || [] | ||||
|       end | ||||
| 
 | ||||
|       def fetch_disk_namespaces(storage_path) | ||||
|  |  | |||
|  | @ -14377,6 +14377,9 @@ msgstr "" | |||
| msgid "Escalation policies must have at least one rule" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Escalation policy" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Escalation policy:" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -19112,6 +19115,12 @@ msgstr "" | |||
| msgid "IncidentManagement|Open" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IncidentManagement|Page your team with escalation policies" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IncidentManagement|Paged" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IncidentManagement|Published" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -19139,6 +19148,9 @@ msgstr "" | |||
| msgid "IncidentManagement|Unpublished" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IncidentManagement|Use escalation policies to automatically page your team when incidents are created." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IncidentSettings|Activate \"time to SLA\" countdown timer" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -19184,7 +19196,10 @@ msgstr "" | |||
| msgid "Incidents" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incidents|Add a URL" | ||||
| msgid "Incidents|Add image details" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident" | ||||
|  | @ -19199,10 +19214,10 @@ msgstr "" | |||
| msgid "Incidents|There was an issue loading metric images." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incidents|There was an issue uploading your image." | ||||
| msgid "Incidents|There was an issue updating your image." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incidents|You can optionally add a URL to link users to the original graph." | ||||
| msgid "Incidents|There was an issue uploading your image." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incident|Alert details" | ||||
|  | @ -19211,9 +19226,18 @@ msgstr "" | |||
| msgid "Incident|Are you sure you wish to delete this image?" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incident|Delete image" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incident|Deleting %{filename}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incident|Edit image text or link" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incident|Editing %{filename}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Incident|Metrics" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -21740,6 +21764,9 @@ msgstr "" | |||
| msgid "Link" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Link (optional)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Link Prometheus monitoring to GitLab." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -35999,6 +36026,9 @@ msgstr "" | |||
| msgid "Tests" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Text (optional)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Text added to the body of all email messages. %{character_limit} character limit" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ module QA | |||
|             end | ||||
| 
 | ||||
|             base.view 'app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue' do | ||||
|               element :milestone_link, 'data-qa-selector="`${issuableAttribute}_link`"' # rubocop:disable QA/ElementWithPattern | ||||
|               element :milestone_link, 'data-qa-selector="`${formatIssuableAttribute.snake}_link`"' # rubocop:disable QA/ElementWithPattern | ||||
|             end | ||||
| 
 | ||||
|             base.view 'app/assets/javascripts/sidebar/components/sidebar_editable_item.vue' do | ||||
|  |  | |||
|  | @ -21,6 +21,12 @@ module Trigger | |||
|     variable_value | ||||
|   end | ||||
| 
 | ||||
|   def self.variables_for_env_file(variables) | ||||
|     variables.map do |key, value| | ||||
|       %Q(#{key}=#{value}) | ||||
|     end.join("\n") | ||||
|   end | ||||
| 
 | ||||
|   class Base | ||||
|     # Can be overridden | ||||
|     def self.access_token | ||||
|  | @ -57,6 +63,21 @@ module Trigger | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def variables | ||||
|       simple_forwarded_variables.merge(base_variables, extra_variables, version_file_variables) | ||||
|     end | ||||
| 
 | ||||
|     def simple_forwarded_variables | ||||
|       { | ||||
|         'TRIGGER_SOURCE' => ENV['CI_JOB_URL'], | ||||
|         'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'], | ||||
|         'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME'], | ||||
|         'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'], | ||||
|         'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => ENV['CI_MERGE_REQUEST_PROJECT_ID'], | ||||
|         'TOP_UPSTREAM_MERGE_REQUEST_IID' => ENV['CI_MERGE_REQUEST_IID'] | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     # Override to trigger and work with pipeline on different GitLab instance | ||||
|  | @ -95,23 +116,13 @@ module Trigger | |||
|       ENV[version_file]&.strip || File.read(version_file).strip | ||||
|     end | ||||
| 
 | ||||
|     def variables | ||||
|       base_variables.merge(extra_variables).merge(version_file_variables) | ||||
|     end | ||||
| 
 | ||||
|     def base_variables | ||||
|       # Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA for omnibus checkouts due to pipeline for merged results, | ||||
|       # and fallback to CI_COMMIT_SHA for the `detached` pipelines. | ||||
|       { | ||||
|         'GITLAB_REF_SLUG' => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : ENV['CI_COMMIT_REF_SLUG'], | ||||
|         'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'], | ||||
|         'TRIGGER_SOURCE' => ENV['CI_JOB_URL'], | ||||
|         'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'], | ||||
|         'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'], | ||||
|         'TOP_UPSTREAM_SOURCE_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'], | ||||
|         'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME'], | ||||
|         'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => ENV['CI_MERGE_REQUEST_PROJECT_ID'], | ||||
|         'TOP_UPSTREAM_MERGE_REQUEST_IID' => ENV['CI_MERGE_REQUEST_IID'] | ||||
|         'TOP_UPSTREAM_SOURCE_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'] | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|  | @ -163,17 +174,16 @@ module Trigger | |||
|   end | ||||
| 
 | ||||
|   class CNG < Base | ||||
|     def self.access_token | ||||
|       # Default to "Multi-pipeline (from 'gitlab-org/gitlab' 'cloud-native-image' job)" at https://gitlab.com/gitlab-org/build/CNG/-/settings/access_tokens | ||||
|       ENV['CNG_PROJECT_ACCESS_TOKEN'] || super | ||||
|     def variables | ||||
|       # Delete variables that aren't useful when using native triggers. | ||||
|       super.tap do |hash| | ||||
|         hash.delete('TRIGGER_SOURCE') | ||||
|         hash.delete('TRIGGERED_USER') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def downstream_project_path | ||||
|       ENV.fetch('CNG_PROJECT_PATH', 'gitlab-org/build/CNG') | ||||
|     end | ||||
| 
 | ||||
|     def ref | ||||
|       return ENV['CI_COMMIT_REF_NAME'] if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/ | ||||
| 
 | ||||
|  | @ -181,17 +191,17 @@ module Trigger | |||
|     end | ||||
| 
 | ||||
|     def extra_variables | ||||
|       edition = Trigger.ee? ? 'EE' : 'CE' | ||||
|       # Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA (MR HEAD commit) so that the image is in sync with the assets and QA images. | ||||
|       source_sha = Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'] | ||||
| 
 | ||||
|       { | ||||
|         "ee" => Trigger.ee? ? "true" : "false", | ||||
|         "TRIGGER_BRANCH" => ref, | ||||
|         "GITLAB_VERSION" => source_sha, | ||||
|         "GITLAB_TAG" => ENV['CI_COMMIT_TAG'], | ||||
|         "GITLAB_TAG" => ENV['CI_COMMIT_TAG'], # Always set a value, even an empty string, so that the downstream pipeline can correctly check it. | ||||
|         "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : source_sha, | ||||
|         "FORCE_RAILS_IMAGE_BUILDS" => 'true', | ||||
|         "#{edition}_PIPELINE" => 'true' | ||||
|         "CE_PIPELINE" => Trigger.ee? ? nil : "true", # Always set a value, even an empty string, so that the downstream pipeline can correctly check it. | ||||
|         "EE_PIPELINE" => Trigger.ee? ? "true" : nil # Always set a value, even an empty string, so that the downstream pipeline can correctly check it. | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|  | @ -445,14 +455,15 @@ module Trigger | |||
|   Job = Class.new(Pipeline) | ||||
| end | ||||
| 
 | ||||
| case ARGV[0] | ||||
| when 'omnibus' | ||||
| if $0 == __FILE__ | ||||
|   case ARGV[0] | ||||
|   when 'omnibus' | ||||
|     Trigger::Omnibus.new.invoke!(post_comment: true, downstream_job_name: 'Trigger:qa-test').wait! | ||||
| when 'cng' | ||||
|   when 'cng' | ||||
|     Trigger::CNG.new.invoke!.wait! | ||||
| when 'gitlab-com-database-testing' | ||||
|   when 'gitlab-com-database-testing' | ||||
|     Trigger::DatabaseTesting.new.invoke! | ||||
| when 'docs' | ||||
|   when 'docs' | ||||
|     docs_trigger = Trigger::Docs.new | ||||
| 
 | ||||
|     case ARGV[1] | ||||
|  | @ -464,9 +475,10 @@ when 'docs' | |||
|       puts 'usage: trigger-build docs <deploy|cleanup>' | ||||
|       exit 1 | ||||
|     end | ||||
| else | ||||
|   else | ||||
|     puts "Please provide a valid option: | ||||
|     omnibus - Triggers a pipeline that builds the omnibus-gitlab package | ||||
|     cng - Triggers a pipeline that builds images used by the GitLab helm chart | ||||
|     gitlab-com-database-testing - Triggers a pipeline that tests database changes on GitLab.com data" | ||||
|   end | ||||
| end | ||||
|  | @ -61,6 +61,15 @@ FactoryBot.define do | |||
|     factory :incident do | ||||
|       issue_type { :incident } | ||||
|       association :work_item_type, :default, :incident | ||||
| 
 | ||||
|       # An escalation status record is created for all incidents | ||||
|       # in app code. This is a trait to avoid creating escalation | ||||
|       # status records in specs which do not need them. | ||||
|       trait :with_escalation_status do | ||||
|         after(:create) do |incident| | ||||
|           create(:incident_management_issuable_escalation_status, issue: incident) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ import { createAlert } from '~/flash'; | |||
| 
 | ||||
| import { getIdFromGraphQLId } from '~/graphql_shared/utils'; | ||||
| import RunnerHeader from '~/runner/components/runner_header.vue'; | ||||
| import RunnerDetails from '~/runner/components/runner_details.vue'; | ||||
| import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; | ||||
| import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; | ||||
| import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql'; | ||||
|  | @ -30,7 +29,6 @@ describe('AdminRunnerShowApp', () => { | |||
|   let mockRunnerQuery; | ||||
| 
 | ||||
|   const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); | ||||
|   const findRunnerDetails = () => wrapper.findComponent(RunnerDetails); | ||||
|   const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); | ||||
|   const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); | ||||
| 
 | ||||
|  | @ -80,8 +78,7 @@ describe('AdminRunnerShowApp', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('shows basic runner details', async () => { | ||||
|       const expected = `Details
 | ||||
|                         Description Instance runner | ||||
|       const expected = `Description Instance runner
 | ||||
|                         Last contact Never contacted | ||||
|                         Version 1.0.0 | ||||
|                         IP Address 127.0.0.1 | ||||
|  | @ -89,7 +86,7 @@ describe('AdminRunnerShowApp', () => { | |||
|                         Maximum job timeout None | ||||
|                         Tags None`.replace(/\s+/g, ' ');
 | ||||
| 
 | ||||
|       expect(findRunnerDetails().text()).toMatchInterpolatedText(expected); | ||||
|       expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected); | ||||
|     }); | ||||
| 
 | ||||
|     describe('when runner cannot be updated', () => { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { GlSprintf, GlIntersperse } from '@gitlab/ui'; | ||||
| import { createWrapper, ErrorWrapper } from '@vue/test-utils'; | ||||
| import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; | ||||
| import { useFakeDate } from 'helpers/fake_date'; | ||||
| import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; | ||||
|  | @ -8,6 +8,8 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner | |||
| import RunnerDetails from '~/runner/components/runner_details.vue'; | ||||
| import RunnerDetail from '~/runner/components/runner_detail.vue'; | ||||
| import RunnerGroups from '~/runner/components/runner_groups.vue'; | ||||
| import RunnerTags from '~/runner/components/runner_tags.vue'; | ||||
| import RunnerTag from '~/runner/components/runner_tag.vue'; | ||||
| 
 | ||||
| import { runnerData, runnerWithGroupData } from '../mock_data'; | ||||
| 
 | ||||
|  | @ -37,16 +39,14 @@ describe('RunnerDetails', () => { | |||
| 
 | ||||
|   const findDetailGroups = () => wrapper.findComponent(RunnerGroups); | ||||
| 
 | ||||
|   const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { | ||||
|   const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => { | ||||
|     wrapper = mountFn(RunnerDetails, { | ||||
|       propsData: { | ||||
|         ...props, | ||||
|       }, | ||||
|       stubs: { | ||||
|         GlIntersperse, | ||||
|         GlSprintf, | ||||
|         TimeAgo, | ||||
|         RunnerDetail, | ||||
|         ...stubs, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
|  | @ -65,6 +65,7 @@ describe('RunnerDetails', () => { | |||
|     expect(wrapper.text()).toBe(''); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Details tab', () => { | ||||
|     describe.each` | ||||
|       field                    | runner                                                             | expectedValue | ||||
|       ${'Description'}         | ${{ description: 'My runner' }}                                    | ${'My runner'} | ||||
|  | @ -92,6 +93,11 @@ describe('RunnerDetails', () => { | |||
|               ...runner, | ||||
|             }, | ||||
|           }, | ||||
|           stubs: { | ||||
|             GlIntersperse, | ||||
|             GlSprintf, | ||||
|             TimeAgo, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|  | @ -101,12 +107,14 @@ describe('RunnerDetails', () => { | |||
|     }); | ||||
| 
 | ||||
|     describe('"Tags" field', () => { | ||||
|       const stubs = { RunnerTags, RunnerTag }; | ||||
| 
 | ||||
|       it('displays expected value "tag-1 tag-2"', () => { | ||||
|         createComponent({ | ||||
|           props: { | ||||
|             runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] }, | ||||
|           }, | ||||
|         mountFn: mountExtended, | ||||
|           stubs, | ||||
|         }); | ||||
| 
 | ||||
|         expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2'); | ||||
|  | @ -117,7 +125,7 @@ describe('RunnerDetails', () => { | |||
|           props: { | ||||
|             runner: { ...mockRunner, tagList: [] }, | ||||
|           }, | ||||
|         mountFn: mountExtended, | ||||
|           stubs, | ||||
|         }); | ||||
| 
 | ||||
|         expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None'); | ||||
|  | @ -137,4 +145,5 @@ describe('RunnerDetails', () => { | |||
|         expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -0,0 +1,65 @@ | |||
| import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils'; | ||||
| 
 | ||||
| describe('~/runner/utils', () => { | ||||
|   describe('formatJobCount', () => { | ||||
|     it('formats a number', () => { | ||||
|       expect(formatJobCount(1)).toBe('1'); | ||||
|       expect(formatJobCount(99)).toBe('99'); | ||||
|     }); | ||||
| 
 | ||||
|     it('formats a large count', () => { | ||||
|       expect(formatJobCount(1000)).toBe('1,000'); | ||||
|       expect(formatJobCount(1001)).toBe('1,000+'); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns an empty string for non-numeric values', () => { | ||||
|       expect(formatJobCount(undefined)).toBe(''); | ||||
|       expect(formatJobCount(null)).toBe(''); | ||||
|       expect(formatJobCount('number')).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('tableField', () => { | ||||
|     it('a field with options', () => { | ||||
|       expect(tableField({ key: 'name' })).toEqual({ | ||||
|         key: 'name', | ||||
|         label: '', | ||||
|         tdAttr: { 'data-testid': 'td-name' }, | ||||
|         thClass: expect.any(Array), | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('a field with a label', () => { | ||||
|       const label = 'A field name'; | ||||
| 
 | ||||
|       expect(tableField({ key: 'name', label })).toMatchObject({ | ||||
|         label, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('a field with custom classes', () => { | ||||
|       const mockClasses = ['foo', 'bar']; | ||||
| 
 | ||||
|       expect(tableField({ thClasses: mockClasses })).toMatchObject({ | ||||
|         thClass: expect.arrayContaining(mockClasses), | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getPaginationVariables', () => { | ||||
|     const after = 'AFTER_CURSOR'; | ||||
|     const before = 'BEFORE_CURSOR'; | ||||
| 
 | ||||
|     it.each` | ||||
|       case                         | pagination    | pageSize     | variables | ||||
|       ${'next page'}               | ${{ after }}  | ${undefined} | ${{ after, first: 10 }} | ||||
|       ${'prev page'}               | ${{ before }} | ${undefined} | ${{ before, last: 10 }} | ||||
|       ${'first page'}              | ${{}}         | ${undefined} | ${{ first: 10 }} | ||||
|       ${'next page with N items'}  | ${{ after }}  | ${20}        | ${{ after, first: 20 }} | ||||
|       ${'prev page with N items'}  | ${{ before }} | ${20}        | ${{ before, last: 20 }} | ||||
|       ${'first page with N items'} | ${{}}         | ${20}        | ${{ first: 20 }} | ||||
|     `('navigates to $case', ({ pagination, pageSize, variables }) => {
 | ||||
|       expect(getPaginationVariables(pagination, pageSize)).toEqual(variables); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,149 @@ | |||
| import { createWrapper } from '@vue/test-utils'; | ||||
| import { GlToggle } from '@gitlab/ui'; | ||||
| import { initToggle } from '~/toggles'; | ||||
| 
 | ||||
| // Selectors
 | ||||
| const TOGGLE_WRAPPER_CLASS = '.gl-toggle-wrapper'; | ||||
| const TOGGLE_LABEL_CLASS = '.gl-toggle-label'; | ||||
| const CHECKED_CLASS = '.is-checked'; | ||||
| const DISABLED_CLASS = '.is-disabled'; | ||||
| const LOADING_CLASS = '.toggle-loading'; | ||||
| const HELP_TEXT_SELECTOR = '[data-testid="toggle-help"]'; | ||||
| 
 | ||||
| // Toggle settings
 | ||||
| const toggleClassName = 'js-custom-toggle-class'; | ||||
| const toggleLabel = 'Toggle label'; | ||||
| 
 | ||||
| describe('toggles/index.js', () => { | ||||
|   let instance; | ||||
|   let toggleWrapper; | ||||
| 
 | ||||
|   const createRootEl = (dataAttrs) => { | ||||
|     const dataset = { | ||||
|       label: toggleLabel, | ||||
|       ...dataAttrs, | ||||
|     }; | ||||
|     const el = document.createElement('span'); | ||||
|     el.classList.add(toggleClassName); | ||||
| 
 | ||||
|     Object.entries(dataset).forEach(([key, value]) => { | ||||
|       el.dataset[key] = value; | ||||
|     }); | ||||
| 
 | ||||
|     document.body.appendChild(el); | ||||
| 
 | ||||
|     return el; | ||||
|   }; | ||||
| 
 | ||||
|   const initToggleWithOptions = (options = {}) => { | ||||
|     const el = createRootEl(options); | ||||
|     instance = initToggle(el); | ||||
|     toggleWrapper = document.querySelector(TOGGLE_WRAPPER_CLASS); | ||||
|   }; | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     document.body.innerHTML = ''; | ||||
|     instance = null; | ||||
|     toggleWrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('initToggle', () => { | ||||
|     describe('default state', () => { | ||||
|       beforeEach(() => { | ||||
|         initToggleWithOptions(); | ||||
|       }); | ||||
| 
 | ||||
|       it('attaches a GlToggle to the element', async () => { | ||||
|         expect(toggleWrapper).not.toBe(null); | ||||
|         expect(toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).textContent).toBe(toggleLabel); | ||||
|       }); | ||||
| 
 | ||||
|       it('passes CSS classes down to GlToggle', () => { | ||||
|         expect(toggleWrapper.className).toContain(toggleClassName); | ||||
|       }); | ||||
| 
 | ||||
|       it('is not checked', () => { | ||||
|         expect(toggleWrapper.querySelector(CHECKED_CLASS)).toBe(null); | ||||
|       }); | ||||
| 
 | ||||
|       it('is enabled', () => { | ||||
|         expect(toggleWrapper.querySelector(DISABLED_CLASS)).toBe(null); | ||||
|       }); | ||||
| 
 | ||||
|       it('is not loading', () => { | ||||
|         expect(toggleWrapper.querySelector(LOADING_CLASS)).toBe(null); | ||||
|       }); | ||||
| 
 | ||||
|       it('emits "change" event when value changes', () => { | ||||
|         const wrapper = createWrapper(instance); | ||||
|         const event = 'change'; | ||||
|         const listener = jest.fn(); | ||||
| 
 | ||||
|         instance.$on(event, listener); | ||||
| 
 | ||||
|         expect(listener).toHaveBeenCalledTimes(0); | ||||
| 
 | ||||
|         wrapper.find(GlToggle).vm.$emit(event, true); | ||||
| 
 | ||||
|         expect(listener).toHaveBeenCalledTimes(1); | ||||
|         expect(listener).toHaveBeenLastCalledWith(true); | ||||
| 
 | ||||
|         wrapper.find(GlToggle).vm.$emit(event, false); | ||||
| 
 | ||||
|         expect(listener).toHaveBeenCalledTimes(2); | ||||
|         expect(listener).toHaveBeenLastCalledWith(false); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('with custom options', () => { | ||||
|       const name = 'toggle-name'; | ||||
|       const help = 'Help text'; | ||||
|       const foo = 'bar'; | ||||
| 
 | ||||
|       beforeEach(() => { | ||||
|         initToggleWithOptions({ | ||||
|           name, | ||||
|           isChecked: true, | ||||
|           disabled: true, | ||||
|           isLoading: true, | ||||
|           help, | ||||
|           labelPosition: 'hidden', | ||||
|           foo, | ||||
|         }); | ||||
|         toggleWrapper = document.querySelector(TOGGLE_WRAPPER_CLASS); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the custom name', () => { | ||||
|         const input = toggleWrapper.querySelector('input[type="hidden"]'); | ||||
| 
 | ||||
|         expect(input.name).toBe(name); | ||||
|       }); | ||||
| 
 | ||||
|       it('is checked', () => { | ||||
|         expect(toggleWrapper.querySelector(CHECKED_CLASS)).not.toBe(null); | ||||
|       }); | ||||
| 
 | ||||
|       it('is disabled', () => { | ||||
|         expect(toggleWrapper.querySelector(DISABLED_CLASS)).not.toBe(null); | ||||
|       }); | ||||
| 
 | ||||
|       it('is loading', () => { | ||||
|         expect(toggleWrapper.querySelector(LOADING_CLASS)).not.toBe(null); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the custom help text', () => { | ||||
|         expect(toggleWrapper.querySelector(HELP_TEXT_SELECTOR).textContent).toBe(help); | ||||
|       }); | ||||
| 
 | ||||
|       it('hides the label', () => { | ||||
|         expect( | ||||
|           toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).classList.contains('gl-sr-only'), | ||||
|         ).toBe(true); | ||||
|       }); | ||||
| 
 | ||||
|       it('passes custom dataset to the wrapper', () => { | ||||
|         expect(toggleWrapper.dataset.foo).toBe('bar'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -88,6 +88,17 @@ RSpec.describe Ci::PipelineEditorHelper do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with a project with no repository' do | ||||
|       let(:project) { create(:project) } | ||||
| 
 | ||||
|       it 'returns pipeline editor data' do | ||||
|         expect(pipeline_editor_data).to include({ | ||||
|           "pipeline_etag" => '', | ||||
|           "total-branches" => 0 | ||||
|         }) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with a non-default branch name' do | ||||
|       let(:user) { create(:user) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,244 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration, schema: 20220208115439 do | ||||
|   let(:namespaces)      { table(:namespaces) } | ||||
|   let(:projects)        { table(:projects) } | ||||
|   let(:ci_cd_settings)  { table(:project_ci_cd_settings) } | ||||
|   let(:builds)          { table(:ci_builds) } | ||||
|   let(:queuing_entries) { table(:ci_pending_builds) } | ||||
|   let(:tags)            { table(:tags) } | ||||
|   let(:taggings)        { table(:taggings) } | ||||
| 
 | ||||
|   subject { described_class.new } | ||||
| 
 | ||||
|   describe '#perform' do | ||||
|     let!(:namespace) do | ||||
|       namespaces.create!( | ||||
|         id: 10, | ||||
|         name: 'namespace10', | ||||
|         path: 'namespace10', | ||||
|         traversal_ids: [10]) | ||||
|     end | ||||
| 
 | ||||
|     let!(:other_namespace) do | ||||
|       namespaces.create!( | ||||
|         id: 11, | ||||
|         name: 'namespace11', | ||||
|         path: 'namespace11', | ||||
|         traversal_ids: [11]) | ||||
|     end | ||||
| 
 | ||||
|     let!(:project) do | ||||
|       projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1') | ||||
|     end | ||||
| 
 | ||||
|     let!(:ci_cd_setting) do | ||||
|       ci_cd_settings.create!(id: 5, project_id: 5, group_runners_enabled: true) | ||||
|     end | ||||
| 
 | ||||
|     let!(:other_project) do | ||||
|       projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2') | ||||
|     end | ||||
| 
 | ||||
|     let!(:other_ci_cd_setting) do | ||||
|       ci_cd_settings.create!(id: 7, project_id: 7, group_runners_enabled: false) | ||||
|     end | ||||
| 
 | ||||
|     let!(:another_project) do | ||||
|       projects.create!(id: 9, namespace_id: 10, name: 'test3', path: 'test3', shared_runners_enabled: false) | ||||
|     end | ||||
| 
 | ||||
|     let!(:ruby_tag) do | ||||
|       tags.create!(id: 22, name: 'ruby') | ||||
|     end | ||||
| 
 | ||||
|     let!(:postgres_tag) do | ||||
|       tags.create!(id: 23, name: 'postgres') | ||||
|     end | ||||
| 
 | ||||
|     it 'creates ci_pending_builds for all pending builds in range' do | ||||
|       builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') | ||||
|       builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build') | ||||
|       builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build') | ||||
| 
 | ||||
|       taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 22) | ||||
|       taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23) | ||||
| 
 | ||||
|       builds.create!(id: 60, status: :pending, name: 'test1', project_id: 7, type: 'Ci::Build') | ||||
|       builds.create!(id: 61, status: :running, name: 'test2', project_id: 7, protected: true, type: 'Ci::Build') | ||||
|       builds.create!(id: 62, status: :pending, name: 'test3', project_id: 7, type: 'Ci::Build') | ||||
| 
 | ||||
|       taggings.create!(taggable_id: 60, taggable_type: 'CommitStatus', tag_id: 23) | ||||
|       taggings.create!(taggable_id: 62, taggable_type: 'CommitStatus', tag_id: 22) | ||||
| 
 | ||||
|       builds.create!(id: 70, status: :pending, name: 'test1', project_id: 9, protected: true, type: 'Ci::Build') | ||||
|       builds.create!(id: 71, status: :failed, name: 'test2', project_id: 9, type: 'Ci::Build') | ||||
|       builds.create!(id: 72, status: :pending, name: 'test3', project_id: 9, type: 'Ci::Build') | ||||
| 
 | ||||
|       taggings.create!(taggable_id: 71, taggable_type: 'CommitStatus', tag_id: 22) | ||||
| 
 | ||||
|       subject.perform(1, 100) | ||||
| 
 | ||||
|       expect(queuing_entries.all).to contain_exactly( | ||||
|         an_object_having_attributes( | ||||
|           build_id: 50, | ||||
|           project_id: 5, | ||||
|           namespace_id: 10, | ||||
|           protected: false, | ||||
|           instance_runners_enabled: true, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [], | ||||
|           namespace_traversal_ids: [10]), | ||||
|         an_object_having_attributes( | ||||
|           build_id: 52, | ||||
|           project_id: 5, | ||||
|           namespace_id: 10, | ||||
|           protected: true, | ||||
|           instance_runners_enabled: true, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [22, 23], | ||||
|           namespace_traversal_ids: [10]), | ||||
|         an_object_having_attributes( | ||||
|           build_id: 60, | ||||
|           project_id: 7, | ||||
|           namespace_id: 11, | ||||
|           protected: false, | ||||
|           instance_runners_enabled: true, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [23], | ||||
|           namespace_traversal_ids: []), | ||||
|         an_object_having_attributes( | ||||
|           build_id: 62, | ||||
|           project_id: 7, | ||||
|           namespace_id: 11, | ||||
|           protected: false, | ||||
|           instance_runners_enabled: true, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [22], | ||||
|           namespace_traversal_ids: []), | ||||
|         an_object_having_attributes( | ||||
|           build_id: 70, | ||||
|           project_id: 9, | ||||
|           namespace_id: 10, | ||||
|           protected: true, | ||||
|           instance_runners_enabled: false, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [], | ||||
|           namespace_traversal_ids: []), | ||||
|         an_object_having_attributes( | ||||
|           build_id: 72, | ||||
|           project_id: 9, | ||||
|           namespace_id: 10, | ||||
|           protected: false, | ||||
|           instance_runners_enabled: false, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [], | ||||
|           namespace_traversal_ids: []) | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     it 'skips builds that already have ci_pending_builds' do | ||||
|       builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') | ||||
|       builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build') | ||||
|       builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build') | ||||
| 
 | ||||
|       taggings.create!(taggable_id: 50, taggable_type: 'CommitStatus', tag_id: 22) | ||||
|       taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23) | ||||
| 
 | ||||
|       queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10) | ||||
| 
 | ||||
|       subject.perform(1, 100) | ||||
| 
 | ||||
|       expect(queuing_entries.all).to contain_exactly( | ||||
|         an_object_having_attributes( | ||||
|           build_id: 50, | ||||
|           project_id: 5, | ||||
|           namespace_id: 10, | ||||
|           protected: false, | ||||
|           instance_runners_enabled: false, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [], | ||||
|           namespace_traversal_ids: []), | ||||
|         an_object_having_attributes( | ||||
|           build_id: 52, | ||||
|           project_id: 5, | ||||
|           namespace_id: 10, | ||||
|           protected: true, | ||||
|           instance_runners_enabled: true, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [23], | ||||
|           namespace_traversal_ids: [10]) | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     it 'upserts values in case of conflicts' do | ||||
|       builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') | ||||
|       queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10) | ||||
| 
 | ||||
|       build = described_class::Ci::Build.find(50) | ||||
|       described_class::Ci::PendingBuild.upsert_from_build!(build) | ||||
| 
 | ||||
|       expect(queuing_entries.all).to contain_exactly( | ||||
|         an_object_having_attributes( | ||||
|           build_id: 50, | ||||
|           project_id: 5, | ||||
|           namespace_id: 10, | ||||
|           protected: false, | ||||
|           instance_runners_enabled: true, | ||||
|           minutes_exceeded: false, | ||||
|           tag_ids: [], | ||||
|           namespace_traversal_ids: [10]) | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'Ci::Build' do | ||||
|     describe '.each_batch' do | ||||
|       let(:model) { described_class::Ci::Build } | ||||
| 
 | ||||
|       before do | ||||
|         builds.create!(id: 1, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') | ||||
|         builds.create!(id: 2, status: :pending, name: 'test2', project_id: 5, type: 'Ci::Build') | ||||
|         builds.create!(id: 3, status: :pending, name: 'test3', project_id: 5, type: 'Ci::Build') | ||||
|         builds.create!(id: 4, status: :pending, name: 'test4', project_id: 5, type: 'Ci::Build') | ||||
|         builds.create!(id: 5, status: :pending, name: 'test5', project_id: 5, type: 'Ci::Build') | ||||
|       end | ||||
| 
 | ||||
|       it 'yields an ActiveRecord::Relation when a block is given' do | ||||
|         model.each_batch do |relation| | ||||
|           expect(relation).to be_a_kind_of(ActiveRecord::Relation) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'yields a batch index as the second argument' do | ||||
|         model.each_batch do |_, index| | ||||
|           expect(index).to eq(1) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'accepts a custom batch size' do | ||||
|         amount = 0 | ||||
| 
 | ||||
|         model.each_batch(of: 1) { amount += 1 } | ||||
| 
 | ||||
|         expect(amount).to eq(5) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not include ORDER BYs in the yielded relations' do | ||||
|         model.each_batch do |relation| | ||||
|           expect(relation.to_sql).not_to include('ORDER BY') | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'orders ascending' do | ||||
|         ids = [] | ||||
| 
 | ||||
|         model.each_batch(of: 1) { |rel| ids.concat(rel.ids) } | ||||
| 
 | ||||
|         expect(ids).to eq(ids.sort) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,48 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| require_migration! | ||||
| 
 | ||||
| RSpec.describe StartBackfillCiQueuingTables do | ||||
|   let(:namespaces) { table(:namespaces) } | ||||
|   let(:projects)   { table(:projects) } | ||||
|   let(:builds)     { table(:ci_builds) } | ||||
| 
 | ||||
|   let!(:namespace) do | ||||
|     namespaces.create!(name: 'namespace1', path: 'namespace1') | ||||
|   end | ||||
| 
 | ||||
|   let!(:project) do | ||||
|     projects.create!(namespace_id: namespace.id, name: 'test1', path: 'test1') | ||||
|   end | ||||
| 
 | ||||
|   let!(:pending_build_1) do | ||||
|     builds.create!(status: :pending, name: 'test1', type: 'Ci::Build', project_id: project.id) | ||||
|   end | ||||
| 
 | ||||
|   let!(:running_build) do | ||||
|     builds.create!(status: :running, name: 'test2', type: 'Ci::Build', project_id: project.id) | ||||
|   end | ||||
| 
 | ||||
|   let!(:pending_build_2) do | ||||
|     builds.create!(status: :pending, name: 'test3', type: 'Ci::Build', project_id: project.id) | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     stub_const("#{described_class.name}::BATCH_SIZE", 1) | ||||
|   end | ||||
| 
 | ||||
|   it 'schedules jobs for builds that are pending' do | ||||
|     Sidekiq::Testing.fake! do | ||||
|       freeze_time do | ||||
|         migrate! | ||||
| 
 | ||||
|         expect(described_class::MIGRATION).to be_scheduled_delayed_migration( | ||||
|           2.minutes, pending_build_1.id, pending_build_1.id) | ||||
|         expect(described_class::MIGRATION).to be_scheduled_delayed_migration( | ||||
|           4.minutes, pending_build_2.id, pending_build_2.id) | ||||
|         expect(BackgroundMigrationWorker.jobs.size).to eq(2) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -66,7 +66,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| | |||
| 
 | ||||
|   it 'shows the help state when icon is clicked' do | ||||
|     page.within '.time-tracking-component-wrap' do | ||||
|       find('.help-button').click | ||||
|       find('[data-testid="helpButton"]').click | ||||
|       expect(page).to have_content 'Track time with quick actions' | ||||
|       expect(page).to have_content 'Learn more' | ||||
|     end | ||||
|  | @ -92,8 +92,8 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| | |||
| 
 | ||||
|   it 'hides the help state when close icon is clicked' do | ||||
|     page.within '.time-tracking-component-wrap' do | ||||
|       find('.help-button').click | ||||
|       find('.close-help-button').click | ||||
|       find('[data-testid="helpButton"]').click | ||||
|       find('[data-testid="closeHelpButton"]').click | ||||
| 
 | ||||
|       expect(page).not_to have_content 'Track time with quick actions' | ||||
|       expect(page).not_to have_content 'Learn more' | ||||
|  | @ -102,7 +102,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| | |||
| 
 | ||||
|   it 'displays the correct help url' do | ||||
|     page.within '.time-tracking-component-wrap' do | ||||
|       find('.help-button').click | ||||
|       find('[data-testid="helpButton"]').click | ||||
| 
 | ||||
|       expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md') | ||||
|     end | ||||
|  |  | |||
|  | @ -0,0 +1,85 @@ | |||
| # frozen_string_literal: true | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'shared/_gl_toggle.html.haml' do | ||||
|   context 'defaults' do | ||||
|     before do | ||||
|       render partial: 'shared/gl_toggle', locals: { | ||||
|         classes: '.js-gl-toggle' | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'does not set a name' do | ||||
|       expect(rendered).not_to have_selector('[data-name]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets default is-checked attributes' do | ||||
|       expect(rendered).to have_selector('[data-is-checked="false"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets default disabled attributes' do | ||||
|       expect(rendered).to have_selector('[data-disabled="false"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets default is-loading attributes' do | ||||
|       expect(rendered).to have_selector('[data-is-loading="false"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'does not set a label' do | ||||
|       expect(rendered).not_to have_selector('[data-label]') | ||||
|     end | ||||
| 
 | ||||
|     it 'does not set a label position' do | ||||
|       expect(rendered).not_to have_selector('[data-label-position]') | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with custom options' do | ||||
|     before do | ||||
|       render partial: 'shared/gl_toggle', locals: { | ||||
|         classes: 'js-custom-gl-toggle', | ||||
|         name: 'toggle-name', | ||||
|         is_checked: true, | ||||
|         disabled: true, | ||||
|         is_loading: true, | ||||
|         label: 'Custom label', | ||||
|         label_position: 'top', | ||||
|         data: { | ||||
|           foo: 'bar' | ||||
|         } | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the custom class' do | ||||
|       expect(rendered).to have_selector('.js-custom-gl-toggle') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the custom name' do | ||||
|       expect(rendered).to have_selector('[data-name="toggle-name"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the custom is-checked attributes' do | ||||
|       expect(rendered).to have_selector('[data-is-checked="true"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the custom disabled attributes' do | ||||
|       expect(rendered).to have_selector('[data-disabled="true"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the custom is-loading attributes' do | ||||
|       expect(rendered).to have_selector('[data-is-loading="true"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the custom label' do | ||||
|       expect(rendered).to have_selector('[data-label="Custom label"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets the cutom label position' do | ||||
|       expect(rendered).to have_selector('[data-label-position="top"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'sets cutom data attributes' do | ||||
|       expect(rendered).to have_selector('[data-foo="bar"]') | ||||
|     end | ||||
|   end | ||||
| end | ||||
		Loading…
	
		Reference in New Issue