Commit 5fbe7b8b authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'docs-bprescott-20200722-rcons' into 'master'

Create dedicated page for rails console

See merge request gitlab-org/gitlab!37647
parents 7fc20187 9be8d605
......@@ -34,6 +34,7 @@
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- tmp/feature_flags/
- log/*.log
reports:
junit: junit_rspec.xml
......@@ -360,6 +361,7 @@ rspec:coverage:
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
- run_timed_command "bundle exec scripts/merge-simplecov"
- run_timed_command "bundle exec scripts/gather-test-memory-data"
- run_timed_command "bundle exec scripts/used-feature-flags"
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
artifacts:
name: coverage
......
......@@ -362,6 +362,15 @@ Graphql/AuthorizeTypes:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Graphql/GIDExpectedType:
Enabled: true
Include:
- 'app/graphql/**/*'
- 'ee/app/graphql/**/*'
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Graphql/JSONType:
Enabled: true
Include:
......
......@@ -116,6 +116,7 @@ export default {
:placeholder="placeholder"
:value="text"
class="note-textarea ide-commit-message-textarea"
data-qa-selector="ide_commit_message_field"
dir="auto"
name="commit-message"
@scroll="handleScroll"
......
import createFlash from '~/flash';
import { BLOB_EDITOR_ERROR } from '~/blob_edit/constants';
import EditorLite from '~/editor/editor_lite';
export default class CILintEditor {
constructor() {
const monacoEnabled = window?.gon?.features?.monacoCi;
this.clearYml = document.querySelector('.clear-yml');
this.clearYml.addEventListener('click', this.clear.bind(this));
return monacoEnabled ? this.initEditorLite() : this.initAce();
return this.initEditorLite();
}
clear() {
......@@ -15,34 +13,20 @@ export default class CILintEditor {
}
initEditorLite() {
import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite')
.then(({ default: EditorLite }) => {
const editorEl = document.getElementById('editor');
const fileContentEl = document.getElementById('content');
const form = document.querySelector('.js-ci-lint-form');
const editorEl = document.getElementById('editor');
const fileContentEl = document.getElementById('content');
const form = document.querySelector('.js-ci-lint-form');
const rootEditor = new EditorLite();
const rootEditor = new EditorLite();
this.editor = rootEditor.createInstance({
el: editorEl,
blobPath: '.gitlab-ci.yml',
blobContent: editorEl.innerText,
});
form.addEventListener('submit', () => {
fileContentEl.value = this.editor.getValue();
});
})
.catch(() => createFlash({ message: BLOB_EDITOR_ERROR }));
}
initAce() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.getElementById('content');
this.editor = rootEditor.createInstance({
el: editorEl,
blobPath: '.gitlab-ci.yml',
blobContent: editorEl.innerText,
});
this.editor.getSession().setMode('ace/mode/yaml');
this.editor.on('input', () => {
this.textarea.value = this.editor.getSession().getValue();
form.addEventListener('submit', () => {
fileContentEl.value = this.editor.getValue();
});
}
}
......@@ -64,10 +64,18 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
// Start point
path.moveTo(sourceNodeX, sourceNodeY);
// Make cross-stages lines a straight line all the way
// until we can safely draw the bezier to look nice.
const straightLineDestinationX = targetNodeX - 100;
const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2;
if (straightLineDestinationX > 0) {
path.lineTo(straightLineDestinationX, sourceNodeY);
}
// Add bezier curve. The first 4 coordinates are the 2 control
// points to create the curve, and the last one is the end point (x, y).
// We want our control points to be in the middle of the line
const controlPointX = sourceNodeX + (targetNodeX - sourceNodeX) / 2;
path.bezierCurveTo(
controlPointX,
sourceNodeY,
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import { n__, __ } from '~/locale';
export default {
name: 'AssigneeTitle',
......@@ -26,12 +26,19 @@ export default {
required: false,
default: false,
},
changing: {
type: Boolean,
required: true,
},
},
computed: {
assigneeTitle() {
const assignees = this.numberOfAssignees;
return n__('Assignee', `%d Assignees`, assignees);
},
titleCopy() {
return this.changing ? __('Apply') : __('Edit');
},
},
};
</script>
......@@ -43,11 +50,12 @@ export default {
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
data-test-id="edit-link"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
>
{{ __('Edit') }}
{{ titleCopy }}
</a>
<a
v-if="showToggle"
......
......@@ -89,6 +89,8 @@ export default {
.saveAssignees(this.field)
.then(() => {
this.loading = false;
this.store.resetChanging();
refreshUserMergeRequestCounts();
})
.catch(() => {
......@@ -113,6 +115,7 @@ export default {
:loading="loading || store.isFetching.assignees"
:editable="store.editable"
:show-toggle="!signedIn"
:changing="store.changing"
/>
<assignees
v-if="!store.isFetching.assignees"
......
<script>
import { mapGetters } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
......@@ -26,7 +25,7 @@ export default {
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
......@@ -79,13 +78,9 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div
v-tooltip
:title="tooltipLabel"
v-gl-tooltip.left.viewport="{ title: tooltipLabel }"
class="sidebar-collapsed-icon"
data-testid="sidebar-collapse-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
@click="toggleForm"
>
<gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
......
......@@ -33,6 +33,7 @@ export default class SidebarStore {
this.projectEmailsDisabled = false;
this.subscribeDisabledDescription = '';
this.subscribed = null;
this.changing = false;
SidebarStore.singleton = this;
}
......@@ -51,6 +52,10 @@ export default class SidebarStore {
}
}
resetChanging() {
this.changing = false;
}
setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent;
......@@ -80,6 +85,7 @@ export default class SidebarStore {
addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.changing = true;
this.assignees.push(assignee);
}
}
......@@ -100,6 +106,7 @@ export default class SidebarStore {
removeAssignee(assignee) {
if (assignee) {
this.changing = true;
this.assignees = this.assignees.filter(({ id }) => id !== assignee.id);
}
}
......@@ -111,6 +118,7 @@ export default class SidebarStore {
}
removeAllAssignees() {
this.changing = true;
this.assignees = [];
}
......
......@@ -471,12 +471,6 @@ $mr-widget-min-height: 69px;
flex: 1;
}
.issuable-meta {
.author-link {
display: inline-block;
}
}
.merge-request-title {
margin-bottom: 2px;
......
......@@ -28,7 +28,10 @@
height: 14px;
width: 14px;
vertical-align: middle;
fill: $gl-text-color-secondary;
&:not(.text-warning) {
fill: $gl-text-color-secondary;
}
}
.sprite {
......
......@@ -11,8 +11,8 @@ class Dashboard::LabelsController < Dashboard::ApplicationController
def labels
finder_params = { project_ids: projects.select(:id) }
labels = LabelsFinder.new(current_user, finder_params).execute
GlobalLabel.build_collection(labels)
LabelsFinder.new(current_user, finder_params).execute
.select('DISTINCT ON (labels.title) labels.*')
end
end
......@@ -25,7 +25,7 @@ module Ci
attr_reader :current_user, :pipeline, :project, :params, :type
def init_collection
if Feature.enabled?(:ci_jobs_finder_refactor)
if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true)
pipeline_jobs || project_jobs || all_jobs
else
project ? project_builds : all_jobs
......@@ -59,7 +59,7 @@ module Ci
end
def filter_by_scope(builds)
if Feature.enabled?(:ci_jobs_finder_refactor)
if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true)
return filter_by_statuses!(params[:scope], builds) if params[:scope].is_a?(Array)
end
......
......@@ -102,7 +102,7 @@ class IssuableFinder
items = filter_items(items)
# Let's see if we have to negate anything
items = filter_negated_items(items)
items = filter_negated_items(items) if should_filter_negated_args?
# This has to be last as we use a CTE as an optimization fence
# for counts by passing the force_cte param and passing the
......@@ -134,13 +134,15 @@ class IssuableFinder
by_my_reaction_emoji(items)
end
# Negates all params found in `negatable_params`
def filter_negated_items(items)
return items unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
def should_filter_negated_args?
return false unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
# API endpoints send in `nil` values so we test if there are any non-nil
return items unless not_params.present? && not_params.values.any?
not_params.present? && not_params.values.any?
end
# Negates all params found in `negatable_params`
def filter_negated_items(items)
items = by_negated_author(items)
items = by_negated_assignee(items)
items = by_negated_label(items)
......
# frozen_string_literal: true
module Mutations
module Issues
module CommonMutationArguments
extend ActiveSupport::Concern
included do
argument :description, GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :description)
argument :due_date, GraphQL::Types::ISO8601Date,
required: false,
description: copy_field_description(Types::IssueType, :due_date)
argument :confidential, GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :confidential)
argument :locked, GraphQL::BOOLEAN_TYPE,
as: :discussion_locked,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
end
end
end
end
Mutations::Issues::CommonMutationArguments.prepend_if_ee('::EE::Mutations::Issues::CommonMutationArguments')
# frozen_string_literal: true
module Mutations
module Issues
class Create < BaseMutation
include ResolvesProject
graphql_name 'CreateIssue'
authorize :create_issue
include CommonMutationArguments
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Project full path the issue is associated with'
argument :iid, GraphQL::INT_TYPE,
required: false,
description: 'The IID (internal ID) of a project issue. Only admins and project owners can modify'
argument :title, GraphQL::STRING_TYPE,
required: true,
description: copy_field_description(Types::IssueType, :title)
argument :milestone_id, ::Types::GlobalIDType[::Milestone],
required: false,
description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null'
argument :labels, [GraphQL::STRING_TYPE],
required: false,
description: copy_field_description(Types::IssueType, :labels)
argument :label_ids, [::Types::GlobalIDType[::Label]],
required: false,
description: 'The IDs of labels to be added to the issue'
argument :created_at, Types::TimeType,
required: false,
description: 'Timestamp when the issue was created. Available only for admins and project owners'
argument :merge_request_to_resolve_discussions_of, ::Types::GlobalIDType[::MergeRequest],
required: false,
description: 'The IID of a merge request for which to resolve discussions'
argument :discussion_to_resolve, GraphQL::STRING_TYPE,
required: false,
description: 'The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`'
argument :assignee_ids, [::Types::GlobalIDType[::User]],
required: false,
description: 'The array of user IDs to assign to the issue'
field :issue,
Types::IssueType,
null: true,
description: 'The issue after mutation'
def ready?(**args)
if args.slice(*mutually_exclusive_label_args).size > 1
arg_str = mutually_exclusive_label_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required."
end
if args[:discussion_to_resolve].present? && args[:merge_request_to_resolve_discussions_of].blank?
raise Gitlab::Graphql::Errors::ArgumentError,
'to resolve a discussion please also provide `merge_request_to_resolve_discussions_of` parameter'
end
super
end
def resolve(project_path:, **attributes)
project = authorized_find!(full_path: project_path)
params = build_create_issue_params(attributes.merge(author_id: current_user.id))
issue = ::Issues::CreateService.new(project, current_user, params).execute
if issue.spam?
issue.errors.add(:base, 'Spam detected.')
end
{
issue: issue.valid? ? issue : nil,
errors: errors_on_object(issue)
}
end
private
def build_create_issue_params(params)
params[:milestone_id] &&= params[:milestone_id]&.model_id
params[:assignee_ids] &&= params[:assignee_ids].map { |assignee_id| assignee_id&.model_id }
params[:label_ids] &&= params[:label_ids].map { |label_id| label_id&.model_id }
params
end
def mutually_exclusive_label_args
[:labels, :label_ids]
end
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
Mutations::Issues::Create.prepend_if_ee('::EE::Mutations::Issues::Create')
......@@ -5,49 +5,26 @@ module Mutations
class Update < Base
graphql_name 'UpdateIssue'
argument :title,
GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :title)
include CommonMutationArguments
argument :description,
GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :description)
argument :due_date,
Types::TimeType,
required: false,
description: copy_field_description(Types::IssueType, :due_date)
argument :confidential,
GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :confidential)
argument :locked,
GraphQL::BOOLEAN_TYPE,
as: :discussion_locked,
argument :title, GraphQL::STRING_TYPE,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
description: copy_field_description(Types::IssueType, :title)
argument :add_label_ids,
[GraphQL::ID_TYPE],
argument :milestone_id, GraphQL::ID_TYPE,
required: false,
description: 'The IDs of labels to be added to the issue.'
description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null'
argument :remove_label_ids,
[GraphQL::ID_TYPE],
argument :add_label_ids, [GraphQL::ID_TYPE],
required: false,
description: 'The IDs of labels to be removed from the issue.'
description: 'The IDs of labels to be added to the issue'
argument :milestone_id,
GraphQL::ID_TYPE,
argument :remove_label_ids, [GraphQL::ID_TYPE],
required: false,
description: 'The ID of the milestone to be assigned, milestone will be removed if set to null.'
description: 'The IDs of labels to be removed from the issue'
argument :state_event, Types::IssueStateEventEnum,
description: 'Close or reopen an issue.',
description: 'Close or reopen an issue',
required: false
def resolve(project_path:, iid:, **args)
......
......@@ -17,7 +17,7 @@ module Mutations
discussion_id = nil
if args[:discussion_id]
discussion = GitlabSchema.object_from_id(args[:discussion_id])
discussion = GitlabSchema.object_from_id(args[:discussion_id], expected_type: ::Discussion)
authorize_discussion!(discussion)
discussion_id = discussion.id
......
......@@ -30,7 +30,7 @@ module Types
# most recent `Version` for an issue
Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do
if version_gid
GitlabSchema.object_from_id(version_gid)&.sync
GitlabSchema.object_from_id(version_gid, expected_type: ::DesignManagement::Version)&.sync
else
object.issue.design_versions.most_recent
end
......
......@@ -23,6 +23,7 @@ module Types
mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::Discussions::ToggleResolve
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked
......
......@@ -61,4 +61,8 @@ class ApplicationRecord < ActiveRecord::Base
def self.underscore
Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
end
def self.where_exists(query)
where('EXISTS (?)', query.select(1))
end
end
......@@ -136,6 +136,10 @@ module Ci
# We are using optimistic locking combined with Redis locking to ensure
# that a chunk gets migrated properly.
#
# We are catching an exception related to an exclusive lock not being
# acquired because it is creating a lot of noise, and is a result of
# duplicated workers running in parallel for the same build trace chunk.
#
def persist_data!
in_lock(*lock_params) do # exclusive Redis lock is acquired first
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
......@@ -144,6 +148,8 @@ module Ci
chunk.unsafe_persist_data! # we migrate the data and update data store
end
end
rescue FailedToObtainLockError
metrics.increment_trace_operation(operation: :stalled)
rescue ActiveRecord::StaleObjectError
raise FailedToPersistDataError, <<~MSG
Data migration race condition detected
......
# frozen_string_literal: true
class GlobalLabel
include Presentable
attr_accessor :title, :labels
alias_attribute :name, :title
delegate :color, :text_color, :description, :scoped_label?, to: :@first_label
def for_display
@first_label
end
def self.build_collection(labels)
labels = labels.group_by(&:title)
labels.map do |title, labels|
new(title, labels)
end
end
def initialize(title, labels)
@title = title
@labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
def present(attributes)
super(attributes.merge(presenter_class: ::LabelPresenter))
end
end
......@@ -106,14 +106,26 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
add_special_file_path(file_name: 'LICENSE')
end
def add_license_ide_path
ide_edit_path(project, default_branch_or_master, 'LICENSE')
end
def add_changelog_path
add_special_file_path(file_name: 'CHANGELOG')
end
def add_changelog_ide_path
ide_edit_path(project, default_branch_or_master, 'CHANGELOG')
end
def add_contribution_guide_path
add_special_file_path(file_name: 'CONTRIBUTING.md', commit_message: 'Add CONTRIBUTING')
end
def add_contribution_guide_ide_path
ide_edit_path(project, default_branch_or_master, 'CONTRIBUTING.md')
end
def add_ci_yml_path
add_special_file_path(file_name: ci_config_path_or_default)
end
......@@ -122,6 +134,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
add_special_file_path(file_name: 'README.md')
end
def add_readme_ide_path
ide_edit_path(project, default_branch_or_master, 'README.md')
end
def license_short_name
license = repository.license
license&.nickname || license&.name || 'LICENSE'
......@@ -218,9 +234,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def new_file_anchor_data
if current_user && can_current_user_push_to_default_branch?
new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_master) : project_new_blob_path(project, default_branch_or_master)
AnchorData.new(false,
statistic_icon + _('New file'),
project_new_blob_path(project, default_branch_or_master),
new_file_path,
'missing')
end
end
......@@ -229,7 +247,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch? && repository.readme.nil?
AnchorData.new(false,
statistic_icon + _('Add README'),
add_readme_path)
empty_repo? ? add_readme_ide_path : add_readme_path)
elsif repository.readme
AnchorData.new(false,
statistic_icon('doc-text') + _('README'),
......@@ -243,7 +261,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
AnchorData.new(false,
statistic_icon + _('Add CHANGELOG'),
add_changelog_path)
empty_repo? ? add_changelog_ide_path : add_changelog_path)
elsif repository.changelog.present?
AnchorData.new(false,
statistic_icon('doc-text') + _('CHANGELOG'),
......@@ -264,7 +282,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false,
content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'),
add_license_path)
empty_repo? ? add_license_ide_path : add_license_path)
else
AnchorData.new(false,
icon + content_tag(:span, _('No license. All rights reserved'), class: 'project-stat-value'),
......@@ -277,7 +295,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
AnchorData.new(false,
statistic_icon + _('Add CONTRIBUTING'),
add_contribution_guide_path)
empty_repo? ? add_contribution_guide_ide_path : add_contribution_guide_path)
elsif repository.contribution_guide.present?
AnchorData.new(false,
statistic_icon('doc-text') + _('CONTRIBUTING'),
......
# frozen_string_literal: true
class LabelEntity < Grape::Entity
expose :id, if: ->(label, _) { !label.is_a?(GlobalLabel) }
expose :id
expose :title
expose :color
expose :description
expose :group_id
expose :project_id, if: ->(label, _) { !label.is_a?(GlobalLabel) }
expose :project_id
expose :template
expose :text_color
expose :created_at
......
......@@ -34,6 +34,18 @@ module Issues
private
def filter_params(merge_request)
super
moved_issue = params.delete(:moved_issue)
# Setting created_at, updated_at and iid is allowed only for admins and owners or
# when moving an issue as we preserve the original issue attributes except id and iid.
params.delete(:iid) unless current_user.can?(:set_issue_iid, project)
params.delete(:created_at) unless moved_issue || current_user.can?(:set_issue_created_at, project)
params.delete(:updated_at) unless moved_issue || current_user.can?(:set_issue_updated_at, project)
end
def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)
......
......@@ -52,7 +52,8 @@ module Issues
iid: nil,
project: target_project,
author: original_entity.author,
assignee_ids: original_entity.assignee_ids
assignee_ids: original_entity.assignee_ids,
moved_issue: true
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
......
......@@ -22,7 +22,7 @@
- if ref
- if job.ref
.icon-container.gl-display-inline-block
= job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
= job.tag? ? sprite_icon('label', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
= link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else
.light= _('none')
......@@ -33,10 +33,12 @@
= link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0"
- if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.'))
%span.has-tooltip{ title: _('Job is stuck. Check runners.') }
= sprite_icon('warning', css_class: 'text-warning!')
- if retried
= icon('refresh', class: 'text-warning has-tooltip', title: _('Job was retried'))
%span.has-tooltip{ title: _('Job was retried') }
= sprite_icon('retry', css_class: 'text-warning')
.label-container
- if job.tags.any?
......@@ -87,7 +89,7 @@
- if job.finished_at
%p.finished-at
= icon("calendar")
= sprite_icon("calendar")
%span= time_ago_with_tooltip(job.finished_at)
%td.coverage
......
- page_title _("CI Lint")
- page_description _("Validate your GitLab CI configuration file")
- unless Feature.enabled?(:monaco_ci, default_enabled: true)
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
%h2.pt-3.pb-3= _("Validate your GitLab CI configuration")
......@@ -17,12 +14,9 @@
.file-holder
.js-file-title.file-title.clearfix
= _("Contents of .gitlab-ci.yml")
- if Feature.enabled?(:monaco_ci, default_enabled: true)
.file-editor.code
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
%pre.editor-loading-content= params[:content]
- else
#ci-editor.ci-editor= @content
.file-editor.code
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
%pre.editor-loading-content= params[:content]
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
.float-left.gl-mt-3
......
......@@ -53,7 +53,7 @@
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= sprite_icon('warning-solid')
- if merge_request.assignees.any?
%li.gl-display-flex
%li.gl-display-flex.gl-align-items-center
= render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
- if Feature.enabled?(:merge_request_reviewers, @project) && merge_request.reviewers.any?
%li.gl-display-flex.issuable-reviewers
......
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, currentRoutePath: current_route_path })
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path })
- breadcrumb_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
......
---
title: Add GraphQL mutation to create an issue
merge_request: 43735
author:
type: added
---
title: Align badge with avatar in MR List
merge_request: 44671
author:
type: fixed
---
title: Use Web IDE to create new files in empty repos
merge_request: 44287
author:
type: added
---
title: Replace fa icons in CI build table
merge_request: 45123
author:
type: changed
---
title: Fixed incorrect parameter in GraphQL startup call
merge_request: 45115
author:
type: fixed
---
title: Add apply button when user changes assignees
merge_request: 44812
author:
type: added
---
name: approval_rule
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true
......@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36622
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/245183
group: group::continuous integration
type: development
default_enabled: false
default_enabled: true
---
name: monaco_ci
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23666
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/249137
group: group::editor
type: development
default_enabled: true
---
name: release_asset_link_editing
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26821
rollout_issue_url:
group: group::release management
type: development
default_enabled: true
---
name: release_show_page
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23792
rollout_issue_url:
group: group::release management
type: development
default_enabled: true
---
name: sql-set-operators
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786
group: group::access
type: development
default_enabled: false
---
name: sql_set_operators
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786#f99799ae4964b7650b877e081b669379d71bcca8
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786
rollout_issue_url:
group: group::access
type: development
default_enabled: false
......@@ -21,4 +21,4 @@ Gitlab::Marginalia.set_application_name
Gitlab::Marginalia.enable_sidekiq_instrumentation
Gitlab::Marginalia.set_feature_cache
Gitlab::Marginalia.set_enabled_from_feature_flag
......@@ -2,7 +2,7 @@
description: 'Learn how to install, configure, update, and maintain your GitLab instance.'
---
# Administrator Docs **(CORE ONLY)**
# Administrator documentation **(CORE ONLY)**
Learn how to administer your self-managed GitLab instance.
......@@ -12,18 +12,16 @@ GitLab has two product distributions available through [different subscriptions]
- The open core [GitLab Enterprise Edition (EE)](https://gitlab.com/gitlab-org/gitlab).
You can [install either GitLab CE or GitLab EE](https://about.gitlab.com/install/ce-or-ee/).
However, the features you'll have access to depend on the subscription you choose
(Core, Starter, Premium, or Ultimate).
However, the features you have access to depend on your chosen [subscription](https://about.gitlab.com/pricing/).
NOTE: **Note:**
GitLab Community Edition installations only have access to Core features.
GitLab Community Edition installations have access only to Core features.
GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have
access to its admin configurations. If you're a GitLab.com user, please check the
[user documentation](../user/index.md).
Non-administrator users can't access GitLab administration tools and settings.
NOTE: **Note:**
Non-administrator users don’t have access to GitLab administration tools and settings.
GitLab.com is administered by GitLab, Inc., and only GitLab team members have
access to its administration tools and settings. Users of GitLab.com should
instead refer to the [User documentation](../user/index.md) for GitLab
configuration and usage documentation.
## Installing and maintaining GitLab
......
......@@ -148,6 +148,35 @@ ote_pid | tls
(1 row)
```
## Procedure for bypassing PgBouncer
Some database changes have to be done directly, and not through PgBouncer. This includes database restores and GitLab upgrades (because of the database migrations).
1. To find the primary node, run the following on a database node:
```shell
sudo gitlab-ctl repmgr cluster show
```
1. Edit `/etc/gitlab/gitlab.rb` on the application node you're performing the task on, and update
`gitlab_rails['db_host']` and `gitlab_rails['db_port']` with the database
primary's host and port.
1. Run reconfigure:
```shell
sudo gitlab-ctl reconfigure
```
Once you've performed the tasks or procedure, switch back to using PgBouncer:
1. Change back `/etc/gitlab/gitlab.rb` to point to PgBouncer.
1. Run reconfigure:
```shell
sudo gitlab-ctl reconfigure
```
## Troubleshooting
In case you are experiencing any issues connecting through PgBouncer, the first
......
......@@ -1445,7 +1445,7 @@ Considering these, you should carefully plan your PostgreSQL upgrade:
sudo gitlab-ctl pg-upgrade -V 12
```
CAUTION: **Warning:**
NOTE: **Note:**
Reverting PostgreSQL upgrade with `gitlab-ctl revert-pg-upgrade` has the same considerations as
`gitlab-ctl pg-upgrade`. It can be complicated and may involve deletion of the data directory.
If you need to do that, please contact GitLab support.
`gitlab-ctl pg-upgrade`. You should follow the same procedure by first stopping the replicas,
then reverting the leader, and finally reverting the replicas.
......@@ -64,18 +64,17 @@ To set up GitLab and its components to accommodate up to 2,000 users:
## Configure the external load balancer
NOTE: **Note:**
This architecture has been tested and validated with [HAProxy](https://www.haproxy.org/).
Although you can use a load balancer with a similar set of features, GitLab
hasn't validated other load balancers.
In an active/active GitLab configuration, you'll need a load balancer to route
traffic to the application servers. The specifics for which load balancer to
use or its exact configuration is out of scope for the GitLab documentation.
If you're managing multi-node systems (including GitLab) you'll probably
already have a load balancer of choice. Some examples including HAProxy
(open-source), F5 Big-IP LTM, and Citrix Net Scaler. This documentation
includes the ports and protocols for use with GitLab.
traffic to the application servers. The specifics on which load balancer to use
or its exact configuration is beyond the scope of GitLab documentation. We hope
that if you're managing multi-node systems like GitLab, you already have a load
balancer of choice. Some load balancer examples include HAProxy (open-source),
F5 Big-IP LTM, and Citrix Net Scaler. This documentation outline the ports and
protocols needed for use with GitLab.
This architecture has been tested and validated with [HAProxy](https://www.haproxy.org/)
as the load balancer. Although other load balancers with similar feature sets
could also be used, those load balancers have not been validated.
The next question is how you will handle SSL in your environment. There are
several different options:
......@@ -489,11 +488,10 @@ Name. If you are addressing the Gitaly server by its IP address, you must add it
as a Subject Alternative Name to the certificate.
[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691).
NOTE: **Note:**
It is possible to configure Gitaly servers with both an
unencrypted listening address `listen_addr` and an encrypted listening
address `tls_listen_addr` at the same time. This allows you to do a
gradual transition from unencrypted to encrypted traffic, if necessary.
It's possible to configure Gitaly servers with both an unencrypted listening
address (`listen_addr`) and an encrypted listening address (`tls_listen_addr`)
at the same time. This allows you to do a gradual transition from unencrypted to
encrypted traffic, if necessary.
To configure Gitaly with TLS:
......@@ -537,14 +535,14 @@ To configure Gitaly with TLS:
## Configure GitLab Rails
NOTE: **Note:**
In our architectures we run each GitLab Rails node using the Puma webserver
and have its number of workers set to 90% of available CPUs along with four threads. For
nodes that are running Rails with other components the worker value should be reduced
accordingly where we've found 50% achieves a good balance but this is dependent
on workload.
This section describes how to configure the GitLab application (Rails) component.
In our architecture, we run each GitLab Rails node using the Puma webserver, and
have its number of workers set to 90% of available CPUs, with four threads. For
nodes running Rails with other components, the worker value should be reduced
accordingly. We've determined that a worker value of 50% achieves a good balance,
but this is dependent on workload.
On each node perform the following:
1. If you're [using NFS](#configure-nfs-optional):
......@@ -572,10 +570,10 @@ On each node perform the following:
mkdir -p /var/opt/gitlab/.ssh /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/git-data
```
1. Download/install Omnibus GitLab using **steps 1 and 2** from
1. Download and install Omnibus GitLab using **steps 1 and 2** from
[GitLab downloads](https://about.gitlab.com/install/). Do not complete other
steps on the download page.
1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration.
To maintain uniformity of links across nodes, the `external_url`
on the application server should point to the external URL that users will use
to access GitLab. This would be the URL of the [load balancer](#configure-the-external-load-balancer)
......@@ -671,12 +669,10 @@ On each node perform the following:
[Gitaly node](#configure-gitaly) and
[reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
NOTE: **Note:**
When you specify `https` in the `external_url`, as in the example
above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
certificates are not present, NGINX will fail to start. See the
[NGINX documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
for more information.
When you specify `https` in the `external_url`, as in the previous example,
GitLab expects that the SSL certificates are in `/etc/gitlab/ssl/`. If the
certificates aren't present, NGINX will fail to start. For more information, see
the [NGINX documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https).
### GitLab Rails post-configuration
......@@ -688,12 +684,11 @@ for more information.
sudo gitlab-rake gitlab:db:configure
```
NOTE: **Note:**
If you encounter a `rake aborted!` error stating that PgBouncer is failing to connect to
PostgreSQL it may be that your PgBouncer node's IP address is missing from
PostgreSQL's `trust_auth_cidr_addresses` in `gitlab.rb` on your database nodes. See
[PgBouncer error `ERROR: pgbouncer cannot connect to server`](troubleshooting.md#pgbouncer-error-error-pgbouncer-cannot-connect-to-server)
in the Troubleshooting section before proceeding.
If you encounter a `rake aborted!` error message stating that PgBouncer is
failing to connect to PostgreSQL, it may be that your PgBouncer node's IP
address is missing from PostgreSQL's `trust_auth_cidr_addresses` in `gitlab.rb`
on your database nodes. Before proceeding, see
[PgBouncer error `ERROR: pgbouncer cannot connect to server`](troubleshooting.md#pgbouncer-error-error-pgbouncer-cannot-connect-to-server).
1. [Configure fast lookup of authorized SSH keys in the database](../operations/fast_ssh_key_lookup.md).
......@@ -893,16 +888,13 @@ functioning backups is encountered.
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
You can leverage Elasticsearch and [enable Advanced Search](../../integration/elasticsearch.md)
for faster, more advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
Elasticsearch cluster design and requirements are dependent on your specific
data. For recommended best practices about how to set up your Elasticsearch
cluster alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
......
......@@ -3422,6 +3422,111 @@ type CreateImageDiffNotePayload {
note: Note
}
"""
Autogenerated input type of CreateIssue
"""
input CreateIssueInput {
"""
The array of user IDs to assign to the issue
"""
assigneeIds: [UserID!]
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Indicates the issue is confidential
"""
confidential: Boolean
"""
Timestamp when the issue was created. Available only for admins and project owners
"""
createdAt: Time
"""
Description of the issue
"""
description: String
"""
The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`
"""
discussionToResolve: String
"""
Due date of the issue
"""
dueDate: ISO8601Date
"""
The ID of an epic to associate the issue with
"""
epicId: EpicID
"""
The IID (internal ID) of a project issue. Only admins and project owners can modify
"""
iid: Int
"""
The IDs of labels to be added to the issue
"""
labelIds: [LabelID!]
"""
Labels of the issue
"""
labels: [String!]
"""
Indicates discussion is locked on the issue
"""
locked: Boolean
"""
The IID of a merge request for which to resolve discussions
"""
mergeRequestToResolveDiscussionsOf: MergeRequestID
"""
The ID of the milestone to assign to the issue. On update milestone will be removed if set to null
"""
milestoneId: MilestoneID
"""
Project full path the issue is associated with
"""
projectPath: ID!
"""
Title of the issue
"""
title: String!
}
"""
Autogenerated return type of CreateIssue
"""
type CreateIssuePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
"""
Autogenerated input type of CreateIteration
"""
......@@ -11188,6 +11293,11 @@ type MergeRequestEdge {
node: MergeRequest
}
"""
Identifier of MergeRequest
"""
scalar MergeRequestID
"""
Check permissions for the current user on a merge request
"""
......@@ -11964,6 +12074,7 @@ type Mutation {
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
createIssue(input: CreateIssueInput!): CreateIssuePayload
createIteration(input: CreateIterationInput!): CreateIterationPayload
createNote(input: CreateNoteInput!): CreateNotePayload
createRequirement(input: CreateRequirementInput!): CreateRequirementPayload
......@@ -19452,7 +19563,7 @@ Autogenerated input type of UpdateIssue
"""
input UpdateIssueInput {
"""
The IDs of labels to be added to the issue.
The IDs of labels to be added to the issue
"""
addLabelIds: [ID!]
......@@ -19474,18 +19585,13 @@ input UpdateIssueInput {
"""
Due date of the issue
"""
dueDate: Time
dueDate: ISO8601Date
"""
The ID of the parent epic. NULL when removing the association
"""
epicId: ID
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The IID of the issue to mutate
"""
......@@ -19497,7 +19603,7 @@ input UpdateIssueInput {
locked: Boolean
"""
The ID of the milestone to be assigned, milestone will be removed if set to null.
The ID of the milestone to assign to the issue. On update milestone will be removed if set to null
"""
milestoneId: ID
......@@ -19507,12 +19613,12 @@ input UpdateIssueInput {
projectPath: ID!
"""
The IDs of labels to be removed from the issue.
The IDs of labels to be removed from the issue
"""
removeLabelIds: [ID!]
"""
Close or reopen an issue.
Close or reopen an issue
"""
stateEvent: IssueStateEvent
......
......@@ -537,6 +537,16 @@ Autogenerated return type of CreateImageDiffNote.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
### CreateIssuePayload
Autogenerated return type of CreateIssue.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
### CreateIterationPayload
Autogenerated return type of CreateIteration.
......
......@@ -248,13 +248,26 @@ Changes to the issue format can be submitted in the
## Cleaning up
Once the change is deemed stable, submit a new merge request to remove the
feature flag. This ensures the change is available to all users and self-managed
instances. Make sure to add the ~"feature flag" label to this merge request so
release managers are aware the changes are hidden behind a feature flag. If the
merge request has to be picked into a stable branch, make sure to also add the
appropriate `~"Pick into X.Y"` label (e.g. `~"Pick into 13.0"`).
See [the process document](process.md#including-a-feature-behind-feature-flag-in-the-final-release) for further details.
A feature flag should be removed as soon as it is no longer needed. Each additional
feature flag in the codebase increases the complexity of the application
and reduces confidence in our testing suite covering all possible combinations.
Additionally, a feature flag overwritten in some of the environments can result
in undefined and untested system behavior.
To remove a feature flag:
1. Open a new merge request with the ~"feature flag" label so
release managers are aware the changes are hidden behind a feature flag.
1. If the merge request has to be picked into a stable branch, add the
appropriate `~"Pick into X.Y"` label, for example `~"Pick into 13.0"`.
See [the feature flag process](process.md#including-a-feature-behind-feature-flag-in-the-final-release)
for further details.
1. Remove all references to the feature flag from the codebase.
1. Remove the YAML definition for the feature from the repository.
1. Clean up the feature flag from all environments with `/chatops run feature delete some_feature`.
1. Close the rollout issue for the feature flag after the feature flag is removed from the codebase.
### Cleanup ChatOps
When a feature gate has been removed from the code base, the feature
record still exists in the database that the flag was deployed too.
......
......@@ -153,6 +153,11 @@ default_enabled: false
TIP: **Tip:**
To create a feature flag that is only used in EE, add the `--ee` flag: `bin/feature-flag --ee`
## Delete a feature flag
See [cleaning up feature flags](controls.md#cleaning-up) for more information about
deleting feature flags.
## Develop with a feature flag
There are two main ways of using Feature Flags in the GitLab codebase:
......
......@@ -619,7 +619,7 @@ Ensure you comply with the [Changelog entries guide](../changelog.md).
### 8. Ask for a Product Analytics Review
On GitLab.com, we have DangerBot setup to monitor Product Analytics related files and DangerBot will recommend a Product Analytics review. Mention `@gitlab-org/growth/product-analytics/engineers` in your MR for a review.
On GitLab.com, we have DangerBot setup to monitor Product Analytics related files and DangerBot will recommend a Product Analytics review. Mention `@gitlab-org/growth/product_analytics/engineers` in your MR for a review.
### 9. Verify your metric
......
......@@ -939,12 +939,10 @@ installed version of GitLab, the restore command aborts with an error
message. Install the [correct GitLab version](https://packages.gitlab.com/gitlab/),
and then try again.
There is a [known issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3470)
for restore not working with `pgbouncer`. To work around the issue, the Rails
node must bypass `pgbouncer` and connect directly to the primary
database node. You can do this by setting `gitlab_rails['db_host']` and
`gitlab_rails['port']` to connect to the primary database node and
[reconfiguring GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure).
NOTE: **Note:**
There is a known issue with restore not working with `pgbouncer`. The [workaround is to bypass
`pgbouncer` and connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer).
[Read more about backup and restore with `pgbouncer`](#backup-and-restore-for-installations-using-pgbouncer).
### Restore for Docker image and GitLab Helm chart installations
......@@ -1039,6 +1037,29 @@ VM snapshots of the entire GitLab server. It's not uncommon however for a VM
snapshot to require you to power down the server, which limits this solution's
practical use.
## Backup and restore for installations using PgBouncer
PgBouncer can cause the following errors when performing backups and restores:
```ruby
ActiveRecord::StatementInvalid: PG::UndefinedTable
```
There is a [known issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3470) for restore not working
with `pgbouncer`.
To workaround this issue, the GitLab server will need to bypass `pgbouncer` and
[connect directly to the primary database node](../administration/postgresql/pgbouncer.md#procedure-for-bypassing-pgbouncer)
to perform the database restore.
There is also a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/23211)
with PostgreSQL 9 and running a database backup through PgBouncer that can cause
an outage to GitLab. If you're still on PostgreSQL 9 and upgrading PostgreSQL isn't
an option, workarounds include having a dedicated application node just for backups,
configured to connect directly the primary database node as noted above. You're
advised to upgrade your PostgreSQL version though, GitLab 11.11 shipped with PostgreSQL
10.7, and that is the recommended version for GitLab 12+.
## Additional notes
This documentation is for GitLab Community and Enterprise Edition. We back up
......
......@@ -146,7 +146,7 @@ NOTE: **Note:**
These commands will not work for artifacts stored on
[object storage](../administration/object_storage.md).
When you notice there are more job artifacts files on disk than there
When you notice there are more job artifacts files and/or directories on disk than there
should be, you can run:
```shell
......@@ -157,7 +157,7 @@ This command:
- Scans through the entire artifacts folder.
- Checks which files still have a record in the database.
- If no database record is found, the file is deleted from disk.
- If no database record is found, the file and directory is deleted from disk.
By default, this task does not delete anything but shows what it can
delete. Run the command with `DRY_RUN=false` if you actually want to
......
......@@ -75,5 +75,18 @@ module EE
items.in_iterations(params.iterations)
end
end
override :filter_negated_items
def filter_negated_items(items)
items = by_negated_epic(items)
super(items)
end
def by_negated_epic(items)
return items unless not_params[:epic_id].present?
items.not_in_epics(not_params[:epic_id].to_i)
end
end
end
# frozen_string_literal: true
module EE
module Mutations
module Issues
module CommonMutationArguments
extend ActiveSupport::Concern
included do
argument :health_status,
::Types::HealthStatusEnum,
required: false,
description: 'The desired health status'
argument :weight, GraphQL::INT_TYPE,
required: false,
description: 'The weight of the issue'
end
end
end
end
end
# frozen_string_literal: true
module EE
module Mutations
module Issues
module Create
extend ActiveSupport::Concern
prepended do
argument :epic_id, ::Types::GlobalIDType[::Epic],
required: false,
description: 'The ID of an epic to associate the issue with'
end
private
def create_issue_params(params)
params[:epic_id] &&= params[:epic_id]&.model_id
super(params)
end
end
end
end
end
......@@ -7,12 +7,7 @@ module EE
extend ActiveSupport::Concern
prepended do
argument :health_status,
::Types::HealthStatusEnum,
required: false,
description: 'The desired health status'
argument :epic_id,
GraphQL::ID_TYPE,
argument :epic_id, GraphQL::ID_TYPE,
required: false,
description: 'The ID of the parent epic. NULL when removing the association'
end
......
......@@ -26,6 +26,7 @@ module EE
scope :no_epic, -> { left_outer_joins(:epic_issue).where(epic_issues: { epic_id: nil }) }
scope :any_epic, -> { joins(:epic_issue) }
scope :in_epics, ->(epics) { joins(:epic_issue).where(epic_issues: { epic_id: epics }) }
scope :not_in_epics, ->(epics) { left_outer_joins(:epic_issue).where('epic_issues.epic_id NOT IN (?) OR epic_issues.epic_id IS NULL', epics) }
scope :no_iteration, -> { where(sprint_id: nil) }
scope :any_iteration, -> { where.not(sprint_id: nil) }
scope :in_iterations, ->(iterations) { where(sprint_id: iterations) }
......
---
title: Fix issue filtering by negated epic parameter
merge_request: 44719
author:
type: fixed
---
name: sectional_codeowners
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true
......@@ -12,8 +12,6 @@ module EE
def perform
if remaining?
::BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, self.class.name)
else
::Feature.enable(:approval_rule)
end
end
......
......@@ -61,7 +61,7 @@ RSpec.describe 'epics swimlanes filtering', :js do
wait_for_empty_boards((3..4))
end
it 'filters by assignee' do
it 'filters by assignee', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/266990' do
wait_for_all_requests
set_filter("assignee", user.username)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Filter issues by epic', :js do
include FilteredSearchHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:issue1) { create(:issue, project: project) }
let_it_be(:issue2) { create(:issue, project: project) }
let_it_be(:issue3) { create(:issue, project: project) }
let_it_be(:issue4) { create(:issue, project: project) }
let_it_be(:epic1) { create(:epic) }
let_it_be(:epic2) { create(:epic) }
let_it_be(:epic_issue1) { create(:epic_issue, issue: issue1, epic: epic1) }
let_it_be(:epic_issue2) { create(:epic_issue, issue: issue2, epic: epic2) }
let_it_be(:epic_issue3) { create(:epic_issue, issue: issue3, epic: epic2) }
let(:js_dropdown) { '#js-dropdown-epic' }
before do
stub_licensed_features(epics: true)
stub_feature_flags(vue_issuables_list: false)
project.add_developer(user)
sign_in(user)
visit project_issues_path(project)
end
it 'filter issues by epic' do
input_filtered_search("epic:=&#{epic1.id}")
expect_issues_list_count(1)
end
it 'filter issues not in the epic' do
input_filtered_search("epic:!=&#{epic1.id}")
expect_issues_list_count(3)
end
end
......@@ -121,6 +121,14 @@ RSpec.describe IssuesFinder do
expect(issues).to contain_exactly(issue_1, issue_2, issue_subepic)
end
end
context 'filter issues not in the epic' do
let(:params) { { not: { epic_id: epic_1.id } } }
it 'returns issues not assigned to the epic' do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue_2, issue_subepic)
end
end
end
context 'filter by iteration' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Issues::Create do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:assignee1) { create(:user) }
let_it_be(:assignee2) { create(:user) }
let(:expected_attributes) do
{
title: 'new title',
description: 'new description',
confidential: true,
due_date: Date.tomorrow,
discussion_locked: true,
weight: 10
}
end
let(:mutation_params) do
{
project_path: project.full_path,
assignee_ids: [assignee1.to_global_id, assignee2.to_global_id],
health_status: Issue.health_statuses[:at_risk]
}.merge(expected_attributes)
end
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:mutated_issue) { subject[:issue] }
specify { expect(described_class).to require_graphql_authorizations(:create_issue) }
describe '#resolve' do
before do
project.add_guest(assignee1)
project.add_guest(assignee2)
stub_licensed_features(issuable_health_status: true)
end
subject { mutation.resolve(mutation_params) }
context 'when user can create issues' do
before do
project.add_developer(user)
end
it 'creates issue with correct EE values' do
expect(mutated_issue).to have_attributes(expected_attributes)
expect(mutated_issue.assignees.pluck(:id)).to eq([assignee1.id, assignee2.id])
expect(mutated_issue.health_status).to eq('at_risk')
end
end
end
end
......@@ -16,7 +16,15 @@ RSpec.describe Mutations::Issues::Update do
let_it_be(:epic) { create(:epic, group: group) }
let(:epic_id) { epic.to_global_id.to_s }
let(:params) { { project_path: project.full_path, iid: issue.iid, epic_id: epic_id } }
let(:params) do
{
project_path: project.full_path,
iid: issue.iid,
epic_id: epic_id,
weight: 10
}
end
let(:mutated_issue) { subject[:issue] }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
......@@ -53,6 +61,7 @@ RSpec.describe Mutations::Issues::Update do
it 'returns the updated issue' do
expect(mutated_issue.epic).to eq(epic)
expect(mutated_issue.weight).to eq(10)
end
end
......
......@@ -22,8 +22,6 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateApproverToApprovalRulesCheckP
allow(Gitlab::BackgroundMigration).to receive(:exists?).with('MigrateApproverToApprovalRulesInBatch').and_return(false)
described_class.new.perform
expect(Feature.enabled?(:approval_rule, default_enabled: true)).to eq(true)
end
end
end
......@@ -132,6 +132,13 @@ RSpec.describe Issue do
end
end
describe '.not_in_epics' do
it 'returns only issues not in selected epics' do
expect(described_class.count).to eq 3
expect(described_class.not_in_epics([epic1])).to match_array([epic_issue2.issue, issue_no_epic])
end
end
describe '.distinct_epic_ids' do
it 'returns distinct epic ids' do
expect(described_class.distinct_epic_ids.map(&:epic_id)).to match_array([epic1.id, epic2.id])
......
......@@ -128,7 +128,7 @@ module API
pipeline = user_project.all_pipelines.find(params[:pipeline_id])
if Feature.enabled?(:ci_jobs_finder_refactor)
if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true)
builds = ::Ci::JobsFinder
.new(current_user: current_user, pipeline: pipeline, params: params)
.execute
......@@ -157,7 +157,7 @@ module API
pipeline = user_project.all_pipelines.find(params[:pipeline_id])
if Feature.enabled?(:ci_jobs_finder_refactor)
if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true)
bridges = ::Ci::JobsFinder
.new(current_user: current_user, pipeline: pipeline, params: params, type: ::Ci::Bridge)
.execute
......
......@@ -231,9 +231,6 @@ module API
authorize! :create_issue, user_project
params.delete(:created_at) unless current_user.can?(:set_issue_created_at, user_project)
params.delete(:iid) unless current_user.can?(:set_issue_iid, user_project)
issue_params = declared_params(include_missing: false)
issue_params[:system_note_timestamp] = params[:created_at]
......@@ -279,8 +276,6 @@ module API
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
# Setting updated_at is allowed only for admins and owners
params.delete(:updated_at) unless current_user.can?(:set_issue_updated_at, user_project)
issue.system_note_timestamp = params[:updated_at]
update_params = declared_params(include_missing: false).merge(request: request, api: true)
......
......@@ -6,9 +6,20 @@ module Gitlab
class Metrics
extend Gitlab::Utils::StrongMemoize
OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite,
:accepted, :finalized, :discarded, :conflict, :locked,
:invalid].freeze
OPERATIONS = [
:appended, # new trace data has been written to a chunk
:streamed, # new trace data has been sent by a runner
:chunked, # new trace chunk has been created
:mutated, # trace has been mutated when removing secrets
:overwrite, # runner requested overwritting a build trace
:accepted, # scheduled chunks for migration and responded with 202
:finalized, # all live build trace chunks have been persisted
:discarded, # failed to persist live chunks before timeout
:conflict, # runner has sent unrecognized build state details
:locked, # build trace has been locked by a different mechanism
:stalled, # failed to migrate chunk due to a worker duplication
:invalid # malformed build trace has been detected using CRC32
].freeze
def increment_trace_operation(operation: :unknown)
unless OPERATIONS.include?(operation)
......
......@@ -44,7 +44,6 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:monaco_blobs, default_enabled: true)
push_frontend_feature_flag(:monaco_ci, default_enabled: true)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
push_frontend_feature_flag(:usage_data_api, default_enabled: false)
......
......@@ -4,8 +4,6 @@ module Gitlab
module Marginalia
cattr_accessor :enabled, default: false
MARGINALIA_FEATURE_FLAG = :marginalia
def self.set_application_name
::Marginalia.application_name = Gitlab.process_name
end
......@@ -16,15 +14,11 @@ module Gitlab
end
end
def self.cached_feature_enabled?
enabled
end
def self.set_feature_cache
def self.set_enabled_from_feature_flag
# During db:create and db:bootstrap skip feature query as DB is not available yet.
return false unless Gitlab::Database.cached_table_exists?('features')
self.enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG)
self.enabled = Feature.enabled?(:marginalia)
end
end
end
......@@ -5,7 +5,7 @@ module Gitlab
module Marginalia
module ActiveRecordInstrumentation
def annotate_sql(sql)
Gitlab::Marginalia.cached_feature_enabled? ? super(sql) : sql
Gitlab::Marginalia.enabled ? super(sql) : sql
end
end
end
......
......@@ -73,6 +73,10 @@ module QA
element :project_path_content
end
view 'app/assets/javascripts/ide/components/commit_sidebar/message_field.vue' do
element :ide_commit_message_field
end
def has_file?(file_name)
within_element(:file_list) do
page.has_content? file_name
......@@ -83,6 +87,10 @@ module QA
has_element?(:project_path_content, project_path: project_path)
end
def go_to_project
click_element(:project_path_content, Page::Project::Show)
end
def create_new_file_from_template(file_name, template)
click_element(:new_file, Page::Component::WebIDE::Modal::CreateNewFile)
......@@ -115,7 +123,7 @@ module QA
find_element(:commit_sha_content).text
end
def commit_changes(open_merge_request: false)
def commit_changes(commit_message = nil, open_merge_request: false)
# Clicking :begin_commit_button switches from the
# edit to the commit view
click_element(:begin_commit_button)
......@@ -133,6 +141,10 @@ module QA
has_element?(:commit_button)
end
if commit_message
fill_element(:ide_commit_message_field, commit_message)
end
if open_merge_request
click_element(:commit_button, Page::MergeRequest::New)
else
......
......@@ -27,11 +27,14 @@ module QA
Page::Project::Show.perform(&:create_first_new_file!)
Page::File::Form.perform do |form|
form.add_name(@name)
form.add_content(@content)
form.add_commit_message(@commit_message)
form.commit_changes
Page::Project::WebIDE::Edit.perform do |ide|
ide.add_file(@name, @content)
ide.commit_changes(@commit_message)
ide.go_to_project
end
Page::Project::Show.perform do |project|
project.click_file(@name)
end
end
......
......@@ -18,7 +18,6 @@ module QA
file.commit_message = commit_message_for_create
end
expect(page).to have_content('The file has been successfully created.')
expect(page).to have_content(file_name)
expect(page).to have_content(file_content)
expect(page).to have_content(commit_message_for_create)
......
......@@ -10,7 +10,6 @@ module QA
end
end
let(:web_ide_url) { current_url + '-/ide/project/' + project.path_with_namespace }
let(:file_name) { 'the very first file.txt' }
before do
......@@ -18,10 +17,8 @@ module QA
end
it "creates the first file in an empty project via Web IDE", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/847' do
# In the first iteration, the test opens Web IDE by modifying the URL to address past regressions.
# Once the Web IDE button is introduced for empty projects, the test will be modified to go through UI.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/27915 and https://gitlab.com/gitlab-org/gitlab/-/issues/27535.
page.visit(web_ide_url)
project.visit!
Page::Project::Show.perform(&:create_first_new_file!)
Page::Project::WebIDE::Edit.perform do |ide|
ide.create_first_file(file_name)
......
# frozen_string_literal: true
module RuboCop
module Cop
module Graphql
class GIDExpectedType < RuboCop::Cop::Cop
MSG = 'Add an expected_type parameter to #object_from_id calls if possible.'
def_node_search :id_from_object?, <<~PATTERN
(send ... :object_from_id (...))
PATTERN
def on_send(node)
return unless id_from_object?(node)
add_offense(node)
end
end
end
end
end
#!/usr/bin/env ruby
class String
def red
"\e[31m#{self}\e[0m"
end
def yellow
"\e[33m#{self}\e[0m"
end
def green
"\e[32m#{self}\e[0m"
end
def bold
"\e[1m#{self}\e[0m"
end
end
flags_paths = [
'config/feature_flags/**/*.yml'
]
# For EE additionally process `ee/` feature flags
if File.exist?('ee/app/models/license.rb') && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
flags_paths << 'ee/config/feature_flags/**/*.yml'
end
all_flags = {}
additional_flags = Set.new
# Iterate all defined feature flags
# to discover which were used
flags_paths.each do |flags_path|
puts flags_path
Dir.glob(flags_path).each do |path|
feature_flag_name = File.basename(path, '.yml')
all_flags[feature_flag_name] = File.exist?(File.join('tmp', 'feature_flags', feature_flag_name + '.used'))
end
end
# Iterate all used feature flags
# to discover which flags are undefined
Dir.glob('tmp/feature_flags/*.used').each do |path|
feature_flag_name = File.basename(path, '.used')
additional_flags.add(feature_flag_name) unless all_flags[feature_flag_name]
end
used_flags = all_flags.select { |name, used| used }
unused_flags = all_flags.reject { |name, used| used }
puts "=========================================".green.bold
puts "Feature Flags usage summary:".green.bold
puts
puts "- #{all_flags.count + additional_flags.count} was found"
puts "- #{unused_flags.count} appear(s) to be UNUSED".yellow
puts "- #{additional_flags.count} appear(s) to be unknown".yellow
puts "- #{used_flags.count} appear(s) to be used".green
puts
if additional_flags.count > 0
puts "==================================================".green.bold
puts "There are feature flags that appears to be unknown".yellow
puts
puts "They appear to be used by CI, but we do lack their YAML definition".yellow
puts "This is likely expected, so feel free to ignore that list:".yellow
puts
additional_flags.sort.each do |name|
puts "- #{name}".yellow
end
puts
end
if unused_flags.count > 0
puts "========================================".green.bold
puts "These feature flags appears to be UNUSED".red.bold
puts
puts "If they are really no longer needed REMOVE their .yml definition".red
puts "If they are needed you need to ENSURE that their usage is covered with specs to continue.".red
puts
unused_flags.keys.sort.each do |name|
puts "- #{name}".yellow
end
puts
puts "Feature flag usage check failed.".red.bold
exit(1)
end
puts "Everything is fine here!".green
puts
......@@ -6,6 +6,10 @@ RSpec.describe RedisTracking do
let(:feature) { 'approval_rule' }
let(:user) { create(:user) }
before do
skip_feature_flags_yaml_validation
end
controller(ApplicationController) do
include RedisTracking
......
......@@ -3,27 +3,32 @@
require 'spec_helper'
RSpec.describe Dashboard::LabelsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:project_2) { create(:project) }
let_it_be(:label) { create(:label, project: project, title: 'some_label') }
let_it_be(:label_with_same_title) { create(:label, project: project_2, title: 'some_label') }
let_it_be(:unrelated_label) { create(:label, project: create(:project, :public)) }
before_all do
project.add_reporter(user)
project_2.add_reporter(user)
end
before do
sign_in(user)
project.add_reporter(user)
end
describe "#index" do
let!(:unrelated_label) { create(:label, project: create(:project, :public)) }
subject { get :index, format: :json }
it 'returns global labels for projects the user has a relationship with' do
it 'returns labels with unique titles for projects the user has a relationship with' do
subject
expect(json_response).to be_kind_of(Array)
expect(json_response.size).to eq(1)
expect(json_response[0]["id"]).to be_nil
expect(json_response[0]["title"]).to eq(label.title)
expect(json_response[0]['title']).to eq(label.title)
end
it_behaves_like 'disabled when using an external authorization service'
......
......@@ -140,6 +140,18 @@ RSpec.describe 'Issue Sidebar' do
end
end
end
it 'shows label text as "Apply" when assignees are changed' do
project.add_developer(user)
visit_issue(project, issue2)
find('.block.assignee .edit-link').click
wait_for_requests
click_on 'Unassigned'
expect(page).to have_link('Apply')
end
end
context 'as a allowed user' do
......
......@@ -250,7 +250,7 @@ RSpec.describe 'User comments on a diff', :js do
end
context 'multiple suggestions in a single note' do
it 'suggestions are presented' do
it 'suggestions are presented', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/258989' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
......
......@@ -2,7 +2,9 @@
require 'spec_helper'
RSpec.describe 'User creates blob in new project', :js do
RSpec.describe 'User creates new blob', :js do
include WebIdeSpecHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :empty_repo) }
......@@ -12,16 +14,19 @@ RSpec.describe 'User creates blob in new project', :js do
visit project_path(project)
end
it 'allows the user to add a new file' do
it 'allows the user to add a new file in Web IDE' do
click_link 'New file'
execute_script("monaco.editor.getModels()[0].setValue('Hello world')")
wait_for_requests
ide_create_new_file('dummy-file', content: "Hello world\n")
fill_in(:file_name, with: 'dummy-file')
ide_commit
click_button('Commit changes')
click_button('Commit')
expect(page).to have_content('The file has been successfully created')
expect(page).to have_content('All changes are committed')
expect(project.repository.blob_at('master', 'dummy-file').data).to eql("Hello world\n")
end
end
......
......@@ -8,117 +8,88 @@ RSpec.describe 'CI Lint', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
shared_examples 'correct ci linting process' do
describe 'YAML parsing' do
shared_examples 'validates the YAML' do
before do
stub_feature_flags(ci_lint_vue: false)
click_on 'Validate'
end
let(:content_selector) { '.content .view-lines' }
context 'YAML is correct' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
before do
stub_feature_flags(ci_lint_vue: false)
project.add_developer(user)
sign_in(user)
it 'parses Yaml and displays the jobs' do
expect(page).to have_content('Status: syntax is correct')
visit project_ci_lint_path(project)
editor_set_value(yaml_content)
within "table" do
aggregate_failures do
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach')
expect(page).to have_content('Deploy Job - staging')
expect(page).to have_content('Deploy Job - production')
end
end
end
end
wait_for('YAML content') do
find(content_selector).text.present?
end
end
context 'YAML is incorrect' do
let(:yaml_content) { 'value: cannot have :' }
describe 'YAML parsing' do
shared_examples 'validates the YAML' do
before do
stub_feature_flags(ci_lint_vue: false)
click_on 'Validate'
end
it 'displays information about an error' do
expect(page).to have_content('Status: syntax is incorrect')
expect(page).to have_selector(content_selector, text: yaml_content)
end
context 'YAML is correct' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
end
it_behaves_like 'validates the YAML'
it 'parses Yaml and displays the jobs' do
expect(page).to have_content('Status: syntax is correct')
context 'when Dry Run is checked' do
before do
check 'Simulate a pipeline created for the default branch'
within "table" do
aggregate_failures do
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach')
expect(page).to have_content('Deploy Job - staging')
expect(page).to have_content('Deploy Job - production')
end
end
end
it_behaves_like 'validates the YAML'
end
describe 'YAML revalidate' do
let(:yaml_content) { 'my yaml content' }
context 'YAML is incorrect' do
let(:yaml_content) { 'value: cannot have :' }
it 'loads previous YAML content after validation' do
expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea')
it 'displays information about an error' do
expect(page).to have_content('Status: syntax is incorrect')
expect(page).to have_selector(content_selector, text: yaml_content)
end
end
end
describe 'YAML clearing' do
it_behaves_like 'validates the YAML'
context 'when Dry Run is checked' do
before do
click_on 'Clear'
check 'Simulate a pipeline created for the default branch'
end
context 'YAML is present' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
it 'YAML content is cleared' do
expect(page).to have_field('content', with: '', visible: false, type: 'textarea')
end
end
it_behaves_like 'validates the YAML'
end
end
context 'with ACE editor' do
it_behaves_like 'correct ci linting process' do
let(:content_selector) { '.ace_content' }
describe 'YAML revalidate' do
let(:yaml_content) { 'my yaml content' }
before do
stub_feature_flags(monaco_ci: false)
stub_feature_flags(ci_lint_vue: false)
project.add_developer(user)
sign_in(user)
visit project_ci_lint_path(project)
find('#ci-editor')
execute_script("ace.edit('ci-editor').setValue(#{yaml_content.to_json});")
# Ace editor updates a hidden textarea and it happens asynchronously
wait_for('YAML content') do
find(content_selector).text.present?
end
it 'loads previous YAML content after validation' do
expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea')
end
end
end
context 'with Editor Lite' do
it_behaves_like 'correct ci linting process' do
let(:content_selector) { '.content .view-lines' }
before do
stub_feature_flags(monaco_ci: true)
stub_feature_flags(ci_lint_vue: false)
project.add_developer(user)
sign_in(user)
describe 'YAML clearing' do
before do
click_on 'Clear'
end
visit project_ci_lint_path(project)
editor_set_value(yaml_content)
context 'YAML is present' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
wait_for('YAML content') do
find(content_selector).text.present?
end
it 'YAML content is cleared' do
expect(page).to have_field('content', with: '', visible: false, type: 'textarea')
end
end
end
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Project owner sees a link to create a license file in empty project', :js do
include WebIdeSpecHelpers
let(:project) { create(:project_empty_repo) }
let(:project_maintainer) { project.owner }
......@@ -10,36 +12,35 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license
sign_in(project_maintainer)
end
it 'project maintainer creates a license file from a template' do
it 'allows project maintainer creates a license file from a template in Web IDE' do
visit project_path(project)
click_on 'Add LICENSE'
expect(page).to have_content('New file')
expect(current_path).to eq(
project_new_blob_path(project, 'master'))
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
expect(current_path).to eq("/-/ide/project/#{project.full_path}/edit/master/-/LICENSE")
expect(page).to have_selector('.qa-file-templates-bar')
select_template('MIT License')
file_content = first('.file-editor')
expect(file_content).to have_content('MIT License')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
expect(ide_editor_value).to have_content('MIT License')
expect(ide_editor_value).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
ide_commit
click_button('Commit')
expect(current_path).to eq("/-/ide/project/#{project.full_path}/tree/master/-/")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
click_button 'Commit changes'
expect(page).to have_content('All changes are committed')
expect(current_path).to eq(
project_blob_path(project, 'master/LICENSE'))
expect(page).to have_content('MIT License')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
license_file = project.repository.blob_at('master', 'LICENSE').data
expect(license_file).to have_content('MIT License')
expect(license_file).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Apply a template'
click_link template
wait_for_requests
end
click_button 'Choose a template...'
click_button template
wait_for_requests
end
end
......@@ -46,21 +46,21 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
end
it '"New file" button linked to new file page' do
it '"New file" button linked to IDE new file page' do
page.within('.project-buttons') do
expect(page).to have_link('New file', href: project_new_blob_path(project, project.default_branch || 'master'))
expect(page).to have_link('New file', href: presenter.ide_edit_path(project, project.default_branch || 'master'))
end
end
it '"Add README" button linked to new file populated for a README' do
it '"Add README" button linked to IDE new file populated for a README' do
page.within('.project-buttons') do
expect(page).to have_link('Add README', href: presenter.add_readme_path)
expect(page).to have_link('Add README', href: presenter.add_readme_ide_path)
end
end
it '"Add license" button linked to new file populated for a license' do
it '"Add license" button linked to IDE new file populated for a license' do
page.within('.project-buttons') do
expect(page).to have_link('Add LICENSE', href: presenter.add_license_path)
expect(page).to have_link('Add LICENSE', href: presenter.add_license_ide_path)
end
end
......@@ -74,9 +74,9 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
end
it '"New file" button linked to new file page' do
it '"New file" button linked to IDE new file page' do
page.within('.project-buttons') do
expect(page).to have_link('New file', href: project_new_blob_path(project, 'example_branch'))
expect(page).to have_link('New file', href: presenter.ide_edit_path(project, 'example_branch'))
end
end
end
......@@ -144,7 +144,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.readme).not_to be_nil
page.within('.project-buttons') do
expect(page).not_to have_link('Add README', href: presenter.add_readme_path)
expect(page).not_to have_link('Add README', href: presenter.add_readme_ide_path)
expect(page).to have_link('README', href: presenter.readme_path)
end
end
......@@ -164,7 +164,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
end
context 'when the project does not have a README' do
it 'shows the "Add README" button' do
it 'shows the single file editor "Add README" button' do
allow(project.repository).to receive(:readme).and_return(nil)
visit project_path(project)
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Developer views tags' do
include RepoHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
......@@ -15,10 +17,13 @@ RSpec.describe 'Developer views tags' do
let(:project) { create(:project_empty_repo, namespace: group) }
before do
visit project_path(project)
click_on 'Add README'
fill_in :commit_message, with: 'Add a README file', visible: true
click_button 'Commit changes'
project.repository.create_file(
user,
'README.md',
'Example readme',
message: 'Add README',
branch_name: 'master')
visit project_tags_path(project)
end
......
......@@ -11,6 +11,7 @@ describe('AssigneeTitle component', () => {
propsData: {
numberOfAssignees: 0,
editable: false,
changing: false,
...props,
},
});
......@@ -62,6 +63,22 @@ describe('AssigneeTitle component', () => {
});
});
describe('when changing is false', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true });
expect(wrapper.find('[data-test-id="edit-link"]').text()).toEqual('Edit');
});
});
describe('when changing is true', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true, changing: true });
expect(wrapper.find('[data-test-id="edit-link"]').text()).toEqual('Apply');
});
});
it('does not render spinner by default', () => {
wrapper = createComponent({
numberOfAssignees: 0,
......
import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
import EditForm from '~/sidebar/components/lock/edit_form.vue';
import createStore from '~/notes/stores';
......@@ -19,6 +20,8 @@ describe('IssuableLockForm', () => {
const findLockStatus = () => wrapper.find('[data-testid="lock-status"]');
const findEditLink = () => wrapper.find('[data-testid="edit-link"]');
const findEditForm = () => wrapper.find(EditForm);
const findSidebarLockStatusTooltip = () =>
getBinding(findSidebarCollapseIcon().element, 'gl-tooltip');
const initStore = isLocked => {
if (issuableType === ISSUABLE_TYPE_ISSUE) {
......@@ -37,6 +40,9 @@ describe('IssuableLockForm', () => {
isEditable: true,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
......@@ -125,6 +131,13 @@ describe('IssuableLockForm', () => {
expect(findEditForm().exists()).toBe(true);
});
});
it('renders a tooltip with the lock status text', () => {
const tooltip = findSidebarLockStatusTooltip();
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
});
});
});
});
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment