Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-21 12:08:33 +00:00
parent c1cea595b6
commit 6c44b67631
46 changed files with 709 additions and 328 deletions

View File

@ -61,6 +61,7 @@ export default {
<gl-button <gl-button
variant="confirm" variant="confirm"
class="gl-mt-3" class="gl-mt-3"
data-testid="create_new_ci_button"
data-qa-selector="create_new_ci_button" data-qa-selector="create_new_ci_button"
@click="createEmptyConfigFile" @click="createEmptyConfigFile"
> >

View File

@ -1,4 +1,3 @@
import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
import { TREE_TYPE } from '../constants'; import { TREE_TYPE } from '../constants';
export const getLowestSingleFolder = (folder) => { export const getLowestSingleFolder = (folder) => {
@ -28,7 +27,7 @@ export const getLowestSingleFolder = (folder) => {
const { path, tree } = getFolder(folder, [folder.name]); const { path, tree } = getFolder(folder, [folder.name]);
return { return {
path: truncatePathMiddleToLength(path.join('/'), 40), path: path.join('/'),
treeAcc: tree.length ? tree[tree.length - 1].tree : null, treeAcc: tree.length ? tree[tree.length - 1].tree : null,
}; };
}; };

View File

@ -167,36 +167,6 @@ export const truncateWidth = (string, options = {}) => {
*/ */
export const truncateSha = (sha) => sha.substring(0, 8); export const truncateSha = (sha) => sha.substring(0, 8);
const ELLIPSIS_CHAR = '…';
export const truncatePathMiddleToLength = (text, maxWidth) => {
let returnText = text;
let ellipsisCount = 0;
while (returnText.length >= maxWidth) {
const textSplit = returnText.split('/').filter((s) => s !== ELLIPSIS_CHAR);
if (textSplit.length === 0) {
// There are n - 1 path separators for n segments, so 2n - 1 <= maxWidth
const maxSegments = Math.floor((maxWidth + 1) / 2);
return new Array(maxSegments).fill(ELLIPSIS_CHAR).join('/');
}
const middleIndex = Math.floor(textSplit.length / 2);
returnText = textSplit
.slice(0, middleIndex)
.concat(
new Array(ellipsisCount + 1).fill().map(() => ELLIPSIS_CHAR),
textSplit.slice(middleIndex + 1),
)
.join('/');
ellipsisCount += 1;
}
return returnText;
};
/** /**
* Capitalizes first character * Capitalizes first character
* *

View File

@ -16,8 +16,6 @@ import DynamicContent from './dynamic_content.vue';
import StatusIcon from './status_icon.vue'; import StatusIcon from './status_icon.vue';
import ActionButtons from './action_buttons.vue'; import ActionButtons from './action_buttons.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
const WIDGET_PREFIX = 'Widget'; const WIDGET_PREFIX = 'Widget';
const MISSING_RESPONSE_HEADERS = const MISSING_RESPONSE_HEADERS =
'MR Widget: raesponse object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.'; 'MR Widget: raesponse object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.';
@ -49,15 +47,6 @@ export default {
SafeHtml, SafeHtml,
}, },
props: { props: {
/**
* @param {value.collapsed} Object
* @param {value.expanded} Object
*/
value: {
type: Object,
required: false,
default: () => ({}),
},
loadingText: { loadingText: {
type: String, type: String,
required: false, required: false,
@ -238,7 +227,7 @@ export default {
try { try {
if (this.fetchCollapsedData) { if (this.fetchCollapsedData) {
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED); await this.fetch(this.fetchCollapsedData);
} }
} catch { } catch {
this.summaryError = this.errorText; this.summaryError = this.errorText;
@ -271,7 +260,7 @@ export default {
this.contentError = null; this.contentError = null;
try { try {
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED); await this.fetch(this.fetchExpandedData);
} catch { } catch {
this.contentError = this.errorText; this.contentError = this.errorText;
@ -282,7 +271,7 @@ export default {
this.isLoadingExpandedContent = false; this.isLoadingExpandedContent = false;
}, },
fetch(handler, dataType) { fetch(handler) {
const requests = this.multiPolling ? handler() : [handler]; const requests = this.multiPolling ? handler() : [handler];
const promises = requests.map((request) => { const promises = requests.map((request) => {
@ -319,9 +308,7 @@ export default {
}); });
}); });
return Promise.all(promises).then((data) => { return Promise.all(promises);
this.$emit('input', { ...this.value, [dataType]: this.multiPolling ? data : data[0] });
});
}, },
}, },
failedStatusIcon: EXTENSION_ICONS.failed, failedStatusIcon: EXTENSION_ICONS.failed,

View File

@ -110,7 +110,7 @@ module Projects
update_pending_builds update_pending_builds
post_update_hooks(project) post_update_hooks(project, @old_group)
rescue Exception # rubocop:disable Lint/RescueException rescue Exception # rubocop:disable Lint/RescueException
rollback_side_effects rollback_side_effects
raise raise
@ -119,7 +119,7 @@ module Projects
end end
# Overridden in EE # Overridden in EE
def post_update_hooks(project) def post_update_hooks(project, _old_group)
ensure_personal_project_owner_membership(project) ensure_personal_project_owner_membership(project)
invalidate_personal_projects_counts invalidate_personal_projects_counts

View File

@ -1,3 +1,4 @@
.form-group .form-group
= f.label s_('SubgroupCreationLevel|Roles allowed to create subgroups'), class: 'label-bold' = f.label s_('SubgroupCreationLevel|Roles allowed to create subgroups'), class: 'label-bold'
= f.select :subgroup_creation_level, options_for_select(::Gitlab::Access.subgroup_creation_options, group.subgroup_creation_level), {}, class: 'form-control' - ::Gitlab::Access.subgroup_creation_options.each do |label, action|
= f.gitlab_ui_radio_component :subgroup_creation_level, action, label

View File

@ -0,0 +1,8 @@
---
name: ci_interpolation_inputs_refactor
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125632
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/418198
milestone: '16.3'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -371,6 +371,11 @@ module.exports = {
include: /node_modules/, include: /node_modules/,
loader: 'babel-loader', loader: 'babel-loader',
}, },
{
test: /gridstack\/.*\.js$/,
include: /node_modules/,
loader: 'babel-loader',
},
{ {
test: /_worker\.js$/, test: /_worker\.js$/,
resourceQuery: /worker/, resourceQuery: /worker/,

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class EnsureBackfillForCiPipelineVariablesPipelineIdIsFinished < Gitlab::Database::Migration[2.1]
include Gitlab::Database::MigrationHelpers::ConvertToBigint
restrict_gitlab_migration gitlab_schema: :gitlab_ci
disable_ddl_transaction!
TABLE_NAME = :ci_pipeline_variables
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
table_name: TABLE_NAME,
column_name: 'pipeline_id',
job_arguments: [['pipeline_id'], ['id_convert_to_bigint']]
)
end
def down
# no-op
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateAsyncIndexForCiPiplineVariablesPipelineId < Gitlab::Database::Migration[2.1]
TABLE_NAME = :ci_pipeline_variables
INDEX_NAME = "index_ci_pipeline_variables_on_pipeline_id_bigint_and_key"
def up
prepare_async_index TABLE_NAME, [:pipeline_id_convert_to_bigint, :key], unique: true, name: INDEX_NAME
end
def down
unprepare_async_index TABLE_NAME, [:pipeline_id_convert_to_bigint, :key], unique: true, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
e2a7487d4da68819a1bdde6626b32ef8399a917a75d4cbc8ecf2fa140ba1ff16

View File

@ -0,0 +1 @@
54ac5a22e121379b1ffcefc7b4c1f26cadd15a9b0cabfd0d9e3cba3886777d46

View File

@ -52,9 +52,9 @@ during indexing and searching operations. Some of the benefits and tradeoffs to
- Routing is not used if too many shards would be hit for global and group scoped searches. - Routing is not used if too many shards would be hit for global and group scoped searches.
- Shard size imbalance might occur. - Shard size imbalance might occur.
## Existing Analyzers/Tokenizers/Filters ## Existing analyzers and tokenizers
These are all defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb) The following analyzers and tokenizers are defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb).
### Analyzers ### Analyzers
@ -72,7 +72,7 @@ Please see the `sha_tokenizer` explanation later below for an example.
#### `code_analyzer` #### `code_analyzer`
Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the filters: [`code`](#code), `lowercase`, and `asciifolding` Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the [`word_delimiter_graph`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-word-delimiter-graph-tokenfilter.html), `lowercase`, and `asciifolding` filters.
The `whitespace` tokenizer was selected to have more control over how tokens are split. For example the string `Foo::bar(4)` needs to generate tokens like `Foo` and `bar(4)` to be properly searched. The `whitespace` tokenizer was selected to have more control over how tokens are split. For example the string `Foo::bar(4)` needs to generate tokens like `Foo` and `bar(4)` to be properly searched.
@ -81,10 +81,6 @@ Please see the `code` filter for an explanation on how tokens are split.
NOTE: NOTE:
The [Elasticsearch `code_analyzer` doesn't account for all code cases](../integration/advanced_search/elasticsearch_troubleshooting.md#elasticsearch-code_analyzer-doesnt-account-for-all-code-cases). The [Elasticsearch `code_analyzer` doesn't account for all code cases](../integration/advanced_search/elasticsearch_troubleshooting.md#elasticsearch-code_analyzer-doesnt-account-for-all-code-cases).
#### `code_search_analyzer`
Not directly used for indexing, but rather used to transform a search input. Uses the `whitespace` tokenizer and the `lowercase` and `asciifolding` filters.
### Tokenizers ### Tokenizers
#### `sha_tokenizer` #### `sha_tokenizer`
@ -115,27 +111,10 @@ Example:
- `'path/application.js'` - `'path/application.js'`
- `'application.js'` - `'application.js'`
### Filters
#### `code`
Uses a [Pattern Capture token filter](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-pattern-capture-tokenfilter.html) to split tokens into more easily searched versions of themselves.
Patterns:
- `"(\\p{Ll}+|\\p{Lu}\\p{Ll}+|\\p{Lu}+)"`: captures CamelCase and lowerCamelCase strings as separate tokens
- `"(\\d+)"`: extracts digits
- `"(?=([\\p{Lu}]+[\\p{L}]+))"`: captures CamelCase strings recursively. For example: `ThisIsATest` => `[ThisIsATest, IsATest, ATest, Test]`
- `'"((?:\\"|[^"]|\\")*)"'`: captures terms inside quotes, removing the quotes
- `"'((?:\\'|[^']|\\')*)'"`: same as above, for single-quotes
- `'\.([^.]+)(?=\.|\s|\Z)'`: separate terms with periods in-between
- `'([\p{L}_.-]+)'`: some common chars in file names to keep the whole filename intact (for example `my_file-ñame.txt`)
- `'([\p{L}\d_]+)'`: letters, numbers and underscores are the most common tokens in programming. Always capture them greedily regardless of context.
## Gotchas ## Gotchas
- Searches can have their own analyzers. Remember to check when editing analyzers - Searches can have their own analyzers. Remember to check when editing analyzers.
- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches - `Character` filters (as opposed to token filters) always replace the original character. These filters can hinder exact searches.
## Zero downtime reindexing with multiple indices ## Zero downtime reindexing with multiple indices

View File

@ -63,6 +63,24 @@ This page shows groups that you are a member of:
- Through membership of a subgroup's parent group. - Through membership of a subgroup's parent group.
- Through direct or inherited membership of a project in the group or subgroup. - Through direct or inherited membership of a project in the group or subgroup.
## View group activity
To view the activity of a project:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Manage > Activity**.
1. Optional. To filter activity by contribution type, select a tab:
- **All**: All contributions by group members in the group and group's projects.
- **Push events**: Push events in the group's projects.
- **Merge events**: Accepted merge requests in the group's projects.
- **Issue events**: Issues opened and closed in the group's projects.
- **Epic events**: Epics opened and closed in the group.
- **Comments**: Comments posted by group members in the group's projects.
- **Wiki**: Updates to wiki pages in the group.
- **Designs**: Designs added, updated, and removed in the group's projects.
- **Team**: Group members who joined and left the group's projects.
## Create a group ## Create a group
To create a group: To create a group:

View File

@ -354,7 +354,16 @@ To view your activity:
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Your work**. 1. Select **Your work**.
1. Select **Activity**. 1. Select **Activity**.
1. Optional. To filter your activity by contribution type, in the **Your Activity** tab, select a tab. 1. Optional. To filter your activity by contribution type, in the **Your Activity** tab, select a tab:
- **All**: All contributions you made in your groups and projects.
- **Push events**: Push events you made in your projects.
- **Merge events**: Merge requests you accepted in your projects.
- **Issue events**: Issues you opened and closed in your projects.
- **Comments**: Comments you posted in your projects.
- **Wiki**: Wiki pages you created and updated in your projects.
- **Designs**: Designs you added, updated, and removed in your projects.
- **Team**: Projects you joined and left.
## Session duration ## Session duration

View File

@ -178,7 +178,15 @@ To view the activity of a project:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Manage > Activity**. 1. Select **Manage > Activity**.
1. Select a tab to view the type of project activity. 1. Optional. To filter activity by contribution type, select a tab:
- **All**: All contributions by project members.
- **Push events**: Push events in the project.
- **Merge events**: Accepted merge requests in the project.
- **Issue events**: Issues opened and closed in the project.
- **Comments**: Comments posted by project members.
- **Designs**: Designs added, updated, and removed in the project.
- **Team**: Members who joined and left the project.
## Search in projects ## Search in projects

View File

@ -203,6 +203,7 @@ module.exports = (path, options = {}) => {
'@gitlab/favicon-overlay', '@gitlab/favicon-overlay',
'@gitlab/cluster-client', '@gitlab/cluster-client',
'bootstrap-vue', 'bootstrap-vue',
'gridstack',
'three', 'three',
'monaco-editor', 'monaco-editor',
'monaco-yaml', 'monaco-yaml',

View File

@ -73,7 +73,11 @@ module Gitlab
end end
def inputs def inputs
@inputs ||= Ci::Input::Inputs.new(spec, args) @inputs ||= if Feature.enabled?(:ci_interpolation_inputs_refactor)
Ci::Interpolation::Inputs.new(spec, args)
else
Ci::Input::Inputs.new(spec, args)
end
end end
def context def context

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Interpolation
# Interpolation inputs provided by the user.
class Inputs
UnknownInputTypeError = Class.new(StandardError)
TYPES = [
StringInput
].freeze
def initialize(specs, args)
@specs = specs.to_h
@args = args.to_h
@inputs = []
@errors = []
validate!
fabricate!
end
def errors
@errors + @inputs.flat_map(&:errors)
end
def valid?
errors.none?
end
def to_hash
@inputs.inject({}) do |hash, input|
hash.merge(input.to_hash)
end
end
private
def validate!
unknown_inputs = @args.keys - @specs.keys
return if unknown_inputs.empty?
@errors.push("unknown input arguments: #{unknown_inputs.join(', ')}")
end
def fabricate!
@specs.each do |input_name, spec|
input_type = TYPES.find { |klass| klass.matches?(spec) }
unless input_type
@errors.push(
"unknown input specification for `#{input_name}` (valid types: #{valid_type_names.join(', ')})")
next
end
@inputs.push(input_type.new(
name: input_name,
spec: spec,
value: @args[input_name]))
end
end
def valid_type_names
TYPES.map(&:type_name)
end
end
end
end
end

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Interpolation
class Inputs
##
# This is a common abstraction for all input types
class BaseInput
ArgumentNotValidError = Class.new(StandardError)
# Checks whether the class matches the type in the specification
def self.matches?(spec)
raise NotImplementedError
end
# Human readable type used in error messages
def self.type_name
raise NotImplementedError
end
# Checks whether the provided value is of the given type
def valid_value?(value)
raise NotImplementedError
end
attr_reader :errors, :name, :spec, :value
def initialize(name:, spec:, value:)
@name = name
@errors = []
# Treat minimal spec definition (nil) as a valid hash:
# spec:
# inputs:
# website:
@spec = spec || {} # specification from input definition
@value = value # actual value provided by the user
validate!
end
def to_hash
raise ArgumentNotValidError unless valid?
{ name => actual_value }
end
def valid?
@errors.none?
end
private
def validate!
return error('required value has not been provided') if required_input? && value.nil?
# validate default value
return error("default value is not a #{self.class.type_name}") if !required_input? && !valid_value?(default)
# validate provided value
error("provided value is not a #{self.class.type_name}") unless valid_value?(value)
end
def error(message)
@errors.push("`#{name}` input: #{message}")
end
def actual_value
# nil check is to support boolean values.
value.nil? ? default : value
end
# An input specification without a default value is required.
# For example:
# ```yaml
# spec:
# inputs:
# website:
# ```
def required_input?
!spec.key?(:default)
end
def default
spec[:default]
end
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Interpolation
class Inputs
class StringInput < BaseInput
def self.matches?(spec)
# The input spec can be `nil` when using a minimal specification
# and also when `type` is not specified.
#
# ```yaml
# spec:
# inputs:
# foo:
# ```
spec.nil? || (spec.is_a?(Hash) && [nil, type_name].include?(spec[:type]))
end
def self.type_name
'string'
end
def valid_value?(value)
value.nil? || value.is_a?(String)
end
end
end
end
end
end

View File

@ -51441,6 +51441,9 @@ msgstr ""
msgid "Vulnerability|Try it out" msgid "Vulnerability|Try it out"
msgstr "" msgstr ""
msgid "Vulnerability|URL:"
msgstr ""
msgid "Vulnerability|Unmodified Response" msgid "Vulnerability|Unmodified Response"
msgstr "" msgstr ""

View File

@ -145,7 +145,7 @@
"gettext-parser": "^6.0.0", "gettext-parser": "^6.0.0",
"graphql": "^15.7.2", "graphql": "^15.7.2",
"graphql-tag": "^2.11.0", "graphql-tag": "^2.11.0",
"gridstack": "^7.3.0", "gridstack": "^8.3.0",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"immer": "^9.0.15", "immer": "^9.0.15",
"ipaddr.js": "^1.9.1", "ipaddr.js": "^1.9.1",

View File

@ -27,13 +27,6 @@ module QA
Page::Project::PipelineEditor::New.perform(&:create_new_ci) Page::Project::PipelineEditor::New.perform(&:create_new_ci)
Page::Project::PipelineEditor::Show.perform do |show| Page::Project::PipelineEditor::Show.perform do |show|
# Editor should display default content when project does not have CI file yet
# New MR checkbox should not be rendered when a new target branch is yet to be provided
aggregate_failures 'check editor default conditions' do
expect(show.editing_content).not_to be_empty
expect(show).to have_no_new_mr_checkbox
end
# The new MR checkbox is visible after a new branch name is set # The new MR checkbox is visible after a new branch name is set
show.set_source_branch(SecureRandom.hex(10)) show.set_source_branch(SecureRandom.hex(10))
expect(show).to have_new_mr_checkbox expect(show).to have_new_mr_checkbox

View File

@ -11,6 +11,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
let(:default_branch) { 'main' } let(:default_branch) { 'main' }
let(:other_branch) { 'test' } let(:other_branch) { 'test' }
let(:branch_with_invalid_ci) { 'despair' } let(:branch_with_invalid_ci) { 'despair' }
let(:branch_without_ci) { 'empty' }
let(:default_content) { 'Default' } let(:default_content) { 'Default' }
@ -45,6 +46,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
project.repository.create_file(user, project.ci_config_path_or_default, default_content, message: 'Create CI file for main', branch_name: default_branch) project.repository.create_file(user, project.ci_config_path_or_default, default_content, message: 'Create CI file for main', branch_name: default_branch)
project.repository.create_file(user, project.ci_config_path_or_default, valid_content, message: 'Create CI file for test', branch_name: other_branch) project.repository.create_file(user, project.ci_config_path_or_default, valid_content, message: 'Create CI file for test', branch_name: other_branch)
project.repository.create_file(user, project.ci_config_path_or_default, invalid_content, message: 'Create CI file for test', branch_name: branch_with_invalid_ci) project.repository.create_file(user, project.ci_config_path_or_default, invalid_content, message: 'Create CI file for test', branch_name: branch_with_invalid_ci)
project.repository.create_file(user, 'index.js', "file", message: 'New js file', branch_name: branch_without_ci)
visit project_ci_pipeline_editor_path(project) visit project_ci_pipeline_editor_path(project)
wait_for_requests wait_for_requests
@ -62,6 +64,31 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end end
end end
describe 'when there are no CI config file' do
before do
visit project_ci_pipeline_editor_path(project, branch_name: branch_without_ci)
end
it 'renders the empty page', :aggregate_failures do
expect(page).to have_content 'Optimize your workflow with CI/CD Pipelines'
expect(page).to have_selector '[data-testid="create_new_ci_button"]'
end
context 'when clicking on the create new CI button' do
before do
click_button 'Configure pipeline'
end
it 'renders the source editor with default content', :aggregate_failures do
expect(page).to have_selector('#source-editor-')
page.within('#source-editor-') do
expect(page).to have_content('This file is a template, and might need editing before it works on your project.')
end
end
end
end
describe 'When CI yml has valid syntax' do describe 'When CI yml has valid syntax' do
before do before do
visit project_ci_pipeline_editor_path(project, branch_name: other_branch) visit project_ci_pipeline_editor_path(project, branch_name: other_branch)
@ -149,15 +176,6 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end end
shared_examples 'default branch switcher behavior' do shared_examples 'default branch switcher behavior' do
def switch_to_branch(branch)
find('[data-testid="branch-selector"]').click
page.within '[data-testid="branch-selector"]' do
click_button branch
wait_for_requests
end
end
it 'displays current branch' do it 'displays current branch' do
page.within('[data-testid="branch-selector"]') do page.within('[data-testid="branch-selector"]') do
expect(page).to have_content(default_branch) expect(page).to have_content(default_branch)
@ -195,12 +213,20 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end end
describe 'Branch Switcher' do describe 'Branch Switcher' do
def switch_to_branch(branch)
# close button for the popover
find('[data-testid="close-button"]').click
find('[data-testid="branch-selector"]').click
page.within '[data-testid="branch-selector"]' do
click_button branch
wait_for_requests
end
end
before do before do
visit project_ci_pipeline_editor_path(project) visit project_ci_pipeline_editor_path(project)
wait_for_requests wait_for_requests
# close button for the popover
find('[data-testid="close-button"]').click
end end
it_behaves_like 'default branch switcher behavior' it_behaves_like 'default branch switcher behavior'
@ -262,6 +288,24 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end end
describe 'Commit Form' do describe 'Commit Form' do
context 'when targetting the main branch' do
it 'does not show the option to create a Merge request', :aggregate_failures do
expect(page).not_to have_selector('[data-testid="new-mr-checkbox"]')
expect(page).not_to have_content('Start a new merge request with these changes')
end
end
context 'when targetting any non-main branch' do
before do
find('#source-branch-field').set('new_branch', clear: :backspace)
end
it 'shows the option to create a Merge request', :aggregate_failures do
expect(page).to have_selector('[data-testid="new-mr-checkbox"]')
expect(page).to have_content('Start a new merge request with these changes')
end
end
it 'is preserved when changing tabs' do it 'is preserved when changing tabs' do
find('#commit-message').set('message', clear: :backspace) find('#commit-message').set('message', clear: :backspace)
find('#source-branch-field').set('new_branch', clear: :backspace) find('#source-branch-field').set('new_branch', clear: :backspace)

View File

@ -382,7 +382,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
}, },
{ {
type: 'tree', type: 'tree',
name: 'ee/lib/…/…/…/longtreenametomakepath', name: 'ee/lib/ee/gitlab/checks/longtreenametomakepath',
tree: [ tree: [
{ {
name: 'diff_check.rb', name: 'diff_check.rb',

View File

@ -238,36 +238,6 @@ describe('text_utility', () => {
}); });
}); });
describe('truncatePathMiddleToLength', () => {
it('does not truncate text', () => {
expect(textUtils.truncatePathMiddleToLength('app/test', 50)).toEqual('app/test');
});
it('truncates middle of the path', () => {
expect(textUtils.truncatePathMiddleToLength('app/test/diff', 13)).toEqual('app/…/diff');
});
it('truncates multiple times in the middle of the path', () => {
expect(textUtils.truncatePathMiddleToLength('app/test/merge_request/diff', 13)).toEqual(
'app/…/…/diff',
);
});
describe('given a path too long for the maxWidth', () => {
it.each`
path | maxWidth | result
${'aa/bb/cc'} | ${1} | ${'…'}
${'aa/bb/cc'} | ${2} | ${'…'}
${'aa/bb/cc'} | ${3} | ${'…/…'}
${'aa/bb/cc'} | ${4} | ${'…/…'}
${'aa/bb/cc'} | ${5} | ${'…/…/…'}
`('truncates ($path, $maxWidth) to $result', ({ path, maxWidth, result }) => {
expect(result.length).toBeLessThanOrEqual(maxWidth);
expect(textUtils.truncatePathMiddleToLength(path, maxWidth)).toEqual(result);
});
});
});
describe('slugifyWithUnderscore', () => { describe('slugifyWithUnderscore', () => {
it('should replaces whitespaces with underscore and convert to lower case', () => { it('should replaces whitespaces with underscore and convert to lower case', () => {
expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string'); expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string');

View File

@ -121,14 +121,15 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
}); });
describe('fetch', () => { describe('fetch', () => {
it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => { it('calls fetchCollapsedData properly when multiPolling is false', async () => {
const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } }; const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } }); const fetchCollapsedData = jest.fn().mockResolvedValue(mockData);
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises(); await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null }); expect(fetchCollapsedData).toHaveBeenCalledTimes(1);
}); });
it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => { it('calls fetchCollapsedData properly when multiPolling is true', async () => {
const mockData1 = { const mockData1 = {
headers: {}, headers: {},
status: HTTP_STATUS_OK, status: HTTP_STATUS_OK,
@ -140,22 +141,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
data: { vulnerabilities: [{ vuln: 2 }] }, data: { vulnerabilities: [{ vuln: 2 }] },
}; };
const fetchCollapsedData = [
jest.fn().mockResolvedValue(mockData1),
jest.fn().mockResolvedValue(mockData2),
];
createComponent({ createComponent({
propsData: { propsData: {
multiPolling: true, multiPolling: true,
fetchCollapsedData: () => [ fetchCollapsedData: () => fetchCollapsedData,
() => Promise.resolve(mockData1),
() => Promise.resolve(mockData2),
],
}, },
}); });
await waitForPromises(); await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({ expect(fetchCollapsedData[0]).toHaveBeenCalledTimes(1);
collapsed: [mockData1.data, mockData2.data], expect(fetchCollapsedData[1]).toHaveBeenCalledTimes(1);
expanded: null,
});
}); });
it('throws an error when the handler does not include headers or status objects', async () => { it('throws an error when the handler does not include headers or status objects', async () => {
@ -328,11 +329,12 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
}; };
const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded); const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded);
const fetchCollapsedData = jest.fn().mockResolvedValue(mockDataCollapsed);
await createComponent({ await createComponent({
propsData: { propsData: {
isCollapsible: true, isCollapsible: true,
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed), fetchCollapsedData,
fetchExpandedData, fetchExpandedData,
}, },
}); });
@ -340,17 +342,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
findToggleButton().vm.$emit('click'); findToggleButton().vm.$emit('click');
await waitForPromises(); await waitForPromises();
// First fetches the collapsed data expect(fetchCollapsedData).toHaveBeenCalledTimes(1);
expect(wrapper.emitted('input')[0][0]).toEqual({ expect(fetchExpandedData).toHaveBeenCalledTimes(1);
collapsed: mockDataCollapsed.data,
expanded: null,
});
// Then fetches the expanded data
expect(wrapper.emitted('input')[1][0]).toEqual({
collapsed: null,
expanded: mockDataExpanded.data,
});
// Triggering a click does not call the expanded data again // Triggering a click does not call the expanded data again
findToggleButton().vm.$emit('click'); findToggleButton().vm.$emit('click');
@ -371,14 +364,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
findToggleButton().vm.$emit('click'); findToggleButton().vm.$emit('click');
await waitForPromises(); await waitForPromises();
// First fetches the collapsed data
expect(wrapper.emitted('input')[0][0]).toEqual({
collapsed: undefined,
expanded: null,
});
expect(fetchExpandedData).toHaveBeenCalledTimes(1); expect(fetchExpandedData).toHaveBeenCalledTimes(1);
expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed
findToggleButton().vm.$emit('click'); findToggleButton().vm.$emit('click');
await waitForPromises(); await waitForPromises();

View File

@ -9,6 +9,8 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
subject { described_class.new(result, arguments) } subject { described_class.new(result, arguments) }
# Remove shared examples when ci_interpolation_inputs_refactor is removed.
shared_examples 'interpolator' do
context 'when input data is valid' do context 'when input data is valid' do
let(:header) do let(:header) do
{ spec: { inputs: { website: nil } } } { spec: { inputs: { website: nil } } }
@ -25,6 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
it 'correctly interpolates the config' do it 'correctly interpolates the config' do
subject.interpolate! subject.interpolate!
expect(subject).to be_interpolated
expect(subject).to be_valid expect(subject).to be_valid
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
end end
@ -90,28 +93,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
end end
end end
context 'when provided interpolation argument is invalid' do
let(:header) do
{ spec: { inputs: { website: nil } } }
end
let(:content) do
{ test: 'deploy $[[ inputs.website ]]' }
end
let(:arguments) do
{ website: ['gitlab.com'] }
end
it 'correctly interpolates the config' do
subject.interpolate!
expect(subject).not_to be_valid
expect(subject.error_message).to eq subject.errors.first
expect(subject.errors).to include 'unsupported value in input argument `website`'
end
end
context 'when multiple interpolation blocks are invalid' do context 'when multiple interpolation blocks are invalid' do
let(:header) do let(:header) do
{ spec: { inputs: { website: nil } } } { spec: { inputs: { website: nil } } }
@ -129,7 +110,8 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
subject.interpolate! subject.interpolate!
expect(subject).not_to be_valid expect(subject).not_to be_valid
expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' expect(subject.error_message)
.to eq 'interpolation interrupted by errors, unknown interpolation key: `something`'
end end
end end
@ -148,7 +130,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
it 'returns original content' do it 'returns original content' do
subject.interpolate! subject.interpolate!
expect(subject).not_to be_interpolated
expect(subject.to_hash).to eq(content) expect(subject.to_hash).to eq(content)
end end
end end
@ -169,9 +150,63 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
it 'correctly interpolates content' do it 'correctly interpolates content' do
subject.interpolate! subject.interpolate!
expect(subject).to be_interpolated
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
end end
end end
end end
end
it_behaves_like 'interpolator' do
context 'when provided interpolation argument is invalid' do
let(:header) do
{ spec: { inputs: { website: nil } } }
end
let(:content) do
{ test: 'deploy $[[ inputs.website ]]' }
end
let(:arguments) do
{ website: ['gitlab.com'] }
end
it 'returns an error' do
subject.interpolate!
expect(subject).not_to be_valid
expect(subject.error_message).to eq subject.errors.first
expect(subject.errors).to include '`website` input: provided value is not a string'
end
end
end
context 'when feature flag ci_interpolation_inputs_refactor is disabled' do
before do
stub_feature_flags(ci_interpolation_inputs_refactor: false)
end
it_behaves_like 'interpolator'
context 'when provided interpolation argument is invalid' do
let(:header) do
{ spec: { inputs: { website: nil } } }
end
let(:content) do
{ test: 'deploy $[[ inputs.website ]]' }
end
let(:arguments) do
{ website: ['gitlab.com'] }
end
it 'returns an error' do
subject.interpolate!
expect(subject).not_to be_valid
expect(subject.error_message).to eq subject.errors.first
expect(subject.errors).to include 'unsupported value in input argument `website`'
end
end
end
end end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do
describe '.matches?' do
it 'is not implemented' do
expect { described_class.matches?(double) }.to raise_error(NotImplementedError)
end
end
describe '.type_name' do
it 'is not implemented' do
expect { described_class.type_name }.to raise_error(NotImplementedError)
end
end
describe '#valid_value?' do
it 'is not implemented' do
expect do
described_class.new(
name: 'website', spec: { website: nil }, value: { website: 'example.com' }
).valid_value?('test')
end.to raise_error(NotImplementedError)
end
end
end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Interpolation::Inputs, feature_category: :pipeline_composition do
let(:inputs) { described_class.new(specs, args) }
let(:specs) { { foo: { default: 'bar' } } }
let(:args) { {} }
context 'when inputs are valid' do
where(:specs, :args, :merged) do
[
[
{ foo: { default: 'bar' } }, {},
{ foo: 'bar' }
],
[
{ foo: { default: 'bar' } }, { foo: 'test' },
{ foo: 'test' }
],
[
{ foo: nil }, { foo: 'bar' },
{ foo: 'bar' }
],
[
{ foo: { type: 'string' } }, { foo: 'bar' },
{ foo: 'bar' }
],
[
{ foo: { type: 'string', default: 'bar' } }, { foo: 'test' },
{ foo: 'test' }
],
[
{ foo: { type: 'string', default: 'bar' } }, {},
{ foo: 'bar' }
],
[
{ foo: { default: 'bar' }, baz: nil }, { baz: 'test' },
{ foo: 'bar', baz: 'test' }
]
]
end
with_them do
it 'contains the merged inputs' do
expect(inputs).to be_valid
expect(inputs.to_hash).to eq(merged)
end
end
end
context 'when inputs are invalid' do
where(:specs, :args, :errors) do
[
[
{ foo: nil }, { foo: 'bar', test: 'bar' },
['unknown input arguments: test']
],
[
{ foo: nil }, { test: 'bar', gitlab: '1' },
['unknown input arguments: test, gitlab', '`foo` input: required value has not been provided']
],
[
{ foo: 123 }, {},
['unknown input specification for `foo` (valid types: string)']
],
[
{ a: nil, foo: 123 }, { a: '123' },
['unknown input specification for `foo` (valid types: string)']
],
[
{ foo: nil }, {},
['`foo` input: required value has not been provided']
],
[
{ foo: { default: 123 } }, { foo: 'test' },
['`foo` input: default value is not a string']
],
[
{ foo: { default: 'test' } }, { foo: 123 },
['`foo` input: provided value is not a string']
],
[
{ foo: nil }, { foo: 123 },
['`foo` input: provided value is not a string']
]
]
end
with_them do
it 'contains the merged inputs', :aggregate_failures do
expect(inputs).not_to be_valid
expect(inputs.errors).to contain_exactly(*errors)
end
end
end
end

View File

@ -117,6 +117,10 @@ module.exports = function storybookWebpackConfig({ config }) {
config.resolve.extensions = Array.from( config.resolve.extensions = Array.from(
new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]), new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]),
); );
config.resolve.alias = {
...config.resolve.alias,
gridstack: require.resolve('gridstack/dist/es5/gridstack.js'),
};
// Replace any Storybook-defined CSS loaders with our custom one. // Replace any Storybook-defined CSS loaders with our custom one.
config.module.rules = [ config.module.rules = [

View File

@ -6746,10 +6746,10 @@ graphql@^15.7.2:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef" resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A== integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
gridstack@^7.3.0: gridstack@^8.3.0:
version "7.3.0" version "8.3.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.3.0.tgz#7b32395edcd885bc39b84068ac86f2831f7a2451" resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-8.3.0.tgz#4c79f8b8c4cffeb3664266108e38ed91b3d0f7b4"
integrity sha512-JKZgsHzm1ljkn1NnBZpf8j4NDOBCXTuw0m1ZC0sr6NKUh0BFWzXAONIxtX1hWGUVeKLj5l1VcmnTwCXw5ypDNw== integrity sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw==
gzip-size@^6.0.0: gzip-size@^6.0.0:
version "6.0.0" version "6.0.0"