diff options
58 files changed, 789 insertions, 169 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index ea3bd07f228..4c144f06f67 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -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. diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue index 8418c0f66ac..6cbe443062a 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue @@ -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" /> diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue index 1f59b709aa2..7e3d3bc5203 100644 --- a/app/assets/javascripts/feature_flags/components/form.vue +++ b/app/assets/javascripts/feature_flags/components/form.vue @@ -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 })" /> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue index d4f51b83e1e..faacabb44ce 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue @@ -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)" /> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js index 72bec74060c..d52a6a626f9 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js @@ -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.', ); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index d62df77ad2c..c110c1d4d62 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -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"> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 419e98dad50..0b7b4c0ded1 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -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]" /> diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2409dc9d77d..8642ea11a57 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -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? diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb index dd565094017..5ae9e721cc8 100644 --- a/app/graphql/resolvers/ci/jobs_resolver.rb +++ b/app/graphql/resolvers/ci/jobs_resolver.rb @@ -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, diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb index 98170e0cd2e..a458e873935 100644 --- a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb +++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb @@ -16,7 +16,7 @@ module Resolvers def preloads { - statuses: [:needs] + jobs: { latest_statuses: [:needs] } } end end diff --git a/app/graphql/types/ci/job_status_enum.rb b/app/graphql/types/ci/job_status_enum.rb new file mode 100644 index 00000000000..ec80b1f4776 --- /dev/null +++ b/app/graphql/types/ci/job_status_enum.rb @@ -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 diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index f2529b8e5c8..eb235e6ee73 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -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 :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 :finished_at, Types::TimeType, null: true, - description: 'When a job has finished running.' - field :duration, GraphQL::INT_TYPE, null: true, - description: 'Duration of the job in seconds.' 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 diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 49be200a788..aaaa5041c5d 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -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 diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb index 836f2430890..04a7698e323 100644 --- a/app/graphql/types/ci/stage_type.rb +++ b/app/graphql/types/ci/stage_type.rb @@ -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 diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index 750bd1bfe8d..2b10edce108 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -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) diff --git a/app/models/application_record.rb b/app/models/application_record.rb index c9ae185583d..1bbace791ed 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -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') diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 4ba09fd8152..47b91fcf2ce 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -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 diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index c49d088fe6b..e2103183247 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -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 diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index 87c0933747d..4b41231ba20 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -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} diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml index 8e4e5ca93e0..ded43a34b48 100644 --- a/app/views/projects/_customize_workflow.html.haml +++ b/app/views/projects/_customize_workflow.html.haml @@ -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" diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml index 59c9c279a39..9888ce417f8 100644 --- a/app/views/projects/_fork_suggestion.html.haml +++ b/app/views/projects/_fork_suggestion.html.haml @@ -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 diff --git a/app/views/projects/_invite_members.html.haml b/app/views/projects/_invite_members.html.haml index ef030cabc93..e3a512d6451 100644 --- a/app/views/projects/_invite_members.html.haml +++ b/app/views/projects/_invite_members.html.haml @@ -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' } diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 8b1bf37ff10..f6adb213916 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -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: "" } diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index da3133dfe15..85a53edc160 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -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' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 12144f908dd..ecaf3467cd2 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -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 diff --git a/app/views/projects/settings/operations/_tracing.html.haml b/app/views/projects/settings/operations/_tracing.html.haml index 8bc9a383a73..218097cd163 100644 --- a/app/views/projects/settings/operations/_tracing.html.haml +++ b/app/views/projects/settings/operations/_tracing.html.haml @@ -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' diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index a505b34f46c..f53b2051835 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -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 diff --git a/app/views/projects/tracings/show.html.haml b/app/views/projects/tracings/show.html.haml index 8c9bffc81bf..21c1d02d92e 100644 --- a/app/views/projects/tracings/show.html.haml +++ b/app/views/projects/tracings/show.html.haml @@ -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' diff --git a/changelogs/unreleased/ajk-graphql-ci-jobs.yml b/changelogs/unreleased/ajk-graphql-ci-jobs.yml new file mode 100644 index 00000000000..396e06ae0ed --- /dev/null +++ b/changelogs/unreleased/ajk-graphql-ci-jobs.yml @@ -0,0 +1,5 @@ +--- +title: Adds CI pipeline and job features to GraphQL API +merge_request: 44703 +author: +type: changed diff --git a/changelogs/unreleased/btn-confirm-projects-snippets.yml b/changelogs/unreleased/btn-confirm-projects-snippets.yml new file mode 100644 index 00000000000..1766609abc5 --- /dev/null +++ b/changelogs/unreleased/btn-confirm-projects-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in projects/snippets directory +merge_request: 56939 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/btn-confirm-projects.yml b/changelogs/unreleased/btn-confirm-projects.yml new file mode 100644 index 00000000000..983f72531be --- /dev/null +++ b/changelogs/unreleased/btn-confirm-projects.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in projects directory +merge_request: 56943 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/cngo-add-gl-toggle-labels.yml b/changelogs/unreleased/cngo-add-gl-toggle-labels.yml new file mode 100644 index 00000000000..d553890901a --- /dev/null +++ b/changelogs/unreleased/cngo-add-gl-toggle-labels.yml @@ -0,0 +1,5 @@ +--- +title: Add labels to UI toggles +merge_request: 56848 +author: +type: fixed diff --git a/changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml b/changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml new file mode 100644 index 00000000000..cf891e28859 --- /dev/null +++ b/changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml @@ -0,0 +1,5 @@ +--- +title: Refactor docs and UI for Jaeger tracing +merge_request: 56819 +author: +type: other diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 3f8aca2f1ff..661fa67220d 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -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: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e54d5d80d3c..96cea368b04 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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. diff --git a/doc/operations/tracing.md b/doc/operations/tracing.md index bf9e0d2390e..a6647641527 100644 --- a/doc/operations/tracing.md +++ b/doc/operations/tracing.md @@ -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. +[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/). -### Deploying Jaeger +See also: -To learn more about deploying 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). +- 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). -### Enabling Jaeger +## 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. diff --git a/lib/api/helpers/graphql_helpers.rb b/lib/api/helpers/graphql_helpers.rb index 3ddef0c16b3..4f7f85bd69d 100644 --- a/lib/api/helpers/graphql_helpers.rb +++ b/lib/api/helpers/graphql_helpers.rb @@ -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) diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb index 945ab7f40c2..6b33b60e850 100644 --- a/lib/gitlab/issuables_count_for_state.rb +++ b/lib/gitlab/issuables_count_for_state.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 71f859ec822..4e4e648e61a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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." diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 886be520668..b06d581d2c0 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -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 diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb index f9952cd9966..b276e6f8cfc 100644 --- a/spec/factories/sequences.rb +++ b/spec/factories/sequences.rb @@ -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 diff --git a/spec/frontend/__mocks__/vue/index.js b/spec/frontend/__mocks__/vue/index.js new file mode 100644 index 00000000000..52a5c6c5fcd --- /dev/null +++ b/spec/frontend/__mocks__/vue/index.js @@ -0,0 +1,7 @@ +import Vue from 'vue'; + +Vue.config.productionTip = false; +Vue.config.devtools = false; + +export default Vue; +export * from 'vue'; diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js index 2e253d24125..635964b6b4a 100644 --- a/spec/frontend/boards/components/issue_time_estimate_spec.js +++ b/spec/frontend/boards/components/issue_time_estimate_spec.js @@ -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(); diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js index a05e23a4250..00d557c11cf 100644 --- a/spec/frontend/feature_flags/components/form_spec.js +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -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(() => { diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js index 2433c50ff24..859d3587223 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js @@ -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', () => { diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 0934dde8230..878721666ff 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -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', () => { diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js index 305dc557b39..40cfd785a20 100644 --- a/spec/frontend/pipelines/nav_controls_spec.js +++ b/spec/frontend/pipelines/nav_controls_spec.js @@ -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'); }); }); }); diff --git a/spec/graphql/types/ci/job_status_enum_spec.rb b/spec/graphql/types/ci/job_status_enum_spec.rb new file mode 100644 index 00000000000..e8a1a2e0aa8 --- /dev/null +++ b/spec/graphql/types/ci/job_status_enum_spec.rb @@ -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 diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index c54137a1c3e..4654b55eea5 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -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) diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index e0e84a1b635..13021c00aec 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -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 ] diff --git a/spec/graphql/types/ci/stage_type_spec.rb b/spec/graphql/types/ci/stage_type_spec.rb index 9a8d4fa96a3..cb8c1cb02cd 100644 --- a/spec/graphql/types/ci/stage_type_spec.rb +++ b/spec/graphql/types/ci/stage_type_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do name groups detailedStatus + jobs ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index 107f707ccd9..7e6ac351e68 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -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 diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 0afc491dc73..677e4b34ecd 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -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') } diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb index 9e81358a152..4c063d359a5 100644 --- a/spec/requests/api/graphql/ci/groups_spec.rb +++ b/spec/requests/api/graphql/ci/groups_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb new file mode 100644 index 00000000000..78f7d3e149b --- /dev/null +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index cc028ff2ff9..6436fe1e9ef 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -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!(:query) do - %( - query { - project(fullPath: "#{project.full_path}") { - pipeline(iid: "#{pipeline.iid}") { - configSource - } - } - } + 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(: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 diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb index 683ee3e4bf2..6e145fed733 100644 --- a/spec/support/helpers/board_helpers.rb +++ b/spec/support/helpers/board_helpers.rb @@ -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 diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index 565c21e0f85..904b7efdd7f 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -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 |