Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-15 12:14:49 +00:00
parent e7e44c0e4c
commit 524e972622
98 changed files with 1458 additions and 391 deletions

View File

@ -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: cloud-native-image:
extends: .cng:rules extends: .cng:rules
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
dependencies: []
stage: post-test stage: post-test
needs: ["cloud-native-image-env"]
inherit:
variables: false
variables: variables:
GIT_DEPTH: "1" TOP_UPSTREAM_SOURCE_PROJECT: "${TOP_UPSTREAM_SOURCE_PROJECT}"
script: TOP_UPSTREAM_SOURCE_REF: "${TOP_UPSTREAM_SOURCE_REF}"
- install_gitlab_gem TOP_UPSTREAM_SOURCE_JOB: "${TOP_UPSTREAM_SOURCE_JOB}"
- ./scripts/trigger-build cng 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

View File

@ -28,7 +28,7 @@
review-docs-deploy: review-docs-deploy:
extends: .review-docs extends: .review-docs
script: script:
- ./scripts/trigger-build docs deploy - ./scripts/trigger-build.rb docs deploy
# Cleanup remote environment of gitlab-docs # Cleanup remote environment of gitlab-docs
review-docs-cleanup: review-docs-cleanup:
@ -37,7 +37,7 @@ review-docs-cleanup:
name: review-docs/mr-${CI_MERGE_REQUEST_IID} name: review-docs/mr-${CI_MERGE_REQUEST_IID}
action: stop action: stop
script: script:
- ./scripts/trigger-build docs cleanup - ./scripts/trigger-build.rb docs cleanup
docs-lint markdown: docs-lint markdown:
extends: extends:

View File

@ -73,7 +73,7 @@ update-qa-cache:
- echo $exit_code - echo $exit_code
- | - |
if [ $exit_code -eq 0 ]; then if [ $exit_code -eq 0 ]; then
./scripts/trigger-build omnibus ./scripts/trigger-build.rb omnibus
elif [ $exit_code -eq 1 ]; then elif [ $exit_code -eq 1 ]; then
exit 1 exit 1
else else
@ -108,7 +108,7 @@ update-qa-cache:
if [[ $feature_flags ]]; then if [[ $feature_flags ]]; then
export GITLAB_QA_OPTIONS="--set-feature-flags $feature_flags" export GITLAB_QA_OPTIONS="--set-feature-flags $feature_flags"
echo $GITLAB_QA_OPTIONS echo $GITLAB_QA_OPTIONS
./scripts/trigger-build omnibus ./scripts/trigger-build.rb omnibus
else else
echo "No changed feature flag found to test. The tests are skipped if the flag was removed." echo "No changed feature flag found to test. The tests are skipped if the flag was removed."
fi fi

View File

@ -438,7 +438,7 @@ db:gitlabcom-database-testing:
script: script:
- source scripts/utils.sh - source scripts/utils.sh
- install_gitlab_gem - install_gitlab_gem
- ./scripts/trigger-build gitlab-com-database-testing - ./scripts/trigger-build.rb gitlab-com-database-testing
gitlab:setup: gitlab:setup:
extends: .db-job-base extends: .db-job-base

View File

@ -16,20 +16,58 @@ include:
- source ./scripts/review_apps/review-apps.sh - source ./scripts/review_apps/review-apps.sh
- install_api_client_dependencies_with_apk - install_api_client_dependencies_with_apk
review-build-cng: review-build-cng-env:
extends: extends:
- .default-retry - .default-retry
- .review:rules:review-build-cng - .review:rules:review-build-cng
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13 image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13
stage: prepare stage: prepare
variables: needs: []
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"
before_script: before_script:
- source ./scripts/utils.sh - source ./scripts/utils.sh
- install_gitlab_gem - install_gitlab_gem
script: 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: .review-workflow-base:
extends: extends:

View File

@ -141,7 +141,7 @@
- ".gitlab/ci/review-apps/**/*" - ".gitlab/ci/review-apps/**/*"
- "scripts/review_apps/base-config.yaml" - "scripts/review_apps/base-config.yaml"
- "scripts/review_apps/review-apps.sh" - "scripts/review_apps/review-apps.sh"
- "scripts/trigger-build" - "scripts/trigger-build.rb"
- "{,ee/,jh/}{bin,config}/**/*.rb" - "{,ee/,jh/}{bin,config}/**/*.rb"
.ci-qa-patterns: &ci-qa-patterns .ci-qa-patterns: &ci-qa-patterns

View File

@ -24,7 +24,6 @@ Database/MultipleDatabases:
- lib/gitlab/import_export/group/relation_tree_restorer.rb - lib/gitlab/import_export/group/relation_tree_restorer.rb
- lib/gitlab/legacy_github_import/importer.rb - lib/gitlab/legacy_github_import/importer.rb
- lib/gitlab/seeder.rb - lib/gitlab/seeder.rb
- lib/system_check/orphans/repository_check.rb
- spec/db/schema_spec.rb - spec/db/schema_spec.rb
- spec/initializers/database_config_spec.rb - spec/initializers/database_config_spec.rb
- spec/lib/backup/manager_spec.rb - spec/lib/backup/manager_spec.rb

View File

@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 14.7.2 (2022-02-08)
### Added (1 change) ### Added (1 change)

View File

@ -1 +1 @@
d3ab199f7923a9d75516b8d1f1ea2f84b03190b1 a67a6fdd96ba690d57c919f9a042dceebab2832e

View File

