Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-23 09:09:17 +00:00
parent fe6c2b9ae0
commit b3647b2a67
58 changed files with 791 additions and 171 deletions

View File

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

View File

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

View File

@ -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 })"
/>

View File

@ -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)"
/>

View File

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

View File

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

View File

@ -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]"
/>

View File

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

View File

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

View File

@ -16,7 +16,7 @@ module Resolvers
def preloads
{
statuses: [:needs]
jobs: { latest_statuses: [:needs] }
}
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,5 +62,5 @@
.option-description
= s_('ProjectsNew|Allows you to immediately clone this projects 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: "" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
title: Adds CI pipeline and job features to GraphQL API
merge_request: 44703
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in projects/snippets directory
merge_request: 56939
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in projects directory
merge_request: 56943
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add labels to UI toggles
merge_request: 56848
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Refactor docs and UI for Jaeger tracing
merge_request: 56819
author:
type: other

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import Vue from 'vue';
Vue.config.productionTip = false;
Vue.config.devtools = false;
export default Vue;
export * from 'vue';

View File

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

View File

@ -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(() => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do
name
groups
detailedStatus
jobs
]
expect(described_class).to have_graphql_fields(*expected_fields)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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