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