@ -44,6 +44,9 @@ export const typePolicies = {
PipelinePermissions: { PipelinePermissions: {
merge: true, merge: true,
}, },
DesignCollection: {
merge: true,
},
}; };
export const stripWhitespaceFromQuery = (url, path) => { export const stripWhitespaceFromQuery = (url, path) => {

View File

@ -2,31 +2,14 @@
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; 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 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 RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue'; import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.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 { export default {
components: { components: {
GlTable, GlTable,
@ -54,10 +37,7 @@ export default {
}, },
methods: { methods: {
formatJobCount(jobCount) { formatJobCount(jobCount) {
if (jobCount > RUNNER_JOB_COUNT_LIMIT) { return formatJobCount(jobCount);
return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
}
return formatNumber(jobCount);
}, },
runnerTrAttr(runner) { runnerTrAttr(runner) {
if (runner) { if (runner) {

View File

@ -9,6 +9,7 @@ import {
I18N_FETCH_ERROR, I18N_FETCH_ERROR,
RUNNER_DETAILS_PROJECTS_PAGE_SIZE, RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
} from '../constants'; } from '../constants';
import { getPaginationVariables } from '../utils';
import { captureException } from '../sentry_utils'; import { captureException } from '../sentry_utils';
import RunnerAssignedItem from './runner_assigned_item.vue'; import RunnerAssignedItem from './runner_assigned_item.vue';
import RunnerPagination from './runner_pagination.vue'; import RunnerPagination from './runner_pagination.vue';
@ -62,19 +63,9 @@ export default {
computed: { computed: {
variables() { variables() {
const { id } = this.runner; const { id } = this.runner;
const { before, after } = this.pagination;
if (before) {
return {
id,
before,
last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
};
}
return { return {
id, id,
after, ...getPaginationVariables(this.pagination, RUNNER_DETAILS_PROJECTS_PAGE_SIZE),
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
}; };
}, },
loading() { loading() {

View File

@ -18,6 +18,7 @@ import {
RUNNER_PAGE_SIZE, RUNNER_PAGE_SIZE,
STATUS_NEVER_CONTACTED, STATUS_NEVER_CONTACTED,
} from './constants'; } from './constants';
import { getPaginationVariables } from './utils';
/** /**
* The filters and sorting of the runners are built around * The filters and sorting of the runners are built around
@ -184,30 +185,27 @@ export const fromSearchToVariables = ({
sort = null, sort = null,
pagination = {}, pagination = {},
} = {}) => { } = {}) => {
const variables = {}; const filterVariables = {};
const queryObj = filterToQueryObject(processFilters(filters), { const queryObj = filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH, filteredSearchTermKey: PARAM_KEY_SEARCH,
}); });
[variables.status] = queryObj[PARAM_KEY_STATUS] || []; [filterVariables.status] = queryObj[PARAM_KEY_STATUS] || [];
variables.search = queryObj[PARAM_KEY_SEARCH]; filterVariables.search = queryObj[PARAM_KEY_SEARCH];
variables.tagList = queryObj[PARAM_KEY_TAG]; filterVariables.tagList = queryObj[PARAM_KEY_TAG];
if (runnerType) { if (runnerType) {
variables.type = runnerType; filterVariables.type = runnerType;
} }
if (sort) { if (sort) {
variables.sort = sort; filterVariables.sort = sort;
} }
if (pagination.before) { const paginationVariables = getPaginationVariables(pagination, RUNNER_PAGE_SIZE);
variables.before = pagination.before;
variables.last = RUNNER_PAGE_SIZE;
} else {
variables.after = pagination.after;
variables.first = RUNNER_PAGE_SIZE;
}
return variables; return {
...filterVariables,
...paginationVariables,
};
}; };

View File

@ -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 };
};

View File

@ -10,6 +10,7 @@ import {
GlIcon, GlIcon,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants'; import { IssuableType } from '~/issues/constants';
@ -221,6 +222,12 @@ export default {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
return this.issuableAttribute === IssuableType.Epic; return this.issuableAttribute === IssuableType.Epic;
}, },
formatIssuableAttribute() {
return {
kebab: kebabCase(this.issuableAttribute),
snake: snakeCase(this.issuableAttribute),
};
},
}, },
methods: { methods: {
updateAttribute(attributeId) { updateAttribute(attributeId) {
@ -300,26 +307,28 @@ export default {
<sidebar-editable-item <sidebar-editable-item
ref="editable" ref="editable"
:title="attributeTypeTitle" :title="attributeTypeTitle"
:data-testid="`${issuableAttribute}-edit`" :data-testid="`${formatIssuableAttribute.kebab}-edit`"
:tracking="tracking" :tracking="tracking"
:loading="updating || loading" :loading="updating || loading"
@open="handleOpen" @open="handleOpen"
@close="handleClose" @close="handleClose"
> >
<template #collapsed> <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 :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
<span class="collapse-truncated-title">
{{ attributeTitle }}
</span>
</div>
</slot>
<div <div
v-if="isClassicSidebar" :data-testid="`select-${formatIssuableAttribute.kebab}`"
v-gl-tooltip.left.viewport
:title="attributeTypeTitle"
class="sidebar-collapsed-icon"
>
<gl-icon :size="16" :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
<span class="collapse-truncated-title">
{{ attributeTitle }}
</span>
</div>
<div
:data-testid="`select-${issuableAttribute}`"
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'" :class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
> >
<span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span> <span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span>
@ -337,7 +346,7 @@ export default {
v-gl-tooltip="tooltipText" v-gl-tooltip="tooltipText"
class="gl-text-gray-900! gl-font-weight-bold" class="gl-text-gray-900! gl-font-weight-bold"
:href="attributeUrl" :href="attributeUrl"
:data-qa-selector="`${issuableAttribute}_link`" :data-qa-selector="`${formatIssuableAttribute.snake}_link`"
> >
{{ attributeTitle }} {{ attributeTitle }}
<span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span> <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-search-box-by-type ref="search" v-model="searchTerm" />
<gl-dropdown-item <gl-dropdown-item
:data-testid="`no-${issuableAttribute}-item`" :data-testid="`no-${formatIssuableAttribute.kebab}-item`"
:is-check-item="true" :is-check-item="true"
:is-checked="isAttributeChecked($options.noAttributeId)" :is-checked="isAttributeChecked($options.noAttributeId)"
@click="updateAttribute($options.noAttributeId)" @click="updateAttribute($options.noAttributeId)"
@ -389,7 +398,7 @@ export default {
:key="attrItem.id" :key="attrItem.id"
:is-check-item="true" :is-check-item="true"
:is-checked="isAttributeChecked(attrItem.id)" :is-checked="isAttributeChecked(attrItem.id)"
:data-testid="`${issuableAttribute}-items`" :data-testid="`${formatIssuableAttribute.kebab}-items`"
@click="updateAttribute(attrItem.id)" @click="updateAttribute(attrItem.id)"
> >
{{ attrItem.title }} {{ attrItem.title }}

View File

@ -38,7 +38,10 @@ export default {
</script> </script>
<template> <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"> <div class="time-tracking-info">
<h4>{{ __('Track time with quick actions') }}</h4> <h4>{{ __('Track time with quick actions') }}</h4>
<p>{{ __('Quick actions can be used in description and comment boxes.') }}</p> <p>{{ __('Quick actions can be used in description and comment boxes.') }}</p>

View File

@ -1,5 +1,5 @@
<script> <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 { IssuableType } from '~/issues/constants';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { timeTrackingQueries } from '~/sidebar/constants'; import { timeTrackingQueries } from '~/sidebar/constants';
@ -21,6 +21,7 @@ export default {
GlIcon, GlIcon,
GlLink, GlLink,
GlModal, GlModal,
GlButton,
GlLoadingIcon, GlLoadingIcon,
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane, TimeTrackingSpentOnlyPane,
@ -187,7 +188,11 @@ export default {
</script> </script>
<template> <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 <time-tracking-collapsed-state
v-if="showCollapsed" v-if="showCollapsed"
:show-comparison-state="showComparisonState" :show-comparison-state="showComparisonState"
@ -198,25 +203,21 @@ export default {
:time-spent-human-readable="humanTotalTimeSpent" :time-spent-human-readable="humanTotalTimeSpent"
:time-estimate-human-readable="humanTimeEstimate" :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') }} {{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" inline /> <gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" inline />
<div <gl-button
v-if="!showHelpState" :data-testid="showHelpState ? 'closeHelpButton' : 'helpButton'"
data-testid="helpButton" category="tertiary"
class="help-button float-right" size="small"
@click="toggleHelpState(true)" variant="link"
class="gl-ml-auto"
@click="toggleHelpState(!showHelpState)"
> >
<gl-icon name="question-o" /> <gl-icon :name="showHelpState ? 'close' : 'question-o'" class="gl-text-gray-900!" />
</div> </gl-button>
<div
v-else
data-testid="closeHelpButton"
class="close-help-button float-right"
@click="toggleHelpState(false)"
>
<gl-icon name="close" />
</div>
</div> </div>
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed"> <div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane"> <div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">

View File

@ -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);
},
},
});
},
});
};

View File

@ -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 { .time-tracker {
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
> .stopwatch-svg { > .stopwatch-svg {
@ -759,11 +779,6 @@
} }
} }
.help-button,
.close-help-button {
cursor: pointer;
}
.compare-meter { .compare-meter {
&.over_estimate { &.over_estimate {
.time-remaining, .time-remaining,
@ -776,31 +791,6 @@
.compare-display-container { .compare-display-container {
font-size: 13px; 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 { .issuable-todo-btn {

View File

@ -4,10 +4,10 @@ module Mutations
module AlertManagement module AlertManagement
module HttpIntegration module HttpIntegration
class Create < HttpIntegrationBase class Create < HttpIntegrationBase
include FindsProject
graphql_name 'HttpIntegrationCreate' graphql_name 'HttpIntegrationCreate'
include FindsProject
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Project to create the integration in.' description: 'Project to create the integration in.'

View File

@ -4,10 +4,10 @@ module Mutations
module AlertManagement module AlertManagement
module PrometheusIntegration module PrometheusIntegration
class Create < PrometheusIntegrationBase class Create < PrometheusIntegrationBase
include FindsProject
graphql_name 'PrometheusIntegrationCreate' graphql_name 'PrometheusIntegrationCreate'
include FindsProject
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Project to create the integration in.' description: 'Project to create the integration in.'

View File

@ -3,10 +3,9 @@
module Mutations module Mutations
module Boards module Boards
class Create < ::Mutations::BaseMutation class Create < ::Mutations::BaseMutation
include Mutations::ResolvesResourceParent
graphql_name 'CreateBoard' graphql_name 'CreateBoard'
include Mutations::ResolvesResourceParent
include Mutations::Boards::CommonMutationArguments include Mutations::Boards::CommonMutationArguments
field :board, field :board,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module Branches module Branches
class Create < BaseMutation class Create < BaseMutation
include FindsProject
graphql_name 'CreateBranch' graphql_name 'CreateBranch'
include FindsProject
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Project full path the branch is associated with.' description: 'Project full path the branch is associated with.'

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module Ci module Ci
class CiCdSettingsUpdate < BaseMutation class CiCdSettingsUpdate < BaseMutation
include FindsProject
graphql_name 'CiCdSettingsUpdate' graphql_name 'CiCdSettingsUpdate'
include FindsProject
authorize :admin_project authorize :admin_project
argument :full_path, GraphQL::Types::ID, argument :full_path, GraphQL::Types::ID,

View File

@ -4,10 +4,10 @@ module Mutations
module Ci module Ci
module JobTokenScope module JobTokenScope
class AddProject < BaseMutation class AddProject < BaseMutation
include FindsProject
graphql_name 'CiJobTokenScopeAddProject' graphql_name 'CiJobTokenScopeAddProject'
include FindsProject
authorize :admin_project authorize :admin_project
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,

View File

@ -4,10 +4,10 @@ module Mutations
module Ci module Ci
module JobTokenScope module JobTokenScope
class RemoveProject < BaseMutation class RemoveProject < BaseMutation
include FindsProject
graphql_name 'CiJobTokenScopeRemoveProject' graphql_name 'CiJobTokenScopeRemoveProject'
include FindsProject
authorize :admin_project authorize :admin_project
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,

View File

@ -4,12 +4,12 @@ module Mutations
module Clusters module Clusters
module Agents module Agents
class Create < BaseMutation class Create < BaseMutation
graphql_name 'CreateClusterAgent'
include FindsProject include FindsProject
authorize :create_cluster authorize :create_cluster
graphql_name 'CreateClusterAgent'
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Full path of the associated project for this cluster agent.' description: 'Full path of the associated project for this cluster agent.'

View File

@ -3,6 +3,8 @@
module Mutations module Mutations
module Commits module Commits
class Create < BaseMutation class Create < BaseMutation
graphql_name 'CommitCreate'
include FindsProject include FindsProject
class UrlHelpers class UrlHelpers
@ -10,8 +12,6 @@ module Mutations
include Gitlab::Routing include Gitlab::Routing
end end
graphql_name 'CommitCreate'
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Project full path the branch is associated with.' description: 'Project full path the branch is associated with.'

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module ContainerExpirationPolicies module ContainerExpirationPolicies
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include FindsProject
graphql_name 'UpdateContainerExpirationPolicy' graphql_name 'UpdateContainerExpirationPolicy'
include FindsProject
authorize :destroy_container_image authorize :destroy_container_image
argument :project_path, argument :project_path,

View File

@ -3,12 +3,11 @@
module Mutations module Mutations
module ContainerRepositories module ContainerRepositories
class DestroyTags < ::Mutations::ContainerRepositories::DestroyBase class DestroyTags < ::Mutations::ContainerRepositories::DestroyBase
LIMIT = 20
TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
graphql_name 'DestroyContainerRepositoryTags' graphql_name 'DestroyContainerRepositoryTags'
LIMIT = 20
TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
authorize :destroy_container_image authorize :destroy_container_image
argument :id, argument :id,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module CustomEmoji module CustomEmoji
class Create < BaseMutation class Create < BaseMutation
include Mutations::ResolvesGroup
graphql_name 'CreateCustomEmoji' graphql_name 'CreateCustomEmoji'
include Mutations::ResolvesGroup
authorize :create_custom_emoji authorize :create_custom_emoji
field :custom_emoji, field :custom_emoji,

View File

@ -4,11 +4,11 @@ module Mutations
module CustomerRelations module CustomerRelations
module Contacts module Contacts
class Create < BaseMutation class Create < BaseMutation
graphql_name 'CustomerRelationsContactCreate'
include ResolvesIds include ResolvesIds
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
graphql_name 'CustomerRelationsContactCreate'
field :contact, field :contact,
Types::CustomerRelations::ContactType, Types::CustomerRelations::ContactType,
null: true, null: true,

View File

@ -4,10 +4,10 @@ module Mutations
module CustomerRelations module CustomerRelations
module Contacts module Contacts
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include ResolvesIds
graphql_name 'CustomerRelationsContactUpdate' graphql_name 'CustomerRelationsContactUpdate'
include ResolvesIds
authorize :admin_crm_contact authorize :admin_crm_contact
field :contact, field :contact,

View File

@ -4,11 +4,11 @@ module Mutations
module CustomerRelations module CustomerRelations
module Organizations module Organizations
class Create < BaseMutation class Create < BaseMutation
graphql_name 'CustomerRelationsOrganizationCreate'
include ResolvesIds include ResolvesIds
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
graphql_name 'CustomerRelationsOrganizationCreate'
field :organization, field :organization,
Types::CustomerRelations::OrganizationType, Types::CustomerRelations::OrganizationType,
null: true, null: true,

View File

@ -4,10 +4,10 @@ module Mutations
module CustomerRelations module CustomerRelations
module Organizations module Organizations
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include ResolvesIds
graphql_name 'CustomerRelationsOrganizationUpdate' graphql_name 'CustomerRelationsOrganizationUpdate'
include ResolvesIds
authorize :admin_crm_organization authorize :admin_crm_organization
field :organization, field :organization,

View File

@ -4,10 +4,10 @@ module Mutations
module DependencyProxy module DependencyProxy
module GroupSettings module GroupSettings
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include Mutations::ResolvesGroup
graphql_name 'UpdateDependencyProxySettings' graphql_name 'UpdateDependencyProxySettings'
include Mutations::ResolvesGroup
authorize :admin_dependency_proxy authorize :admin_dependency_proxy
argument :group_path, argument :group_path,

View File

@ -4,10 +4,10 @@ module Mutations
module DependencyProxy module DependencyProxy
module ImageTtlGroupPolicy module ImageTtlGroupPolicy
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include Mutations::ResolvesGroup
graphql_name 'UpdateDependencyProxyImageTtlGroupPolicy' graphql_name 'UpdateDependencyProxyImageTtlGroupPolicy'
include Mutations::ResolvesGroup
authorize :admin_dependency_proxy authorize :admin_dependency_proxy
argument :group_path, argument :group_path,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module DesignManagement module DesignManagement
class Delete < Base class Delete < Base
Errors = ::Gitlab::Graphql::Errors
graphql_name "DesignManagementDelete" graphql_name "DesignManagementDelete"
Errors = ::Gitlab::Graphql::Errors
argument :filenames, [GraphQL::Types::String], argument :filenames, [GraphQL::Types::String],
required: true, required: true,
description: "Filenames of the designs to delete.", description: "Filenames of the designs to delete.",

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module Groups module Groups
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include Mutations::ResolvesGroup
graphql_name 'GroupUpdate' graphql_name 'GroupUpdate'
include Mutations::ResolvesGroup
authorize :admin_group authorize :admin_group
field :group, Types::GroupType, field :group, Types::GroupType,

View File

@ -3,12 +3,12 @@
module Mutations module Mutations
module Issues module Issues
class Create < BaseMutation class Create < BaseMutation
graphql_name 'CreateIssue'
include Mutations::SpamProtection include Mutations::SpamProtection
include FindsProject include FindsProject
include CommonMutationArguments include CommonMutationArguments
graphql_name 'CreateIssue'
authorize :create_issue authorize :create_issue
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module Issues module Issues
class SetConfidential < Base class SetConfidential < Base
include Mutations::SpamProtection
graphql_name 'IssueSetConfidential' graphql_name 'IssueSetConfidential'
include Mutations::SpamProtection
argument :confidential, argument :confidential,
GraphQL::Types::Boolean, GraphQL::Types::Boolean,
required: true, required: true,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module JiraImport module JiraImport
class ImportUsers < BaseMutation class ImportUsers < BaseMutation
include FindsProject
graphql_name 'JiraImportUsers' graphql_name 'JiraImportUsers'
include FindsProject
authorize :admin_project authorize :admin_project
field :jira_users, field :jira_users,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module JiraImport module JiraImport
class Start < BaseMutation class Start < BaseMutation
include FindsProject
graphql_name 'JiraImportStart' graphql_name 'JiraImportStart'
include FindsProject
authorize :admin_project authorize :admin_project
field :jira_import, field :jira_import,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module Labels module Labels
class Create < BaseMutation class Create < BaseMutation
include Mutations::ResolvesResourceParent
graphql_name 'LabelCreate' graphql_name 'LabelCreate'
include Mutations::ResolvesResourceParent
field :label, field :label,
Types::LabelType, Types::LabelType,
null: true, null: true,

View File

@ -3,12 +3,6 @@
module Mutations module Mutations
module MergeRequests module MergeRequests
class Accept < Base 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' graphql_name 'MergeRequestAccept'
authorize :accept_merge_request authorize :accept_merge_request
description <<~DESC description <<~DESC
@ -17,6 +11,12 @@ module Mutations
immediately if possible, or using one of the automatic merge strategies. immediately if possible, or using one of the automatic merge strategies.
DESC 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, argument :strategy,
::Types::MergeStrategyEnum, ::Types::MergeStrategyEnum,
required: false, required: false,

View File

@ -3,10 +3,10 @@
module Mutations module Mutations
module MergeRequests module MergeRequests
class Create < BaseMutation class Create < BaseMutation
include FindsProject
graphql_name 'MergeRequestCreate' graphql_name 'MergeRequestCreate'
include FindsProject
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Project full path the merge request is associated with.' description: 'Project full path the merge request is associated with.'

View File

@ -4,10 +4,10 @@ module Mutations
module Namespace module Namespace
module PackageSettings module PackageSettings
class Update < Mutations::BaseMutation class Update < Mutations::BaseMutation
include Mutations::ResolvesNamespace
graphql_name 'UpdateNamespacePackageSettings' graphql_name 'UpdateNamespacePackageSettings'
include Mutations::ResolvesNamespace
authorize :create_package_settings authorize :create_package_settings
argument :namespace_path, argument :namespace_path,

View File

@ -3,14 +3,13 @@
module Mutations module Mutations
module ReleaseAssetLinks module ReleaseAssetLinks
class Create < BaseMutation class Create < BaseMutation
include FindsProject
graphql_name 'ReleaseAssetLinkCreate' graphql_name 'ReleaseAssetLinkCreate'
authorize :create_release include FindsProject
include Types::ReleaseAssetLinkSharedInputArguments include Types::ReleaseAssetLinkSharedInputArguments
authorize :create_release
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: true,
description: 'Full path of the project the asset link is associated with.' description: 'Full path of the project the asset link is associated with.'

View File

@ -3,14 +3,14 @@
module Mutations module Mutations
module Snippets module Snippets
class Create < BaseMutation class Create < BaseMutation
graphql_name 'CreateSnippet'
include ServiceCompatibility include ServiceCompatibility
include CanMutateSpammable include CanMutateSpammable
include Mutations::SpamProtection include Mutations::SpamProtection
authorize :create_snippet authorize :create_snippet
graphql_name 'CreateSnippet'
field :snippet, field :snippet,
Types::SnippetType, Types::SnippetType,
null: true, null: true,

View File

@ -3,12 +3,12 @@
module Mutations module Mutations
module Snippets module Snippets
class Update < Base class Update < Base
graphql_name 'UpdateSnippet'
include ServiceCompatibility include ServiceCompatibility
include CanMutateSpammable include CanMutateSpammable
include Mutations::SpamProtection include Mutations::SpamProtection
graphql_name 'UpdateSnippet'
argument :id, ::Types::GlobalIDType[::Snippet], argument :id, ::Types::GlobalIDType[::Snippet],
required: true, required: true,
description: 'Global ID of the snippet to update.' description: 'Global ID of the snippet to update.'

View File

@ -3,11 +3,11 @@
module Mutations module Mutations
module WorkItems module WorkItems
class Create < BaseMutation class Create < BaseMutation
graphql_name 'WorkItemCreate'
include Mutations::SpamProtection include Mutations::SpamProtection
include FindsProject include FindsProject
graphql_name 'WorkItemCreate'
authorize :create_work_item authorize :create_work_item
argument :description, GraphQL::Types::String, argument :description, GraphQL::Types::String,

View File

@ -3,11 +3,10 @@
module Mutations module Mutations
module WorkItems module WorkItems
class Delete < BaseMutation class Delete < BaseMutation
graphql_name 'WorkItemDelete'
description "Deletes a work item." \ 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." " 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 authorize :delete_work_item
argument :id, ::Types::GlobalIDType[::WorkItem], argument :id, ::Types::GlobalIDType[::WorkItem],

View File

@ -3,12 +3,11 @@
module Mutations module Mutations
module WorkItems module WorkItems
class Update < BaseMutation class Update < BaseMutation
include Mutations::SpamProtection graphql_name 'WorkItemUpdate'
description "Updates a work item by Global ID." \ 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." " 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 authorize :update_work_item

View File

@ -5,10 +5,11 @@ module Types
module Analytics module Analytics
module UsageTrends module UsageTrends
class MeasurementType < BaseObject class MeasurementType < BaseObject
include Gitlab::Graphql::Authorize::AuthorizeResource
graphql_name 'UsageTrendsMeasurement' graphql_name 'UsageTrendsMeasurement'
description 'Represents a recorded measurement (object count) for the Admins' description 'Represents a recorded measurement (object count) for the Admins'
include Gitlab::Graphql::Authorize::AuthorizeResource
authorize :read_usage_trends_measurement authorize :read_usage_trends_measurement
field :recorded_at, Types::TimeType, null: true, field :recorded_at, Types::TimeType, null: true,

View File

@ -3,11 +3,11 @@
module Types module Types
module AlertManagement module AlertManagement
class PrometheusIntegrationType < ::Types::BaseObject class PrometheusIntegrationType < ::Types::BaseObject
include ::Gitlab::Routing
graphql_name 'AlertManagementPrometheusIntegration' graphql_name 'AlertManagementPrometheusIntegration'
description 'An endpoint and credentials used to accept Prometheus alerts for a project' description 'An endpoint and credentials used to accept Prometheus alerts for a project'
include ::Gitlab::Routing
implements(Types::AlertManagement::IntegrationType) implements(Types::AlertManagement::IntegrationType)
authorize :admin_project authorize :admin_project

View File

@ -3,11 +3,11 @@
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
class BoardListType < BaseObject class BoardListType < BaseObject
include Gitlab::Utils::StrongMemoize
graphql_name 'BoardList' graphql_name 'BoardList'
description 'Represents a list for an issue board' description 'Represents a list for an issue board'
include Gitlab::Utils::StrongMemoize
alias_method :list, :object alias_method :list, :object
field :id, GraphQL::Types::ID, field :id, GraphQL::Types::ID,

View File

@ -3,12 +3,13 @@
module Types module Types
module Ci module Ci
class RunnerType < BaseObject class RunnerType < BaseObject
graphql_name 'CiRunner'
edge_type_class(RunnerWebUrlEdge) edge_type_class(RunnerWebUrlEdge)
connection_type_class(Types::CountableConnectionType) connection_type_class(Types::CountableConnectionType)
graphql_name 'CiRunner'
authorize :read_runner authorize :read_runner
present_using ::Ci::RunnerPresenter present_using ::Ci::RunnerPresenter
expose_permissions Types::PermissionTypes::Ci::Runner expose_permissions Types::PermissionTypes::Ci::Runner
JOB_COUNT_LIMIT = 1000 JOB_COUNT_LIMIT = 1000

View File

@ -2,14 +2,14 @@
module Types module Types
class GroupInvitationType < BaseObject class GroupInvitationType < BaseObject
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
expose_permissions Types::PermissionTypes::Group expose_permissions Types::PermissionTypes::Group
authorize :admin_group authorize :admin_group
implements InvitationInterface implements InvitationInterface
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
field :group, Types::GroupType, null: true, field :group, Types::GroupType, null: true,
description: 'Group that a User is invited to.' description: 'Group that a User is invited to.'

View File

@ -2,14 +2,14 @@
module Types module Types
class GroupMemberType < BaseObject class GroupMemberType < BaseObject
graphql_name 'GroupMember'
description 'Represents a Group Membership'
expose_permissions Types::PermissionTypes::Group expose_permissions Types::PermissionTypes::Group
authorize :read_group authorize :read_group
implements MemberInterface implements MemberInterface
graphql_name 'GroupMember'
description 'Represents a Group Membership'
field :group, Types::GroupType, null: true, field :group, Types::GroupType, null: true,
description: 'Group that a User is a member of.' description: 'Group that a User is a member of.'

View File

@ -3,11 +3,12 @@
module Types module Types
module MergeRequests module MergeRequests
class AssigneeType < ::Types::UserType class AssigneeType < ::Types::UserType
graphql_name 'MergeRequestAssignee'
description 'A user assigned to a merge request.'
include FindClosest include FindClosest
include ::Types::MergeRequests::InteractsWithMergeRequest include ::Types::MergeRequests::InteractsWithMergeRequest
graphql_name 'MergeRequestAssignee'
description 'A user assigned to a merge request.'
authorize :read_user authorize :read_user
end end
end end

View File

@ -3,11 +3,12 @@
module Types module Types
module MergeRequests module MergeRequests
class ReviewerType < ::Types::UserType class ReviewerType < ::Types::UserType
graphql_name 'MergeRequestReviewer'
description 'A user assigned to a merge request as a reviewer.'
include FindClosest include FindClosest
include ::Types::MergeRequests::InteractsWithMergeRequest include ::Types::MergeRequests::InteractsWithMergeRequest
graphql_name 'MergeRequestReviewer'
description 'A user assigned to a merge request as a reviewer.'
authorize :read_user authorize :read_user
end end
end end

View File

@ -4,8 +4,8 @@ module Types
module Metrics module Metrics
module Dashboards module Dashboards
class AnnotationType < ::Types::BaseObject class AnnotationType < ::Types::BaseObject
authorize :read_metrics_dashboard_annotation
graphql_name 'MetricsDashboardAnnotation' graphql_name 'MetricsDashboardAnnotation'
authorize :read_metrics_dashboard_annotation
field :description, GraphQL::Types::String, null: true, field :description, GraphQL::Types::String, null: true,
description: 'Description of the annotation.' description: 'Description of the annotation.'

View File

@ -2,10 +2,10 @@
module Types module Types
class MutationType < BaseObject class MutationType < BaseObject
include Gitlab::Graphql::MountMutation
graphql_name 'Mutation' graphql_name 'Mutation'
include Gitlab::Graphql::MountMutation
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::CreateAlertIssue mount_mutation Mutations::AlertManagement::CreateAlertIssue
mount_mutation Mutations::AlertManagement::UpdateAlertStatus mount_mutation Mutations::AlertManagement::UpdateAlertStatus

View File

@ -3,10 +3,10 @@
module Types module Types
module Notes module Notes
class DiscussionType < BaseObject class DiscussionType < BaseObject
DiscussionID = ::Types::GlobalIDType[::Discussion]
graphql_name 'Discussion' graphql_name 'Discussion'
DiscussionID = ::Types::GlobalIDType[::Discussion]
authorize :read_note authorize :read_note
implements(Types::ResolvableInterface) implements(Types::ResolvableInterface)

View File

@ -3,10 +3,11 @@
module Types module Types
module Packages module Packages
class PackageDetailsType < PackageType class PackageDetailsType < PackageType
include ::PackagesHelper
graphql_name 'PackageDetailsType' graphql_name 'PackageDetailsType'
description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes' 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 authorize :read_package
field :versions, ::Types::Packages::PackageType.connection_type, null: true, field :versions, ::Types::Packages::PackageType.connection_type, null: true,

View File

@ -3,8 +3,8 @@
module Types module Types
module PermissionTypes module PermissionTypes
class Issue < BasePermissionType class Issue < BasePermissionType
description 'Check permissions for the current user on a issue'
graphql_name 'IssuePermissions' graphql_name 'IssuePermissions'
description 'Check permissions for the current user on a issue'
abilities :read_issue, :admin_issue, :update_issue, :reopen_issue, abilities :read_issue, :admin_issue, :update_issue, :reopen_issue,
:read_design, :create_design, :destroy_design, :read_design, :create_design, :destroy_design,

View File

@ -3,15 +3,16 @@
module Types module Types
module PermissionTypes module PermissionTypes
class MergeRequest < BasePermissionType 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 PERMISSION_FIELDS = %i[push_to_source_branch
remove_source_branch remove_source_branch
cherry_pick_on_current_merge_request cherry_pick_on_current_merge_request
revert_on_current_merge_request].freeze 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, abilities :read_merge_request, :admin_merge_request,
:update_merge_request, :create_note :update_merge_request, :create_note

View File

@ -3,10 +3,10 @@
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
class QueryComplexityType < ::Types::BaseObject class QueryComplexityType < ::Types::BaseObject
ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity }
graphql_name 'QueryComplexity' graphql_name 'QueryComplexity'
ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity }
alias_method :query, :object alias_method :query, :object
field :limit, GraphQL::Types::Int, field :limit, GraphQL::Types::Int,

View File

@ -4,10 +4,10 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization # This is presented through `Repository` that has its own authorization
class BlobType < BaseObject class BlobType < BaseObject
present_using BlobPresenter
graphql_name 'RepositoryBlob' graphql_name 'RepositoryBlob'
present_using BlobPresenter
field :id, GraphQL::Types::ID, null: false, field :id, GraphQL::Types::ID, null: false,
description: 'ID of the blob.' description: 'ID of the blob.'

View File

@ -3,10 +3,10 @@
module Types module Types
module Terraform module Terraform
class StateVersionType < BaseObject class StateVersionType < BaseObject
include ::API::Helpers::RelatedResourcesHelpers
graphql_name 'TerraformStateVersion' graphql_name 'TerraformStateVersion'
include ::API::Helpers::RelatedResourcesHelpers
authorize :read_terraform_state authorize :read_terraform_state
field :id, GraphQL::Types::ID, field :id, GraphQL::Types::ID,

View File

@ -4,12 +4,11 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization # This is presented through `Repository` that has its own authorization
class BlobType < BaseObject class BlobType < BaseObject
implements Types::Tree::EntryType
present_using BlobPresenter
graphql_name 'Blob' graphql_name 'Blob'
implements Types::Tree::EntryType
present_using BlobPresenter
field :web_url, GraphQL::Types::String, null: true, field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the blob.' description: 'Web URL of the blob.'
field :web_path, GraphQL::Types::String, null: true, field :web_path, GraphQL::Types::String, null: true,

View File

@ -4,10 +4,10 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization # This is presented through `Repository` that has its own authorization
class SubmoduleType < BaseObject class SubmoduleType < BaseObject
implements Types::Tree::EntryType
graphql_name 'Submodule' graphql_name 'Submodule'
implements Types::Tree::EntryType
field :web_url, type: GraphQL::Types::String, null: true, field :web_url, type: GraphQL::Types::String, null: true,
description: 'Web URL for the sub-module.' description: 'Web URL for the sub-module.'
field :tree_url, type: GraphQL::Types::String, null: true, field :tree_url, type: GraphQL::Types::String, null: true,

View File

@ -4,13 +4,12 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization # This is presented through `Repository` that has its own authorization
class TreeEntryType < BaseObject class TreeEntryType < BaseObject
implements Types::Tree::EntryType
present_using TreeEntryPresenter
graphql_name 'TreeEntry' graphql_name 'TreeEntry'
description 'Represents a directory' description 'Represents a directory'
implements Types::Tree::EntryType
present_using TreeEntryPresenter
field :web_url, GraphQL::Types::String, null: true, field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL for the tree entry (directory).' description: 'Web URL for the tree entry (directory).'
field :web_path, GraphQL::Types::String, null: true, field :web_path, GraphQL::Types::String, null: true,

View File

@ -12,6 +12,8 @@ module Ci
initial_branch = params[:branch_name] initial_branch = params[:branch_name]
latest_commit = project.repository.commit(initial_branch) || project.commit latest_commit = project.repository.commit(initial_branch) || project.commit
commit_sha = latest_commit ? latest_commit.sha : '' 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-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'), "ci-examples-help-page-path" => help_page_path('ci/examples/index'),
@ -29,7 +31,7 @@ module Ci
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'), "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') "yml-help-page-path" => help_page_path('ci/yaml/index')
} }
end end

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
dbe6760198b8fa068c30871a439298e56802867044a178baa6b8b009f8da13e6

View File

@ -528,7 +528,7 @@ You can use it either for personal or business websites, such as portfolios, doc
#### GitLab Runner #### 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: - Configuration:
- [Omnibus](https://docs.gitlab.com/runner/) - [Omnibus](https://docs.gitlab.com/runner/)
- [Charts](https://docs.gitlab.com/runner/install/kubernetes.html) - [Charts](https://docs.gitlab.com/runner/install/kubernetes.html)

View File

@ -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: 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. 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" 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`). 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 1. The preview URL is shown both at the job output and in the merge request

View File

@ -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 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 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): If you need to rebuild the Docker images immediately (must have maintainer level permissions):

View File

@ -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) - [`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) - [`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) - [`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 This configuration is also used in build pipelines, where
[error-level rules](#vale-result-types) are enforced. [error-level rules](#vale-result-types) are enforced.

View File

@ -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 The DAST tool always emits a JSON report file called `gl-dast-report.json` and
sample reports can be found in the 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 ## Optimizing DAST

View File

@ -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 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. 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: 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 ## 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: To view the planning hierarchy in a project:
@ -34,7 +34,7 @@ The work items outside your subscription plan show up below **Unavailable struct
![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_8.png) ![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_8.png)
## Hierarchies with epics ## Hierarchies with epics **(PREMIUM)**
With epics, you can achieve the following hierarchy: With epics, you can achieve the following hierarchy:
@ -68,14 +68,14 @@ Epic "1"*-- "0..*" Issue
![Diagram showing possible relationships of multi-level epics](img/hierarchy_with_multi_level_epics.png) ![Diagram showing possible relationships of multi-level epics](img/hierarchy_with_multi_level_epics.png)
## View ancestry of an epic
In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**.
![epics state dropdown](img/epic-view-ancestors-in-sidebar_v14_6.png)
## View ancestry of an issue ## View ancestry of an issue
In an issue, you can view the parented epic above the issue in the right sidebar under **Epic**. In an issue, you can view the parented epic above the issue in the right sidebar under **Epic**.
![epics state dropdown](img/issue-view-parent-epic-in-sidebar_v14_6.png) ![epics state dropdown](img/issue-view-parent-epic-in-sidebar_v14_6.png)
## View ancestry of an epic **(PREMIUM)**
In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**.
![epics state dropdown](img/epic-view-ancestors-in-sidebar_v14_6.png)

View File

@ -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

View File

@ -57,8 +57,8 @@ module SystemCheck
WHERE (p.repository_storage LIKE ?) WHERE (p.repository_storage LIKE ?)
" "
query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) # rubocop:disable GitlabSecurity/PublicSend query = ::Project.sanitize_sql_array([sql, storage_name])
ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) || [] ::Project.connection.select_all(query).rows.try(:flatten!) || []
end end
def fetch_disk_namespaces(storage_path) def fetch_disk_namespaces(storage_path)

View File

@ -14377,6 +14377,9 @@ msgstr ""
msgid "Escalation policies must have at least one rule" msgid "Escalation policies must have at least one rule"
msgstr "" msgstr ""
msgid "Escalation policy"
msgstr ""
msgid "Escalation policy:" msgid "Escalation policy:"
msgstr "" msgstr ""
@ -19112,6 +19115,12 @@ msgstr ""
msgid "IncidentManagement|Open" msgid "IncidentManagement|Open"
msgstr "" msgstr ""
msgid "IncidentManagement|Page your team with escalation policies"
msgstr ""
msgid "IncidentManagement|Paged"
msgstr ""
msgid "IncidentManagement|Published" msgid "IncidentManagement|Published"
msgstr "" msgstr ""
@ -19139,6 +19148,9 @@ msgstr ""
msgid "IncidentManagement|Unpublished" msgid "IncidentManagement|Unpublished"
msgstr "" msgstr ""
msgid "IncidentManagement|Use escalation policies to automatically page your team when incidents are created."
msgstr ""
msgid "IncidentSettings|Activate \"time to SLA\" countdown timer" msgid "IncidentSettings|Activate \"time to SLA\" countdown timer"
msgstr "" msgstr ""
@ -19184,7 +19196,10 @@ msgstr ""
msgid "Incidents" msgid "Incidents"
msgstr "" 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 "" msgstr ""
msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident" 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." msgid "Incidents|There was an issue loading metric images."
msgstr "" msgstr ""
msgid "Incidents|There was an issue uploading your image." msgid "Incidents|There was an issue updating your image."
msgstr "" 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 "" msgstr ""
msgid "Incident|Alert details" msgid "Incident|Alert details"
@ -19211,9 +19226,18 @@ msgstr ""
msgid "Incident|Are you sure you wish to delete this image?" msgid "Incident|Are you sure you wish to delete this image?"
msgstr "" msgstr ""
msgid "Incident|Delete image"
msgstr ""
msgid "Incident|Deleting %{filename}" msgid "Incident|Deleting %{filename}"
msgstr "" msgstr ""
msgid "Incident|Edit image text or link"
msgstr ""
msgid "Incident|Editing %{filename}"
msgstr ""
msgid "Incident|Metrics" msgid "Incident|Metrics"
msgstr "" msgstr ""
@ -21740,6 +21764,9 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "" msgstr ""
msgid "Link (optional)"
msgstr ""
msgid "Link Prometheus monitoring to GitLab." msgid "Link Prometheus monitoring to GitLab."
msgstr "" msgstr ""
@ -35999,6 +36026,9 @@ msgstr ""
msgid "Tests" msgid "Tests"
msgstr "" msgstr ""
msgid "Text (optional)"
msgstr ""
msgid "Text added to the body of all email messages. %{character_limit} character limit" msgid "Text added to the body of all email messages. %{character_limit} character limit"
msgstr "" msgstr ""

View File

@ -40,7 +40,7 @@ module QA
end end
base.view 'app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue' do 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 end
base.view 'app/assets/javascripts/sidebar/components/sidebar_editable_item.vue' do base.view 'app/assets/javascripts/sidebar/components/sidebar_editable_item.vue' do

View File

@ -21,6 +21,12 @@ module Trigger
variable_value variable_value
end end
def self.variables_for_env_file(variables)
variables.map do |key, value|
%Q(#{key}=#{value})
end.join("\n")
end
class Base class Base
# Can be overridden # Can be overridden
def self.access_token def self.access_token
@ -57,6 +63,21 @@ module Trigger
end end
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 private
# Override to trigger and work with pipeline on different GitLab instance # 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 ENV[version_file]&.strip || File.read(version_file).strip
end end
def variables
base_variables.merge(extra_variables).merge(version_file_variables)
end
def base_variables def base_variables
# Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA for omnibus checkouts due to pipeline for merged results, # 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. # 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'], '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'], 'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'],
'TRIGGER_SOURCE' => 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_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']
} }
end end
@ -163,17 +174,16 @@ module Trigger
end end
class CNG < Base class CNG < Base
def self.access_token def variables
# Default to "Multi-pipeline (from 'gitlab-org/gitlab' 'cloud-native-image' job)" at https://gitlab.com/gitlab-org/build/CNG/-/settings/access_tokens # Delete variables that aren't useful when using native triggers.
ENV['CNG_PROJECT_ACCESS_TOKEN'] || super super.tap do |hash|
hash.delete('TRIGGER_SOURCE')
hash.delete('TRIGGERED_USER')
end
end end
private private
def downstream_project_path
ENV.fetch('CNG_PROJECT_PATH', 'gitlab-org/build/CNG')
end
def ref def ref
return ENV['CI_COMMIT_REF_NAME'] if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/ return ENV['CI_COMMIT_REF_NAME'] if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/
@ -181,17 +191,17 @@ module Trigger
end end
def extra_variables 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. # 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'] 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_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, "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : source_sha,
"FORCE_RAILS_IMAGE_BUILDS" => 'true', "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 end
@ -445,28 +455,30 @@ module Trigger
Job = Class.new(Pipeline) Job = Class.new(Pipeline)
end end
case ARGV[0] if $0 == __FILE__
when 'omnibus' case ARGV[0]
Trigger::Omnibus.new.invoke!(post_comment: true, downstream_job_name: 'Trigger:qa-test').wait! when 'omnibus'
when 'cng' Trigger::Omnibus.new.invoke!(post_comment: true, downstream_job_name: 'Trigger:qa-test').wait!
Trigger::CNG.new.invoke!.wait! when 'cng'
when 'gitlab-com-database-testing' Trigger::CNG.new.invoke!.wait!
Trigger::DatabaseTesting.new.invoke! when 'gitlab-com-database-testing'
when 'docs' Trigger::DatabaseTesting.new.invoke!
docs_trigger = Trigger::Docs.new when 'docs'
docs_trigger = Trigger::Docs.new
case ARGV[1] case ARGV[1]
when 'deploy' when 'deploy'
docs_trigger.deploy! docs_trigger.deploy!
when 'cleanup' when 'cleanup'
docs_trigger.cleanup! docs_trigger.cleanup!
else
puts 'usage: trigger-build docs <deploy|cleanup>'
exit 1
end
else else
puts 'usage: trigger-build docs <deploy|cleanup>' puts "Please provide a valid option:
exit 1 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
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

View File

@ -61,6 +61,15 @@ FactoryBot.define do
factory :incident do factory :incident do
issue_type { :incident } issue_type { :incident }
association :work_item_type, :default, :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 end
end end

View File

@ -7,7 +7,6 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue'; 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 RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql'; import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
@ -30,7 +29,6 @@ describe('AdminRunnerShowApp', () => {
let mockRunnerQuery; let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
@ -80,8 +78,7 @@ describe('AdminRunnerShowApp', () => {
}); });
it('shows basic runner details', async () => { it('shows basic runner details', async () => {
const expected = `Details const expected = `Description Instance runner
Description Instance runner
Last contact Never contacted Last contact Never contacted
Version 1.0.0 Version 1.0.0
IP Address 127.0.0.1 IP Address 127.0.0.1
@ -89,7 +86,7 @@ describe('AdminRunnerShowApp', () => {
Maximum job timeout None Maximum job timeout None
Tags None`.replace(/\s+/g, ' '); Tags None`.replace(/\s+/g, ' ');
expect(findRunnerDetails().text()).toMatchInterpolatedText(expected); expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
}); });
describe('when runner cannot be updated', () => { describe('when runner cannot be updated', () => {

View File

@ -1,6 +1,6 @@
import { GlSprintf, GlIntersperse } from '@gitlab/ui'; import { GlSprintf, GlIntersperse } from '@gitlab/ui';
import { createWrapper, ErrorWrapper } from '@vue/test-utils'; 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 TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; 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 RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue'; import RunnerDetail from '~/runner/components/runner_detail.vue';
import RunnerGroups from '~/runner/components/runner_groups.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'; import { runnerData, runnerWithGroupData } from '../mock_data';
@ -37,16 +39,14 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups); const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
wrapper = mountFn(RunnerDetails, { wrapper = mountFn(RunnerDetails, {
propsData: { propsData: {
...props, ...props,
}, },
stubs: { stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
RunnerDetail, RunnerDetail,
...stubs,
}, },
}); });
}; };
@ -65,76 +65,85 @@ describe('RunnerDetails', () => {
expect(wrapper.text()).toBe(''); expect(wrapper.text()).toBe('');
}); });
describe.each` describe('Details tab', () => {
field | runner | expectedValue describe.each`
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'} field | runner | expectedValue
${'Description'} | ${{ description: null }} | ${'None'} ${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'} ${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'} ${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Version'} | ${{ version: '12.3' }} | ${'12.3'} ${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
${'Version'} | ${{ version: null }} | ${'None'} ${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'} ${'Version'} | ${{ version: null }} | ${'None'}
${'IP Address'} | ${{ ipAddress: null }} | ${'None'} ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'} ${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'} ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
`('"$field" field', ({ field, runner, expectedValue }) => { ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
beforeEach(() => { `('"$field" field', ({ field, runner, expectedValue }) => {
createComponent({ beforeEach(() => {
props: { createComponent({
runner: { props: {
...mockRunner, runner: {
...runner, ...mockRunner,
...runner,
},
}, },
}, stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
},
});
});
it(`displays expected value "${expectedValue}"`, () => {
expect(findDd(field).text()).toBe(expectedValue);
}); });
}); });
it(`displays expected value "${expectedValue}"`, () => { describe('"Tags" field', () => {
expect(findDd(field).text()).toBe(expectedValue); const stubs = { RunnerTags, RunnerTag };
});
});
describe('"Tags" field', () => { it('displays expected value "tag-1 tag-2"', () => {
it('displays expected value "tag-1 tag-2"', () => { createComponent({
createComponent({ props: {
props: { runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] }, },
}, stubs,
mountFn: mountExtended, });
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
}); });
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2'); it('displays "None" when runner has no tags', () => {
}); createComponent({
props: {
runner: { ...mockRunner, tagList: [] },
},
stubs,
});
it('displays "None" when runner has no tags', () => { expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
createComponent({
props: {
runner: { ...mockRunner, tagList: [] },
},
mountFn: mountExtended,
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
});
});
describe('Group runners', () => {
beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
}); });
}); });
it('Shows a group runner details', () => { describe('Group runners', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner); beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
});
});
it('Shows a group runner details', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
}); });
}); });
}); });

View File

@ -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);
});
});
});

View File

@ -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');
});
});
});
});

View File

@ -88,6 +88,17 @@ RSpec.describe Ci::PipelineEditorHelper do
end end
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 context 'with a non-default branch name' do
let(:user) { create(:user) } let(:user) { create(:user) }

View File

@ -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

View File

@ -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

View File

@ -66,7 +66,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'shows the help state when icon is clicked' do it 'shows the help state when icon is clicked' do
page.within '.time-tracking-component-wrap' 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 'Track time with quick actions'
expect(page).to have_content 'Learn more' expect(page).to have_content 'Learn more'
end 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 it 'hides the help state when close icon is clicked' do
page.within '.time-tracking-component-wrap' do page.within '.time-tracking-component-wrap' do
find('.help-button').click find('[data-testid="helpButton"]').click
find('.close-help-button').click find('[data-testid="closeHelpButton"]').click
expect(page).not_to have_content 'Track time with quick actions' expect(page).not_to have_content 'Track time with quick actions'
expect(page).not_to have_content 'Learn more' 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 it 'displays the correct help url' do
page.within '.time-tracking-component-wrap' 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') expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md')
end end

View File

@ -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