Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fe6c2b9ae0
commit
b3647b2a67
|
@ -35,6 +35,7 @@ If applicable, any groups/projects that are happy to have this feature turned on
|
|||
- [ ] Test on staging
|
||||
- [ ] Ensure that documentation has been updated
|
||||
- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`)
|
||||
- [ ] If it is possible to perform an incremental rollout, this should be preferred. Proposed increments are: `10%`, `50%`, `100%`. Proposed minimum time between increments is 15 minutes.
|
||||
- [ ] Coordinate a time to enable the flag with the SRE oncall and release managers
|
||||
- In `#production` mention `@sre-oncall` and `@release-managers`. Once an SRE on call and Release Manager on call confirm, you can proceed with the rollout
|
||||
- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com. **Note**: Once a feature rollout has started, it is not necessary to inform `@sre-oncall`/`@release-managers` at each stage of the gradual rollout.
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<script>
|
||||
import { GlToggle } from '@gitlab/ui';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
import { __ } from '~/locale';
|
||||
import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
toggleLabel: __('Keyboard shortcuts'),
|
||||
},
|
||||
components: {
|
||||
GlToggle,
|
||||
},
|
||||
|
@ -31,7 +35,7 @@ export default {
|
|||
<gl-toggle
|
||||
v-model="shortcutsEnabled"
|
||||
aria-describedby="shortcutsToggle"
|
||||
label="Keyboard shortcuts"
|
||||
:label="$options.i18n.toggleLabel"
|
||||
label-position="left"
|
||||
@change="onChange"
|
||||
/>
|
||||
|
|
|
@ -29,6 +29,9 @@ import EnvironmentsDropdown from './environments_dropdown.vue';
|
|||
import Strategy from './strategy.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
statusLabel: s__('FeatureFlags|Status'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
GlBadge,
|
||||
|
@ -396,12 +399,14 @@ export default {
|
|||
|
||||
<div class="table-section section-20 text-center" role="gridcell">
|
||||
<div class="table-mobile-header" role="rowheader">
|
||||
{{ s__('FeatureFlags|Status') }}
|
||||
{{ $options.i18n.statusLabel }}
|
||||
</div>
|
||||
<div class="table-mobile-content gl-display-flex gl-justify-content-center">
|
||||
<gl-toggle
|
||||
:value="scope.active"
|
||||
:disabled="!active || !canUpdateScope(scope)"
|
||||
:label="$options.i18n.statusLabel"
|
||||
label-position="hidden"
|
||||
@change="(status) => (scope.active = status)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -529,11 +534,13 @@ export default {
|
|||
|
||||
<div class="table-section section-20 text-center" role="gridcell">
|
||||
<div class="table-mobile-header" role="rowheader">
|
||||
{{ s__('FeatureFlags|Status') }}
|
||||
{{ $options.i18n.statusLabel }}
|
||||
</div>
|
||||
<div class="table-mobile-content gl-display-flex gl-justify-content-center">
|
||||
<gl-toggle
|
||||
:disabled="!active"
|
||||
:label="$options.i18n.statusLabel"
|
||||
label-position="hidden"
|
||||
:value="false"
|
||||
@change="createNewScope({ active: true })"
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
|
||||
import {
|
||||
MAVEN_TOGGLE_LABEL,
|
||||
MAVEN_TITLE,
|
||||
MAVEN_SETTINGS_SUBTITLE,
|
||||
MAVEN_DUPLICATES_ALLOWED_DISABLED,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
export default {
|
||||
name: 'MavenSettings',
|
||||
i18n: {
|
||||
MAVEN_TOGGLE_LABEL,
|
||||
MAVEN_TITLE,
|
||||
MAVEN_SETTINGS_SUBTITLE,
|
||||
MAVEN_SETTING_EXCEPTION_TITLE,
|
||||
|
@ -80,6 +82,8 @@ export default {
|
|||
<div class="gl-display-flex">
|
||||
<gl-toggle
|
||||
data-qa-selector="allow_duplicates_toggle"
|
||||
:label="$options.i18n.MAVEN_TOGGLE_LABEL"
|
||||
label-position="hidden"
|
||||
:value="mavenDuplicatesAllowed"
|
||||
@change="update($options.modelNames.MAVEN_DUPLICATES_ALLOWED, $event)"
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,7 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__(
|
|||
|
||||
export const MAVEN_TITLE = s__('PackageRegistry|Maven');
|
||||
export const MAVEN_SETTINGS_SUBTITLE = s__('PackageRegistry|Settings for Maven packages');
|
||||
export const MAVEN_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
|
||||
export const MAVEN_DUPLICATES_ALLOWED_DISABLED = s__(
|
||||
'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Packages with the same name and version are rejected.',
|
||||
);
|
||||
|
|
|
@ -12,6 +12,11 @@ export default {
|
|||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -82,6 +87,8 @@ export default {
|
|||
class="gl-mr-3"
|
||||
:value="featureEnabled"
|
||||
:disabled="disabledInput"
|
||||
:label="label"
|
||||
label-position="hidden"
|
||||
@change="toggleFeature"
|
||||
/>
|
||||
<div class="select-wrapper gl-flex-fill-1">
|
||||
|
|
|
@ -22,6 +22,21 @@ const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
|
|||
export default {
|
||||
i18n: {
|
||||
...CVE_ID_REQUEST_BUTTON_I18N,
|
||||
analyticsLabel: s__('ProjectSettings|Analytics'),
|
||||
containerRegistryLabel: s__('ProjectSettings|Container registry'),
|
||||
forksLabel: s__('ProjectSettings|Forks'),
|
||||
issuesLabel: s__('ProjectSettings|Issues'),
|
||||
lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
|
||||
mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
|
||||
operationsLabel: s__('ProjectSettings|Operations'),
|
||||
packagesLabel: s__('ProjectSettings|Packages'),
|
||||
pagesLabel: s__('ProjectSettings|Pages'),
|
||||
ciCdLabel: s__('CI/CD'),
|
||||
repositoryLabel: s__('ProjectSettings|Repository'),
|
||||
requirementsLabel: s__('ProjectSettings|Requirements'),
|
||||
securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
|
||||
snippetsLabel: s__('ProjectSettings|Snippets'),
|
||||
wikiLabel: s__('ProjectSettings|Wiki'),
|
||||
},
|
||||
|
||||
components: {
|
||||
|
@ -423,11 +438,12 @@ export default {
|
|||
>
|
||||
<project-setting-row
|
||||
ref="issues-settings"
|
||||
:label="s__('ProjectSettings|Issues')"
|
||||
:label="$options.i18n.issuesLabel"
|
||||
:help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="issuesAccessLevel"
|
||||
:label="$options.i18n.issuesLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][issues_access_level]"
|
||||
/>
|
||||
|
@ -440,6 +456,8 @@ export default {
|
|||
v-model="cveIdRequestEnabled"
|
||||
class="gl-my-2"
|
||||
:disabled="cveIdRequestIsDisabled"
|
||||
:label="$options.i18n.cve_request_toggle_label"
|
||||
label-position="hidden"
|
||||
name="project[project_setting_attributes][cve_id_request_enabled]"
|
||||
data-testid="cve_id_request_toggle"
|
||||
/>
|
||||
|
@ -447,11 +465,12 @@ export default {
|
|||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="repository-settings"
|
||||
:label="s__('ProjectSettings|Repository')"
|
||||
:label="$options.i18n.repositoryLabel"
|
||||
:help-text="repositoryHelpText"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="repositoryAccessLevel"
|
||||
:label="$options.i18n.repositoryLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][repository_access_level]"
|
||||
/>
|
||||
|
@ -459,11 +478,12 @@ export default {
|
|||
<div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
|
||||
<project-setting-row
|
||||
ref="merge-request-settings"
|
||||
:label="s__('ProjectSettings|Merge requests')"
|
||||
:label="$options.i18n.mergeRequestsLabel"
|
||||
:help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="mergeRequestsAccessLevel"
|
||||
:label="$options.i18n.mergeRequestsLabel"
|
||||
:options="repoFeatureAccessLevelOptions"
|
||||
:disabled-input="!repositoryEnabled"
|
||||
name="project[project_feature_attributes][merge_requests_access_level]"
|
||||
|
@ -471,11 +491,12 @@ export default {
|
|||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="fork-settings"
|
||||
:label="s__('ProjectSettings|Forks')"
|
||||
:label="$options.i18n.forksLabel"
|
||||
:help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="forkingAccessLevel"
|
||||
:label="$options.i18n.forksLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
:disabled-input="!repositoryEnabled"
|
||||
name="project[project_feature_attributes][forking_access_level]"
|
||||
|
@ -485,7 +506,7 @@ export default {
|
|||
v-if="registryAvailable"
|
||||
ref="container-registry-settings"
|
||||
:help-path="registryHelpPath"
|
||||
:label="s__('ProjectSettings|Container registry')"
|
||||
:label="$options.i18n.containerRegistryLabel"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Every project can have its own space to store its Docker images')
|
||||
"
|
||||
|
@ -501,6 +522,8 @@ export default {
|
|||
v-model="containerRegistryEnabled"
|
||||
class="gl-my-2"
|
||||
:disabled="!repositoryEnabled"
|
||||
:label="$options.i18n.containerRegistryLabel"
|
||||
label-position="hidden"
|
||||
name="project[container_registry_enabled]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
|
@ -508,7 +531,7 @@ export default {
|
|||
v-if="lfsAvailable"
|
||||
ref="git-lfs-settings"
|
||||
:help-path="lfsHelpPath"
|
||||
:label="s__('ProjectSettings|Git Large File Storage (LFS)')"
|
||||
:label="$options.i18n.lfsLabel"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Manages large files such as audio, video, and graphics files.')
|
||||
"
|
||||
|
@ -517,6 +540,8 @@ export default {
|
|||
v-model="lfsEnabled"
|
||||
class="gl-my-2"
|
||||
:disabled="!repositoryEnabled"
|
||||
:label="$options.i18n.lfsLabel"
|
||||
label-position="hidden"
|
||||
name="project[lfs_enabled]"
|
||||
/>
|
||||
<p v-if="!lfsEnabled && lfsObjectsExist">
|
||||
|
@ -541,7 +566,7 @@ export default {
|
|||
v-if="packagesAvailable"
|
||||
ref="package-settings"
|
||||
:help-path="packagesHelpPath"
|
||||
:label="s__('ProjectSettings|Packages')"
|
||||
:label="$options.i18n.packagesLabel"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Every project can have its own space to store its packages.')
|
||||
"
|
||||
|
@ -550,17 +575,20 @@ export default {
|
|||
v-model="packagesEnabled"
|
||||
class="gl-my-2"
|
||||
:disabled="!repositoryEnabled"
|
||||
:label="$options.i18n.packagesLabel"
|
||||
label-position="hidden"
|
||||
name="project[packages_enabled]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
</div>
|
||||
<project-setting-row
|
||||
ref="pipeline-settings"
|
||||
:label="__('CI/CD')"
|
||||
:label="$options.i18n.ciCdLabel"
|
||||
:help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="buildsAccessLevel"
|
||||
:label="$options.i18n.ciCdLabel"
|
||||
:options="repoFeatureAccessLevelOptions"
|
||||
:disabled-input="!repositoryEnabled"
|
||||
name="project[project_feature_attributes][builds_access_level]"
|
||||
|
@ -568,11 +596,12 @@ export default {
|
|||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="analytics-settings"
|
||||
:label="s__('ProjectSettings|Analytics')"
|
||||
:label="$options.i18n.analyticsLabel"
|
||||
:help-text="s__('ProjectSettings|View project analytics.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="analyticsAccessLevel"
|
||||
:label="$options.i18n.analyticsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][analytics_access_level]"
|
||||
/>
|
||||
|
@ -580,43 +609,47 @@ export default {
|
|||
<project-setting-row
|
||||
v-if="requirementsAvailable"
|
||||
ref="requirements-settings"
|
||||
:label="s__('ProjectSettings|Requirements')"
|
||||
:label="$options.i18n.requirementsLabel"
|
||||
:help-text="s__('ProjectSettings|Requirements management system.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="requirementsAccessLevel"
|
||||
:label="$options.i18n.requirementsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][requirements_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
<project-setting-row
|
||||
:label="s__('ProjectSettings|Security & Compliance')"
|
||||
:label="$options.i18n.securityAndComplianceLabel"
|
||||
:help-text="s__('ProjectSettings|Security & Compliance for this project')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="securityAndComplianceAccessLevel"
|
||||
:label="$options.i18n.securityAndComplianceLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][security_and_compliance_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="wiki-settings"
|
||||
:label="s__('ProjectSettings|Wiki')"
|
||||
:label="$options.i18n.wikiLabel"
|
||||
:help-text="s__('ProjectSettings|Pages for project documentation.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="wikiAccessLevel"
|
||||
:label="$options.i18n.wikiLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][wiki_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="snippet-settings"
|
||||
:label="s__('ProjectSettings|Snippets')"
|
||||
:label="$options.i18n.snippetsLabel"
|
||||
:help-text="s__('ProjectSettings|Share code with others outside the project.')"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="snippetsAccessLevel"
|
||||
:label="$options.i18n.snippetsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][snippets_access_level]"
|
||||
/>
|
||||
|
@ -625,26 +658,28 @@ export default {
|
|||
v-if="pagesAvailable && pagesAccessControlEnabled"
|
||||
ref="pages-settings"
|
||||
:help-path="pagesHelpPath"
|
||||
:label="s__('ProjectSettings|Pages')"
|
||||
:label="$options.i18n.pagesLabel"
|
||||
:help-text="
|
||||
s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
|
||||
"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="pagesAccessLevel"
|
||||
:label="$options.i18n.pagesLabel"
|
||||
:options="pagesFeatureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][pages_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
<project-setting-row
|
||||
ref="operations-settings"
|
||||
:label="s__('ProjectSettings|Operations')"
|
||||
:label="$options.i18n.operationsLabel"
|
||||
:help-text="
|
||||
s__('ProjectSettings|Configure your project resources and monitor their health.')
|
||||
"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="operationsAccessLevel"
|
||||
:label="$options.i18n.operationsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][operations_access_level]"
|
||||
/>
|
||||
|
|
|
@ -125,14 +125,14 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def filter_items(items)
|
||||
# Selection by group is already covered by `by_project` and `projects` for project-based issuables
|
||||
# Group-based issuables have their own group filter methods
|
||||
items = by_project(items)
|
||||
items = by_group(items)
|
||||
items = by_scope(items)
|
||||
items = by_created_at(items)
|
||||
items = by_updated_at(items)
|
||||
items = by_closed_at(items)
|
||||
items = by_state(items)
|
||||
items = by_group(items)
|
||||
items = by_assignee(items)
|
||||
items = by_author(items)
|
||||
items = by_non_archived(items)
|
||||
|
@ -320,11 +320,6 @@ class IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def by_group(items)
|
||||
# Selection by group is already covered by `by_project` and `projects`
|
||||
items
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def by_project(items)
|
||||
if params.project?
|
||||
|
|
|
@ -11,7 +11,18 @@ module Resolvers
|
|||
required: false,
|
||||
description: 'Filter jobs by the type of security report they produce.'
|
||||
|
||||
def resolve(security_report_types: [])
|
||||
argument :statuses, [::Types::Ci::JobStatusEnum],
|
||||
required: false,
|
||||
description: 'Filter jobs by status.'
|
||||
|
||||
def resolve(statuses: nil, security_report_types: [])
|
||||
jobs = init_collection(security_report_types)
|
||||
jobs = jobs.with_status(statuses) if statuses.present?
|
||||
|
||||
jobs
|
||||
end
|
||||
|
||||
def init_collection(security_report_types)
|
||||
if security_report_types.present?
|
||||
::Security::SecurityJobsFinder.new(
|
||||
pipeline: pipeline,
|
||||
|
|
|
@ -16,7 +16,7 @@ module Resolvers
|
|||
|
||||
def preloads
|
||||
{
|
||||
statuses: [:needs]
|
||||
jobs: { latest_statuses: [:needs] }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
class JobStatusEnum < BaseEnum
|
||||
graphql_name 'CiJobStatus'
|
||||
|
||||
::Ci::HasStatus::AVAILABLE_STATUSES.each do |status|
|
||||
value status.upcase,
|
||||
description: "A job that is #{status.tr('_', ' ')}.",
|
||||
value: status
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,9 @@ module Types
|
|||
graphql_name 'CiJob'
|
||||
authorize :read_commit_status
|
||||
|
||||
field :id, GraphQL::ID_TYPE, null: false,
|
||||
connection_type_class(Types::CountableConnectionType)
|
||||
|
||||
field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true,
|
||||
description: 'ID of the job.'
|
||||
field :pipeline, Types::Ci::PipelineType, null: true,
|
||||
description: 'Pipeline the job belongs to.'
|
||||
|
@ -14,16 +16,33 @@ module Types
|
|||
description: 'Name of the job.'
|
||||
field :needs, BuildNeedType.connection_type, null: true,
|
||||
description: 'References to builds that must complete before the jobs run.'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the job.'
|
||||
field :scheduled_at, Types::TimeType, null: true,
|
||||
description: 'Schedule for the build.'
|
||||
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
|
||||
description: 'Artifacts generated by the job.'
|
||||
field :finished_at, Types::TimeType, null: true,
|
||||
description: 'When a job has finished running.'
|
||||
field :status,
|
||||
type: ::Types::Ci::JobStatusEnum,
|
||||
null: true,
|
||||
description: "Status of the job."
|
||||
field :stage, Types::Ci::StageType, null: true,
|
||||
description: 'Stage of the job.'
|
||||
field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
|
||||
description: 'Whether this job is allowed to fail.'
|
||||
field :duration, GraphQL::INT_TYPE, null: true,
|
||||
description: 'Duration of the job in seconds.'
|
||||
|
||||
# Life-cycle timestamps:
|
||||
field :created_at, Types::TimeType, null: false,
|
||||
description: "When the job was created."
|
||||
field :queued_at, Types::TimeType, null: true,
|
||||
description: 'When the job was enqueued and marked as pending.'
|
||||
field :started_at, Types::TimeType, null: true,
|
||||
description: 'When the job was started.'
|
||||
field :finished_at, Types::TimeType, null: true,
|
||||
description: 'When a job has finished running.'
|
||||
field :scheduled_at, Types::TimeType, null: true,
|
||||
description: 'Schedule for the build.'
|
||||
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the job.'
|
||||
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
|
||||
description: 'Artifacts generated by the job.'
|
||||
field :short_sha, type: GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Short SHA1 ID of the commit.'
|
||||
|
||||
|
@ -40,6 +59,30 @@ module Types
|
|||
object.job_artifacts
|
||||
end
|
||||
end
|
||||
|
||||
def stage
|
||||
::Gitlab::Graphql::Lazy.with_value(pipeline) do |pl|
|
||||
BatchLoader::GraphQL.for([pl, object.stage]).batch do |ids, loader|
|
||||
by_pipeline = ids
|
||||
.group_by(&:first)
|
||||
.transform_values { |grp| grp.map(&:second) }
|
||||
|
||||
by_pipeline.each do |p, names|
|
||||
p.stages.by_name(names).each { |s| loader.call([p, s.name], s) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This class is a secret union!
|
||||
# TODO: turn this into an actual union, so that fields can be referenced safely!
|
||||
def id
|
||||
return unless object.id.present?
|
||||
|
||||
model_name = object.type || ::CommitStatus.name
|
||||
id = object.id
|
||||
Gitlab::GlobalId.build(model_name: model_name, id: id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,6 +81,20 @@ module Types
|
|||
description: 'Jobs belonging to the pipeline.',
|
||||
resolver: ::Resolvers::Ci::JobsResolver
|
||||
|
||||
field :job,
|
||||
type: ::Types::Ci::JobType,
|
||||
null: true,
|
||||
description: 'A specific job in this pipeline, either by name or ID.' do
|
||||
argument :id,
|
||||
type: ::Types::GlobalIDType[::CommitStatus],
|
||||
required: false,
|
||||
description: 'ID of the job.'
|
||||
argument :name,
|
||||
type: ::GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Name of the job.'
|
||||
end
|
||||
|
||||
field :source_job, Types::Ci::JobType, null: true,
|
||||
description: 'Job where pipeline was triggered from.'
|
||||
|
||||
|
@ -105,7 +119,7 @@ module Types
|
|||
description: 'Indicates if the pipeline is active.'
|
||||
|
||||
def detailed_status
|
||||
object.detailed_status(context[:current_user])
|
||||
object.detailed_status(current_user)
|
||||
end
|
||||
|
||||
def user
|
||||
|
@ -119,6 +133,19 @@ module Types
|
|||
def path
|
||||
::Gitlab::Routing.url_helpers.project_pipeline_path(object.project, object)
|
||||
end
|
||||
|
||||
def job(id: nil, name: nil)
|
||||
raise ::Gitlab::Graphql::Errors::ArgumentError, 'One of id or name is required' unless id || name
|
||||
|
||||
if id
|
||||
id = ::Types::GlobalIDType[::CommitStatus].coerce_isolated_input(id) if id
|
||||
pipeline.statuses.id_in(id.model_id)
|
||||
else
|
||||
pipeline.statuses.by_name(name)
|
||||
end.take # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
alias_method :pipeline, :object
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,10 +12,13 @@ module Types
|
|||
extras: [:lookahead],
|
||||
description: 'Group of jobs for the stage.'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the stage.'
|
||||
description: 'Detailed status of the stage.'
|
||||
field :jobs, Ci::JobType.connection_type, null: true,
|
||||
description: 'Jobs for the stage.',
|
||||
method: 'latest_statuses'
|
||||
|
||||
def detailed_status
|
||||
object.detailed_status(context[:current_user])
|
||||
object.detailed_status(current_user)
|
||||
end
|
||||
|
||||
# Issues one query per pipeline
|
||||
|
|
|
@ -67,6 +67,17 @@ module Types
|
|||
graphql_name
|
||||
end
|
||||
|
||||
define_singleton_method(:as) do |new_name|
|
||||
if @renamed && graphql_name != new_name
|
||||
raise "Conflicting names for ID of #{model_class.name}: " \
|
||||
"#{graphql_name} and #{new_name}"
|
||||
end
|
||||
|
||||
@renamed = true
|
||||
graphql_name(new_name)
|
||||
self
|
||||
end
|
||||
|
||||
define_singleton_method(:coerce_result) do |gid, ctx|
|
||||
global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_class.name)
|
||||
|
||||
|
|
|
@ -52,9 +52,9 @@ class ApplicationRecord < ActiveRecord::Base
|
|||
|
||||
# Start a new transaction with a shorter-than-usual statement timeout. This is
|
||||
# currently one third of the default 15-second timeout
|
||||
def self.with_fast_statement_timeout
|
||||
def self.with_fast_read_statement_timeout(timeout_ms = 5000)
|
||||
transaction(requires_new: true) do
|
||||
connection.exec_query("SET LOCAL statement_timeout = 5000")
|
||||
connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
|
||||
|
||||
yield
|
||||
end
|
||||
|
@ -79,3 +79,5 @@ class ApplicationRecord < ActiveRecord::Base
|
|||
enum(enum_mod.key => values)
|
||||
end
|
||||
end
|
||||
|
||||
ApplicationRecord.prepend_if_ee('EE::ApplicationRecordHelpers')
|
||||
|
|
|
@ -22,6 +22,13 @@ module Ci
|
|||
@jobs = jobs
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.present? && other.is_a?(self.class) &&
|
||||
project == other.project &&
|
||||
stage == other.stage &&
|
||||
name == other.name
|
||||
end
|
||||
|
||||
def status
|
||||
strong_memoize(:status) do
|
||||
status_struct.status
|
||||
|
|
|
@ -20,6 +20,7 @@ module Ci
|
|||
|
||||
scope :ordered, -> { order(position: :asc) }
|
||||
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
|
||||
scope :by_name, ->(names) { where(name: names) }
|
||||
|
||||
with_options unless: :importing? do
|
||||
validates :project, presence: true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.form-actions
|
||||
= button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-success js-commit-button qa-commit-button'
|
||||
= button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button'
|
||||
|
||||
= link_to 'Cancel', cancel_path,
|
||||
class: 'gl-button btn btn-default btn-cancel', data: {confirm: leave_edit_message}
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
%p
|
||||
Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production!
|
||||
- if can?(current_user, :admin_project, @project)
|
||||
= link_to "Get started", edit_project_path(@project), class: "gl-button btn btn-success"
|
||||
= link_to "Get started", edit_project_path(@project), class: "gl-button btn btn-confirm"
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
edit
|
||||
files in this project directly. Please fork this project,
|
||||
make your changes there, and submit a merge request.
|
||||
= link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-inverted btn-success'
|
||||
= link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-confirm-secondary'
|
||||
%button.js-cancel-fork-suggestion-button.gl-button.btn.btn-grouped{ type: 'button' }
|
||||
Cancel
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
= s_('InviteMember|Invite your team')
|
||||
%p= s_('InviteMember|Add members to this project and start collaborating with your team.')
|
||||
= link_to s_('InviteMember|Invite members'), project_project_members_path(@project, sort: :access_level_desc),
|
||||
class: 'gl-button btn btn-success gl-mb-8 gl-xs-w-full',
|
||||
class: 'gl-button btn btn-confirm gl-mb-8 gl-xs-w-full',
|
||||
data: { track_event: 'click_button', track_label: 'invite_members_empty_project' }
|
||||
|
|
|
@ -62,5 +62,5 @@
|
|||
.option-description
|
||||
= s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
|
||||
|
||||
= f.submit _('Create project'), class: "btn gl-button btn-success", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
|
||||
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
|
||||
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel", track_value: "" }
|
||||
|
|
|
@ -24,4 +24,4 @@
|
|||
distributed with computer software, forming part of its documentation.
|
||||
GitLab will render it here instead of this message.
|
||||
%p
|
||||
= link_to "Add Readme", @project.add_readme_path, class: 'btn btn-success'
|
||||
= link_to "Add Readme", @project.add_readme_path, class: 'gl-button btn btn-confirm'
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
.js-project-permissions-form
|
||||
- if show_visibility_confirm_modal?(@project)
|
||||
= render "visibility_modal"
|
||||
= f.submit _('Save changes'), class: "btn gl-button btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
|
||||
= f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
|
||||
|
||||
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
|
||||
.settings-header
|
||||
|
@ -37,7 +37,7 @@
|
|||
= form_for @project, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
|
||||
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
|
||||
= render 'projects/merge_request_settings', form: f
|
||||
= f.submit _('Save changes'), class: "btn gl-button btn-success rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
|
||||
= f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
|
||||
|
||||
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
|
||||
|
||||
|
|
|
@ -17,17 +17,18 @@
|
|||
- tracing_link = link_to project_tracing_path(@project) do
|
||||
%span
|
||||
= _('Tracing')
|
||||
= _("To open Jaeger and easily view tracing from GitLab, link the %{link} page to your server").html_safe % { link: tracing_link }
|
||||
= _("To open Jaeger from GitLab to view tracing from the %{link} page, add a URL to your Jaeger server.").html_safe % { link: tracing_link }
|
||||
= link_to _('Learn more.'), help_page_path('operations/tracing'), target: '_blank', rel: 'noopener noreferrer'
|
||||
.settings-content
|
||||
= form_for @project, url: project_settings_operations_path(@project), method: :patch do |f|
|
||||
= form_errors(@project)
|
||||
.form-group
|
||||
= f.fields_for :tracing_setting_attributes, setting do |form|
|
||||
= form.label :external_url, _('Jaeger URL'), class: 'label-bold'
|
||||
= form.url_field :external_url, class: 'form-control gl-form-input', placeholder: 'e.g. https://jaeger.mycompany.com'
|
||||
= form.url_field :external_url, class: 'form-control gl-form-input', placeholder: 'https://jaeger.example.com'
|
||||
%p.form-text.text-muted
|
||||
- jaeger_help_url = "https://www.jaegertracing.io/docs/1.7/getting-started/"
|
||||
- jaeger_help_url = "https://www.jaegertracing.io/docs/getting-started/"
|
||||
- link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url }
|
||||
- link_end_tag = "#{sprite_icon('external-link', css_class: 'ml-1 vertical-align-middle')}</a>".html_safe
|
||||
= _("For more information, please review %{link_start_tag}Jaeger's configuration doc%{link_end_tag}").html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
|
||||
= _("Learn more about %{link_start_tag}Jaeger configuration%{link_end_tag}.").html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
|
||||
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
- if new_project_snippet_link.present?
|
||||
.nav-controls
|
||||
= link_to _("New snippet"), new_project_snippet_link, class: "btn btn-success", title: _("New snippet")
|
||||
= link_to _("New snippet"), new_project_snippet_link, class: "gl-button btn btn-confirm", title: _("New snippet")
|
||||
|
||||
= render 'shared/snippets/list'
|
||||
- else
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
.text-content
|
||||
%h4.text-left= _('Troubleshoot and monitor your application with tracing')
|
||||
%p
|
||||
- jaeger_help_url = "https://www.jaegertracing.io/docs/1.7/getting-started/"
|
||||
- jaeger_help_url = "https://www.jaegertracing.io/docs/getting-started/"
|
||||
- link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url }
|
||||
- link_end_tag = "#{sprite_icon('external-link', css_class: 'ml-1 vertical-align-middle')}</a>".html_safe
|
||||
= _('To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
|
||||
= _('Add a Jaeger URL to replace this page with a link to your Jaeger server. You first need to %{link_start_tag}install Jaeger%{link_end_tag}.').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
|
||||
|
||||
.text-center
|
||||
= render 'tracing_button'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds CI pipeline and job features to GraphQL API
|
||||
merge_request: 44703
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move from btn-success to btn-confirm in projects/snippets directory
|
||||
merge_request: 56939
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move from btn-success to btn-confirm in projects directory
|
||||
merge_request: 56943
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add labels to UI toggles
|
||||
merge_request: 56848
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactor docs and UI for Jaeger tracing
|
||||
merge_request: 56819
|
||||
author:
|
||||
type: other
|
|
@ -526,6 +526,9 @@ If you need to manually remove job artifacts associated with multiple jobs while
|
|||
- `3.months.ago`
|
||||
- `1.year.ago`
|
||||
|
||||
`erase_erasable_artifacts!` is a synchronous method, and upon execution, the artifacts are removed immediately.
|
||||
They are not scheduled via some background queue.
|
||||
|
||||
#### Delete job artifacts and logs from jobs completed before a specific date
|
||||
|
||||
WARNING:
|
||||
|
|
|
@ -1194,16 +1194,22 @@ An edge in a connection.
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `allowFailure` | [`Boolean!`](#boolean) | Whether this job is allowed to fail. |
|
||||
| `artifacts` | [`CiJobArtifactConnection`](#cijobartifactconnection) | Artifacts generated by the job. |
|
||||
| `createdAt` | [`Time!`](#time) | When the job was created. |
|
||||
| `detailedStatus` | [`DetailedStatus`](#detailedstatus) | Detailed status of the job. |
|
||||
| `duration` | [`Int`](#int) | Duration of the job in seconds. |
|
||||
| `finishedAt` | [`Time`](#time) | When a job has finished running. |
|
||||
| `id` | [`ID!`](#id) | ID of the job. |
|
||||
| `id` | [`JobID`](#jobid) | ID of the job. |
|
||||
| `name` | [`String`](#string) | Name of the job. |
|
||||
| `needs` | [`CiBuildNeedConnection`](#cibuildneedconnection) | References to builds that must complete before the jobs run. |
|
||||
| `pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. |
|
||||
| `queuedAt` | [`Time`](#time) | When the job was enqueued and marked as pending. |
|
||||
| `scheduledAt` | [`Time`](#time) | Schedule for the build. |
|
||||
| `shortSha` | [`String!`](#string) | Short SHA1 ID of the commit. |
|
||||
| `stage` | [`CiStage`](#cistage) | Stage of the job. |
|
||||
| `startedAt` | [`Time`](#time) | When the job was started. |
|
||||
| `status` | [`CiJobStatus`](#cijobstatus) | Status of the job. |
|
||||
|
||||
### `CiJobArtifact`
|
||||
|
||||
|
@ -1237,6 +1243,7 @@ The connection type for CiJob.
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `count` | [`Int!`](#int) | Total count of collection. |
|
||||
| `edges` | [`[CiJobEdge]`](#cijobedge) | A list of edges. |
|
||||
| `nodes` | [`[CiJob]`](#cijob) | A list of nodes. |
|
||||
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
@ -1256,6 +1263,7 @@ An edge in a connection.
|
|||
| ----- | ---- | ----------- |
|
||||
| `detailedStatus` | [`DetailedStatus`](#detailedstatus) | Detailed status of the stage. |
|
||||
| `groups` | [`CiGroupConnection`](#cigroupconnection) | Group of jobs for the stage. |
|
||||
| `jobs` | [`CiJobConnection`](#cijobconnection) | Jobs for the stage. |
|
||||
| `name` | [`String`](#string) | Name of the stage. |
|
||||
|
||||
### `CiStageConnection`
|
||||
|
@ -4582,6 +4590,7 @@ Information about pagination in a connection.
|
|||
| `finishedAt` | [`Time`](#time) | Timestamp of the pipeline's completion. |
|
||||
| `id` | [`ID!`](#id) | ID of the pipeline. |
|
||||
| `iid` | [`String!`](#string) | Internal ID of the pipeline. |
|
||||
| `job` | [`CiJob`](#cijob) | A specific job in this pipeline, either by name or ID. |
|
||||
| `jobs` | [`CiJobConnection`](#cijobconnection) | Jobs belonging to the pipeline. |
|
||||
| `path` | [`String`](#string) | Relative path to the pipeline's page. |
|
||||
| `project` | [`Project`](#project) | Project the pipeline belongs to. |
|
||||
|
@ -7274,6 +7283,22 @@ Values for YAML processor result.
|
|||
| `INVALID` | The configuration file is not valid. |
|
||||
| `VALID` | The configuration file is valid. |
|
||||
|
||||
### `CiJobStatus`
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| `CANCELED` | A job that is canceled. |
|
||||
| `CREATED` | A job that is created. |
|
||||
| `FAILED` | A job that is failed. |
|
||||
| `MANUAL` | A job that is manual. |
|
||||
| `PENDING` | A job that is pending. |
|
||||
| `PREPARING` | A job that is preparing. |
|
||||
| `RUNNING` | A job that is running. |
|
||||
| `SCHEDULED` | A job that is scheduled. |
|
||||
| `SKIPPED` | A job that is skipped. |
|
||||
| `SUCCESS` | A job that is success. |
|
||||
| `WAITING_FOR_RESOURCE` | A job that is waiting for resource. |
|
||||
|
||||
### `CommitActionMode`
|
||||
|
||||
Mode of a commit action.
|
||||
|
@ -8472,6 +8497,12 @@ An example `IterationsCadenceID` is: `"gid://gitlab/Iterations::Cadence/1"`.
|
|||
|
||||
Represents untyped JSON.
|
||||
|
||||
### `JobID`
|
||||
|
||||
A `CommitStatusID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `CommitStatusID` is: `"gid://gitlab/CommitStatus/1"`.
|
||||
|
||||
### `JsonString`
|
||||
|
||||
JSON object as raw string.
|
||||
|
|
|
@ -9,27 +9,26 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7903) in GitLab Ultimate 11.5.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42645) to GitLab Free in 13.5.
|
||||
|
||||
Tracing provides insight into the performance and health of a deployed application,
|
||||
tracking each function or microservice which handles a given request.
|
||||
Tracing provides insight into the performance and health of a deployed application, tracking each
|
||||
function or microservice that handles a given request. Tracing makes it easy to understand the
|
||||
end-to-end flow of a request, regardless of whether you are using a monolithic or distributed
|
||||
system.
|
||||
|
||||
This makes it easy to
|
||||
understand the end-to-end flow of a request, regardless of whether you are using a monolithic or distributed system.
|
||||
## Install Jaeger
|
||||
|
||||
## Jaeger tracing
|
||||
|
||||
[Jaeger](https://www.jaegertracing.io/) is an open source, end-to-end distributed
|
||||
tracing system used for monitoring and troubleshooting microservices-based distributed
|
||||
systems.
|
||||
|
||||
### Deploying Jaeger
|
||||
|
||||
To learn more about deploying Jaeger, read the official
|
||||
[Jaeger](https://www.jaegertracing.io/) is an open source, end-to-end distributed tracing system
|
||||
used for monitoring and troubleshooting microservices-based distributed systems. To learn more about
|
||||
installing Jaeger, read the official
|
||||
[Getting Started documentation](https://www.jaegertracing.io/docs/latest/getting-started/).
|
||||
There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#AllinoneDockerimage),
|
||||
as well as deployment options for [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes)
|
||||
and [OpenShift](https://github.com/jaegertracing/jaeger-openshift).
|
||||
|
||||
### Enabling Jaeger
|
||||
See also:
|
||||
|
||||
- An [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one).
|
||||
- Deployment options for:
|
||||
- [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes).
|
||||
- [OpenShift](https://github.com/jaegertracing/jaeger-openshift).
|
||||
|
||||
## Link to Jaeger
|
||||
|
||||
GitLab provides an easy way to open the Jaeger UI from within your project:
|
||||
|
||||
|
@ -37,5 +36,5 @@ GitLab provides an easy way to open the Jaeger UI from within your project:
|
|||
[client libraries](https://www.jaegertracing.io/docs/latest/client-libraries/).
|
||||
1. Navigate to your project's **Settings > Operations** and provide the Jaeger URL.
|
||||
1. Click **Save changes** for the changes to take effect.
|
||||
1. You can now visit **Operations > Tracing** in your project's sidebar and
|
||||
GitLab redirects you to the configured Jaeger URL.
|
||||
1. You can now visit **Operations > Tracing** in your project's sidebar and GitLab redirects you to
|
||||
the configured Jaeger URL.
|
||||
|
|
|
@ -6,8 +6,8 @@ module API
|
|||
# against the graphql API. Helper code for the graphql server implementation
|
||||
# should be in app/graphql/ or lib/gitlab/graphql/
|
||||
module GraphqlHelpers
|
||||
def run_graphql!(query:, context: {}, transform: nil)
|
||||
result = GitlabSchema.execute(query, context: context)
|
||||
def run_graphql!(query:, context: {}, variables: nil, transform: nil)
|
||||
result = GitlabSchema.execute(query, variables: variables, context: context)
|
||||
|
||||
if transform
|
||||
transform.call(result)
|
||||
|
|
|
@ -78,7 +78,7 @@ module Gitlab
|
|||
# to perform the calculation more efficiently. Until then, use a shorter
|
||||
# timeout and return -1 as a sentinel value if it is triggered
|
||||
begin
|
||||
ApplicationRecord.with_fast_statement_timeout do
|
||||
ApplicationRecord.with_fast_read_statement_timeout do
|
||||
finder.count_by_state
|
||||
end
|
||||
rescue ActiveRecord::QueryCanceled => err
|
||||
|
|
|
@ -1820,6 +1820,9 @@ msgstr ""
|
|||
msgid "Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "Add a Jaeger URL to replace this page with a link to your Jaeger server. You first need to %{link_start_tag}install Jaeger%{link_end_tag}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Add a bullet list"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13488,9 +13491,6 @@ msgstr ""
|
|||
msgid "For more information, go to the "
|
||||
msgstr ""
|
||||
|
||||
msgid "For more information, please review %{link_start_tag}Jaeger's configuration doc%{link_end_tag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "For more information, see the File Hooks documentation."
|
||||
msgstr ""
|
||||
|
||||
|
@ -17951,6 +17951,9 @@ msgstr ""
|
|||
msgid "Learn more"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more about %{link_start_tag}Jaeger configuration%{link_end_tag}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more about %{username}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21795,6 +21798,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|Add composer registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Allow duplicates"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|An error occurred while saving the settings"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31680,9 +31686,6 @@ msgstr ""
|
|||
msgid "To get started, click the link below to confirm your account."
|
||||
msgstr ""
|
||||
|
||||
msgid "To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "To get started, please enter your Gitea Host URL and a %{link_to_personal_token}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -31710,7 +31713,7 @@ msgstr ""
|
|||
msgid "To only use CI/CD features for an external repository, choose %{strong_open}CI/CD for external repo%{strong_close}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To open Jaeger and easily view tracing from GitLab, link the %{link} page to your server"
|
||||
msgid "To open Jaeger from GitLab to view tracing from the %{link} page, add a URL to your Jaeger server."
|
||||
msgstr ""
|
||||
|
||||
msgid "To personalize your GitLab experience, we'd like to know a bit more about you. We won't share this information with anyone."
|
||||
|
|
|
@ -30,6 +30,21 @@ FactoryBot.define do
|
|||
yaml_variables { nil }
|
||||
end
|
||||
|
||||
trait :unique_name do
|
||||
name { generate(:job_name) }
|
||||
end
|
||||
|
||||
trait :dependent do
|
||||
transient do
|
||||
sequence(:needed_name) { |n| "dependency #{n}" }
|
||||
needed { association(:ci_build, name: needed_name, pipeline: pipeline) }
|
||||
end
|
||||
|
||||
after(:create) do |build, evaluator|
|
||||
build.needs << create(:ci_build_need, build: build, name: evaluator.needed.name)
|
||||
end
|
||||
end
|
||||
|
||||
trait :started do
|
||||
started_at { 'Di 29. Okt 09:51:28 CET 2013' }
|
||||
end
|
||||
|
|
|
@ -19,4 +19,5 @@ FactoryBot.define do
|
|||
sequence(:wip_title) { |n| "WIP: #{n}" }
|
||||
sequence(:jira_title) { |n| "[PROJ-#{n}]: fix bug" }
|
||||
sequence(:jira_branch) { |n| "feature/PROJ-#{n}" }
|
||||
sequence(:job_name) { |n| "job #{n}" }
|
||||
end
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.config.devtools = false;
|
||||
|
||||
export default Vue;
|
||||
export * from 'vue';
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { config as vueConfig } from 'vue';
|
||||
import Vue from 'vue';
|
||||
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
|
||||
|
||||
describe('Issue Time Estimate component', () => {
|
||||
|
@ -34,10 +34,10 @@ describe('Issue Time Estimate component', () => {
|
|||
|
||||
try {
|
||||
// This will raise props validating warning by Vue, silencing it
|
||||
vueConfig.silent = true;
|
||||
Vue.config.silent = true;
|
||||
await wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
|
||||
} finally {
|
||||
vueConfig.silent = false;
|
||||
Vue.config.silent = false;
|
||||
}
|
||||
|
||||
expect(alertSpy).not.toHaveBeenCalled();
|
||||
|
|
|
@ -123,6 +123,10 @@ describe('feature flag form', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('has label', () => {
|
||||
expect(findGlToggle().props('label')).toBe(Form.i18n.statusLabel);
|
||||
});
|
||||
|
||||
it('should be disabled if the feature flag is not active', (done) => {
|
||||
wrapper.setProps({ active: false });
|
||||
wrapper.vm.$nextTick(() => {
|
||||
|
|
|
@ -59,7 +59,10 @@ describe('Maven Settings', () => {
|
|||
mountComponent();
|
||||
|
||||
expect(findToggle().exists()).toBe(true);
|
||||
expect(findToggle().props('value')).toBe(defaultProps.mavenDuplicatesAllowed);
|
||||
expect(findToggle().props()).toMatchObject({
|
||||
label: component.i18n.MAVEN_TOGGLE_LABEL,
|
||||
value: defaultProps.mavenDuplicatesAllowed,
|
||||
});
|
||||
});
|
||||
|
||||
it('toggle emits an update event', () => {
|
||||
|
|
|
@ -46,6 +46,7 @@ const defaultProps = {
|
|||
pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control',
|
||||
packagesAvailable: false,
|
||||
packagesHelpPath: '/help/user/packages/index',
|
||||
requestCveAvailable: true,
|
||||
};
|
||||
|
||||
describe('Settings Panel', () => {
|
||||
|
@ -76,6 +77,7 @@ describe('Settings Panel', () => {
|
|||
const findRepositoryFeatureSetting = () =>
|
||||
findRepositoryFeatureProjectRow().find(projectFeatureSetting);
|
||||
const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' });
|
||||
const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' });
|
||||
const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' });
|
||||
const findProjectVisibilityLevelInput = () => wrapper.find('[name="project[visibility_level]"]');
|
||||
const findRequestAccessEnabledInput = () =>
|
||||
|
@ -174,6 +176,16 @@ describe('Settings Panel', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Issues settings', () => {
|
||||
it('has label for CVE request toggle', () => {
|
||||
wrapper = mountComponent();
|
||||
|
||||
expect(findIssuesSettingsRow().findComponent(GlToggle).props('label')).toBe(
|
||||
settingsPanel.i18n.cve_request_toggle_label,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Repository', () => {
|
||||
it('should set the repository help text when the visibility level is set to private', () => {
|
||||
wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } });
|
||||
|
@ -304,6 +316,17 @@ describe('Settings Panel', () => {
|
|||
|
||||
expect(findContainerRegistryEnabledInput().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('has label for the toggle', () => {
|
||||
wrapper = mountComponent({
|
||||
currentSettings: { visibilityLevel: visibilityOptions.PUBLIC },
|
||||
registryAvailable: true,
|
||||
});
|
||||
|
||||
expect(findContainerRegistrySettings().findComponent(GlToggle).props('label')).toBe(
|
||||
settingsPanel.i18n.containerRegistryLabel,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Git Large File Storage', () => {
|
||||
|
@ -342,6 +365,15 @@ describe('Settings Panel', () => {
|
|||
expect(findLFSFeatureToggle().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('has label for toggle', () => {
|
||||
wrapper = mountComponent({
|
||||
currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
|
||||
lfsAvailable: true,
|
||||
});
|
||||
|
||||
expect(findLFSFeatureToggle().props('label')).toBe(settingsPanel.i18n.lfsLabel);
|
||||
});
|
||||
|
||||
it('should not change lfsEnabled when disabling the repository', async () => {
|
||||
// mount over shallowMount, because we are aiming to test rendered state of toggle
|
||||
wrapper = mountComponent({ currentSettings: { lfsEnabled: true } }, mount);
|
||||
|
@ -432,6 +464,17 @@ describe('Settings Panel', () => {
|
|||
|
||||
expect(findPackagesEnabledInput().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('has label for toggle', () => {
|
||||
wrapper = mountComponent({
|
||||
currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
|
||||
packagesAvailable: true,
|
||||
});
|
||||
|
||||
expect(findPackagesEnabledInput().findComponent(GlToggle).props('label')).toBe(
|
||||
settingsPanel.i18n.packagesLabel,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pages', () => {
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import navControlsComp from '~/pipelines/components/pipelines_list/nav_controls.vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
|
||||
|
||||
describe('Pipelines Nav Controls', () => {
|
||||
let NavControlsComponent;
|
||||
let component;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
NavControlsComponent = Vue.extend(navControlsComp);
|
||||
});
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMount(NavControls, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findRunPipeline = () => wrapper.find('.js-run-pipeline');
|
||||
|
||||
afterEach(() => {
|
||||
component.$destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render link to create a new pipeline', () => {
|
||||
|
@ -21,12 +26,11 @@ describe('Pipelines Nav Controls', () => {
|
|||
resetCachePath: 'foo',
|
||||
};
|
||||
|
||||
component = mountComponent(NavControlsComponent, mockData);
|
||||
createComponent(mockData);
|
||||
|
||||
expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
|
||||
expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
|
||||
mockData.newPipelinePath,
|
||||
);
|
||||
const runPipeline = findRunPipeline();
|
||||
expect(runPipeline.text()).toContain('Run Pipeline');
|
||||
expect(runPipeline.attributes('href')).toBe(mockData.newPipelinePath);
|
||||
});
|
||||
|
||||
it('should not render link to create pipeline if no path is provided', () => {
|
||||
|
@ -36,9 +40,9 @@ describe('Pipelines Nav Controls', () => {
|
|||
resetCachePath: 'foo',
|
||||
};
|
||||
|
||||
component = mountComponent(NavControlsComponent, mockData);
|
||||
createComponent(mockData);
|
||||
|
||||
expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
|
||||
expect(findRunPipeline().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render link for CI lint', () => {
|
||||
|
@ -49,12 +53,10 @@ describe('Pipelines Nav Controls', () => {
|
|||
resetCachePath: 'foo',
|
||||
};
|
||||
|
||||
component = mountComponent(NavControlsComponent, mockData);
|
||||
createComponent(mockData);
|
||||
|
||||
expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
|
||||
expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(
|
||||
mockData.ciLintPath,
|
||||
);
|
||||
expect(wrapper.find('.js-ci-lint').text().trim()).toContain('CI Lint');
|
||||
expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(mockData.ciLintPath);
|
||||
});
|
||||
|
||||
describe('Reset Runners Cache', () => {
|
||||
|
@ -64,22 +66,20 @@ describe('Pipelines Nav Controls', () => {
|
|||
ciLintPath: 'foo',
|
||||
resetCachePath: 'foo',
|
||||
};
|
||||
|
||||
component = mountComponent(NavControlsComponent, mockData);
|
||||
createComponent(mockData);
|
||||
});
|
||||
|
||||
it('should render button for resetting runner caches', () => {
|
||||
expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain(
|
||||
'Clear Runner Caches',
|
||||
);
|
||||
expect(wrapper.find('.js-clear-cache').text().trim()).toContain('Clear Runner Caches');
|
||||
});
|
||||
|
||||
it('should emit postAction event when reset runner cache button is clicked', () => {
|
||||
jest.spyOn(component, '$emit').mockImplementation(() => {});
|
||||
it('should emit postAction event when reset runner cache button is clicked', async () => {
|
||||
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
|
||||
|
||||
component.$el.querySelector('.js-clear-cache').click();
|
||||
wrapper.find('.js-clear-cache').vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(component.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo');
|
||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['CiJobStatus'] do
|
||||
it 'exposes all job status values' do
|
||||
expect(described_class.values.values).to contain_exactly(
|
||||
*::Ci::HasStatus::AVAILABLE_STATUSES.map do |status|
|
||||
have_attributes(value: status, graphql_name: status.upcase)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
|
@ -8,16 +8,23 @@ RSpec.describe Types::Ci::JobType do
|
|||
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
allow_failure
|
||||
artifacts
|
||||
created_at
|
||||
detailedStatus
|
||||
duration
|
||||
finished_at
|
||||
id
|
||||
shortSha
|
||||
pipeline
|
||||
name
|
||||
needs
|
||||
detailedStatus
|
||||
pipeline
|
||||
queued_at
|
||||
scheduledAt
|
||||
artifacts
|
||||
finished_at
|
||||
duration
|
||||
scheduledAt
|
||||
shortSha
|
||||
stage
|
||||
started_at
|
||||
status
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe Types::Ci::PipelineType do
|
|||
expected_fields = %w[
|
||||
id iid sha before_sha status detailed_status config_source duration
|
||||
coverage created_at updated_at started_at finished_at committed_at
|
||||
stages user retryable cancelable jobs source_job downstream
|
||||
stages user retryable cancelable jobs job source_job downstream
|
||||
upstream path project active user_permissions warnings commit_path
|
||||
]
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do
|
|||
name
|
||||
groups
|
||||
detailedStatus
|
||||
jobs
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -100,4 +100,33 @@ RSpec.describe ApplicationRecord do
|
|||
expect(User.where_exists(User.limit(1))).to eq([user])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_fast_read_statement_timeout' do
|
||||
context 'when the query runs faster than configured timeout' do
|
||||
it 'executes the query without error' do
|
||||
result = nil
|
||||
|
||||
expect do
|
||||
described_class.with_fast_read_statement_timeout(100) do
|
||||
result = described_class.connection.exec_query('SELECT 1')
|
||||
end
|
||||
end.not_to raise_error
|
||||
|
||||
expect(result).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
# This query hangs for 10ms and then gets cancelled. As there is no
|
||||
# other way to test the timeout for sure, 10ms of waiting seems to be
|
||||
# reasonable!
|
||||
context 'when the query runs longer than configured timeout' do
|
||||
it 'cancels the query and raises an exception' do
|
||||
expect do
|
||||
described_class.with_fast_read_statement_timeout(10) do
|
||||
described_class.connection.exec_query('SELECT pg_sleep(0.1)')
|
||||
end
|
||||
end.to raise_error(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,6 +27,18 @@ RSpec.describe Ci::Stage, :models do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.by_name' do
|
||||
it 'finds stages by name' do
|
||||
a = create(:ci_stage_entity, name: 'a')
|
||||
b = create(:ci_stage_entity, name: 'b')
|
||||
c = create(:ci_stage_entity, name: 'c')
|
||||
|
||||
expect(described_class.by_name('a')).to contain_exactly(a)
|
||||
expect(described_class.by_name('b')).to contain_exactly(b)
|
||||
expect(described_class.by_name(%w[a c])).to contain_exactly(a, c)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#status' do
|
||||
context 'when stage is pending' do
|
||||
let(:stage) { create(:ci_stage_entity, status: 'pending') }
|
||||
|
|
|
@ -4,10 +4,14 @@ require 'spec_helper'
|
|||
RSpec.describe 'Query.project.pipeline.stages.groups' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:project) { create(:project, :repository, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
|
||||
let(:group_graphql_data) { graphql_data.dig('project', 'pipeline', 'stages', 'nodes', 0, 'groups', 'nodes') }
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
|
||||
let(:group_graphql_data) { graphql_data_at(:project, :pipeline, :stages, :nodes, 0, :groups, :nodes) }
|
||||
|
||||
let_it_be(:job_a) { create(:commit_status, pipeline: pipeline, name: 'rspec 0 2') }
|
||||
let_it_be(:job_b) { create(:ci_build, pipeline: pipeline, name: 'rspec 0 1') }
|
||||
let_it_be(:job_c) { create(:ci_bridge, pipeline: pipeline, name: 'spinach 0 1') }
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
|
@ -38,18 +42,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups' do
|
|||
end
|
||||
|
||||
before do
|
||||
create(:commit_status, pipeline: pipeline, name: 'rspec 0 2')
|
||||
create(:commit_status, pipeline: pipeline, name: 'rspec 0 1')
|
||||
create(:commit_status, pipeline: pipeline, name: 'spinach 0 1')
|
||||
post_graphql(query, current_user: user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it 'returns a array of jobs belonging to a pipeline' do
|
||||
expect(group_graphql_data.map { |g| g.slice('name', 'size') }).to eq([
|
||||
{ 'name' => 'rspec', 'size' => 2 },
|
||||
{ 'name' => 'spinach', 'size' => 1 }
|
||||
])
|
||||
expect(group_graphql_data).to contain_exactly(
|
||||
a_hash_including('name' => 'rspec', 'size' => 2),
|
||||
a_hash_including('name' => 'spinach', 'size' => 1)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create_default(:user) }
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') }
|
||||
let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') }
|
||||
|
||||
let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') }
|
||||
let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') }
|
||||
let_it_be(:job_3) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 3') }
|
||||
|
||||
let(:path_to_job) do
|
||||
[
|
||||
[:project, { full_path: project.full_path }],
|
||||
[:pipelines, { first: 1 }],
|
||||
[:nodes, nil],
|
||||
[:job, { id: global_id_of(job_2) }]
|
||||
]
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
wrap_fields(query_graphql_path(query_path, all_graphql_fields_for(terminal_type)))
|
||||
end
|
||||
|
||||
describe 'scalar fields' do
|
||||
let(:path) { [:project, :pipelines, :nodes, 0, :job] }
|
||||
let(:query_path) { path_to_job }
|
||||
let(:terminal_type) { 'CiJob' }
|
||||
|
||||
it 'retrieves scalar fields' do
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(graphql_data_at(*path)).to match a_hash_including(
|
||||
'id' => global_id_of(job_2),
|
||||
'name' => job_2.name,
|
||||
'allowFailure' => job_2.allow_failure,
|
||||
'duration' => job_2.duration,
|
||||
'status' => job_2.status.upcase
|
||||
)
|
||||
end
|
||||
|
||||
context 'when fetching by name' do
|
||||
before do
|
||||
query_path.last[1] = { name: job_2.name }
|
||||
end
|
||||
|
||||
it 'retrieves scalar fields' do
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(graphql_data_at(*path)).to match a_hash_including(
|
||||
'id' => global_id_of(job_2),
|
||||
'name' => job_2.name
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.detailedStatus' do
|
||||
let(:path) { [:project, :pipelines, :nodes, 0, :job, :detailed_status] }
|
||||
let(:query_path) { path_to_job + [:detailed_status] }
|
||||
let(:terminal_type) { 'DetailedStatus' }
|
||||
|
||||
it 'retrieves detailed status' do
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(graphql_data_at(*path)).to match a_hash_including(
|
||||
'text' => 'pending',
|
||||
'label' => 'pending',
|
||||
'action' => a_hash_including('buttonTitle' => 'Cancel this job', 'icon' => 'cancel')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.stage' do
|
||||
let(:path) { [:project, :pipelines, :nodes, 0, :job, :stage] }
|
||||
let(:query_path) { path_to_job + [:stage] }
|
||||
let(:terminal_type) { 'CiStage' }
|
||||
|
||||
it 'returns appropriate data' do
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
expect(graphql_data_at(*path)).to match a_hash_including(
|
||||
'name' => test_stage.name,
|
||||
'jobs' => a_hash_including(
|
||||
'nodes' => contain_exactly(
|
||||
a_hash_including('id' => global_id_of(job_2)),
|
||||
a_hash_including('id' => global_id_of(job_3))
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,24 +5,28 @@ require 'spec_helper'
|
|||
RSpec.describe 'getting pipeline information nested in a project' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let!(:project) { create(:project, :repository, :public) }
|
||||
let!(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:current_user) { create(:user) }
|
||||
let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] }
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline) }
|
||||
let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline) }
|
||||
let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline) }
|
||||
|
||||
let!(:query) do
|
||||
%(
|
||||
query {
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
pipeline(iid: "#{pipeline.iid}") {
|
||||
configSource
|
||||
}
|
||||
}
|
||||
}
|
||||
let(:path) { %i[project pipeline] }
|
||||
let(:pipeline_graphql_data) { graphql_data_at(*path) }
|
||||
let(:depth) { 3 }
|
||||
let(:excluded) { %w[job project] } # Project is very expensive, due to the number of fields
|
||||
let(:fields) { all_graphql_fields_for('Pipeline', excluded: excluded, max_depth: depth) }
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
:project,
|
||||
{ full_path: project.full_path },
|
||||
query_graphql_field(:pipeline, { iid: pipeline.iid.to_s }, fields)
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
it_behaves_like 'a working graphql query', :use_clean_rails_memory_store_caching, :request_store do
|
||||
before do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
@ -37,14 +41,18 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
it 'contains configSource' do
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE')
|
||||
expect(pipeline_graphql_data['configSource']).to eq('UNKNOWN_SOURCE')
|
||||
end
|
||||
|
||||
context 'batching' do
|
||||
let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
|
||||
let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
|
||||
context 'when batching' do
|
||||
let!(:pipeline2) { successful_pipeline }
|
||||
let!(:pipeline3) { successful_pipeline }
|
||||
let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
|
||||
|
||||
def successful_pipeline
|
||||
create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)])
|
||||
end
|
||||
|
||||
it 'executes the finder once' do
|
||||
mock = double(Ci::PipelinesFinder)
|
||||
opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
|
||||
|
@ -80,4 +88,151 @@ RSpec.describe 'getting pipeline information nested in a project' do
|
|||
|
||||
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
|
||||
end
|
||||
|
||||
context 'when enough data is requested' do
|
||||
let(:fields) do
|
||||
query_graphql_field(:jobs, nil,
|
||||
query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3)))
|
||||
end
|
||||
|
||||
it 'contains jobs' do
|
||||
post_graphql(query, current_user: current_user)
|
||||
|
||||
expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly(
|
||||
a_hash_including(
|
||||
'name' => build_job.name,
|
||||
'status' => build_job.status.upcase,
|
||||
'duration' => build_job.duration
|
||||
),
|
||||
a_hash_including(
|
||||
'id' => global_id_of(failed_build),
|
||||
'status' => failed_build.status.upcase
|
||||
),
|
||||
a_hash_including(
|
||||
'id' => global_id_of(bridge),
|
||||
'status' => bridge.status.upcase
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requesting only builds with certain statuses' do
|
||||
let(:variables) do
|
||||
{
|
||||
path: project.full_path,
|
||||
pipelineIID: pipeline.iid.to_s,
|
||||
status: :FAILED
|
||||
}
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
<<~GQL
|
||||
query($path: ID!, $pipelineIID: ID!, $status: CiJobStatus!) {
|
||||
project(fullPath: $path) {
|
||||
pipeline(iid: $pipelineIID) {
|
||||
jobs(statuses: [$status]) {
|
||||
nodes {
|
||||
#{all_graphql_fields_for('CiJob', max_depth: 1)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
it 'can filter build jobs by status' do
|
||||
post_graphql(query, current_user: current_user, variables: variables)
|
||||
|
||||
expect(graphql_data_at(*path, :jobs, :nodes))
|
||||
.to contain_exactly(a_hash_including('id' => global_id_of(failed_build)))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requesting a specific job' do
|
||||
let(:variables) do
|
||||
{
|
||||
path: project.full_path,
|
||||
pipelineIID: pipeline.iid.to_s
|
||||
}
|
||||
end
|
||||
|
||||
let(:build_fields) do
|
||||
all_graphql_fields_for('CiJob', max_depth: 1)
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
<<~GQL
|
||||
query($path: ID!, $pipelineIID: ID!, $jobName: String, $jobID: JobID) {
|
||||
project(fullPath: $path) {
|
||||
pipeline(iid: $pipelineIID) {
|
||||
job(id: $jobID, name: $jobName) {
|
||||
#{build_fields}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
let(:the_job) do
|
||||
a_hash_including('name' => build_job.name, 'id' => global_id_of(build_job))
|
||||
end
|
||||
|
||||
it 'can request a build by name' do
|
||||
vars = variables.merge(jobName: build_job.name)
|
||||
|
||||
post_graphql(query, current_user: current_user, variables: vars)
|
||||
|
||||
expect(graphql_data_at(*path, :job)).to match(the_job)
|
||||
end
|
||||
|
||||
it 'can request a build by ID' do
|
||||
vars = variables.merge(jobID: global_id_of(build_job))
|
||||
|
||||
post_graphql(query, current_user: current_user, variables: vars)
|
||||
|
||||
expect(graphql_data_at(*path, :job)).to match(the_job)
|
||||
end
|
||||
|
||||
context 'when we request nested fields of the build' do
|
||||
let_it_be(:needy) { create(:ci_build, :dependent, pipeline: pipeline) }
|
||||
|
||||
let(:build_fields) { 'needs { nodes { name } }' }
|
||||
let(:vars) { variables.merge(jobID: global_id_of(needy)) }
|
||||
|
||||
it 'returns the nested data' do
|
||||
post_graphql(query, current_user: current_user, variables: vars)
|
||||
|
||||
expect(graphql_data_at(*path, :job, :needs, :nodes)).to contain_exactly(
|
||||
a_hash_including('name' => needy.needs.first.name)
|
||||
)
|
||||
end
|
||||
|
||||
it 'requires a constant number of queries' do
|
||||
fst_user = create(:user)
|
||||
snd_user = create(:user)
|
||||
path = %i[project pipeline job needs nodes name]
|
||||
|
||||
baseline = ActiveRecord::QueryRecorder.new do
|
||||
post_graphql(query, current_user: fst_user, variables: vars)
|
||||
end
|
||||
|
||||
expect(baseline.count).to be > 0
|
||||
dep_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
|
||||
|
||||
deps = create_list(:ci_build, 3, :unique_name, pipeline: pipeline)
|
||||
deps.each { |d| create(:ci_build_need, build: needy, name: d.name) }
|
||||
|
||||
expect do
|
||||
post_graphql(query, current_user: snd_user, variables: vars)
|
||||
end.not_to exceed_query_limit(baseline)
|
||||
|
||||
more_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
|
||||
|
||||
expect(more_names).to include(*dep_names)
|
||||
expect(more_names.count).to be > dep_names.count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,14 +5,5 @@ module BoardHelpers
|
|||
within card do
|
||||
first('.board-card-number').click
|
||||
end
|
||||
|
||||
wait_for_sidebar
|
||||
end
|
||||
|
||||
def wait_for_sidebar
|
||||
# loop until the CSS transition is complete
|
||||
Timeout.timeout(0.5) do
|
||||
loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,11 +30,13 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
|
|||
end
|
||||
|
||||
match do |kls|
|
||||
if @allow_extra
|
||||
expect(kls.fields.keys).to include(*expected_field_names)
|
||||
else
|
||||
expect(kls.fields.keys).to contain_exactly(*expected_field_names)
|
||||
end
|
||||
keys = kls.fields.keys.to_set
|
||||
fields = expected_field_names.to_set
|
||||
|
||||
next true if fields == keys
|
||||
next true if @allow_extra && fields.proper_subset?(keys)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
failure_message do |kls|
|
||||
|
@ -108,7 +110,7 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected|
|
|||
names = expected_names(field).inspect
|
||||
args = field.arguments.keys.inspect
|
||||
|
||||
"expected that #{field.name} would have the following arguments: #{names}, but it has #{args}."
|
||||
"expected #{field.name} to have the following arguments: #{names}, but it has #{args}."
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue