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
variant="confirm"
class="gl-mt-3"
data-testid="create_new_ci_button"
data-qa-selector="create_new_ci_button"
@click="createEmptyConfigFile"
>

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
.form-group
= 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/,
loader: 'babel-loader',
},
{
test: /gridstack\/.*\.js$/,
include: /node_modules/,
loader: 'babel-loader',
},
{
test: /_worker\.js$/,
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.
- 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
@ -72,7 +72,7 @@ Please see the `sha_tokenizer` explanation later below for an example.
#### `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.
@ -81,10 +81,6 @@ Please see the `code` filter for an explanation on how tokens are split.
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).
#### `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
#### `sha_tokenizer`
@ -115,27 +111,10 @@ Example:
- `'path/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
- 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
- Searches can have their own analyzers. Remember to check when editing analyzers.
- `Character` filters (as opposed to token filters) always replace the original character. These filters can hinder exact searches.
## 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 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
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. Select **Your work**.
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

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

View File

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

View File

@ -73,7 +73,11 @@ module Gitlab
end
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
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"
msgstr ""
msgid "Vulnerability|URL:"
msgstr ""
msgid "Vulnerability|Unmodified Response"
msgstr ""

View File

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

View File

@ -27,13 +27,6 @@ module QA
Page::Project::PipelineEditor::New.perform(&:create_new_ci)
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
show.set_source_branch(SecureRandom.hex(10))
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(:other_branch) { 'test' }
let(:branch_with_invalid_ci) { 'despair' }
let(:branch_without_ci) { 'empty' }
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, 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, 'index.js', "file", message: 'New js file', branch_name: branch_without_ci)
visit project_ci_pipeline_editor_path(project)
wait_for_requests
@ -62,6 +64,31 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
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
before do
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
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
page.within('[data-testid="branch-selector"]') do
expect(page).to have_content(default_branch)
@ -195,12 +213,20 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end
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
visit project_ci_pipeline_editor_path(project)
wait_for_requests
# close button for the popover
find('[data-testid="close-button"]').click
end
it_behaves_like 'default branch switcher behavior'
@ -262,6 +288,24 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end
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
find('#commit-message').set('message', 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',
name: 'ee/lib/…/…/…/longtreenametomakepath',
name: 'ee/lib/ee/gitlab/checks/longtreenametomakepath',
tree: [
{
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', () => {
it('should replaces whitespaces with underscore and convert to lower case', () => {
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', () => {
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: [] } };
createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } });
const fetchCollapsedData = jest.fn().mockResolvedValue(mockData);
createComponent({ propsData: { fetchCollapsedData } });
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 = {
headers: {},
status: HTTP_STATUS_OK,
@ -140,22 +141,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
data: { vulnerabilities: [{ vuln: 2 }] },
};
const fetchCollapsedData = [
jest.fn().mockResolvedValue(mockData1),
jest.fn().mockResolvedValue(mockData2),
];
createComponent({
propsData: {
multiPolling: true,
fetchCollapsedData: () => [
() => Promise.resolve(mockData1),
() => Promise.resolve(mockData2),
],
fetchCollapsedData: () => fetchCollapsedData,
},
});
await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({
collapsed: [mockData1.data, mockData2.data],
expanded: null,
});
expect(fetchCollapsedData[0]).toHaveBeenCalledTimes(1);
expect(fetchCollapsedData[1]).toHaveBeenCalledTimes(1);
});
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 fetchCollapsedData = jest.fn().mockResolvedValue(mockDataCollapsed);
await createComponent({
propsData: {
isCollapsible: true,
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
fetchCollapsedData,
fetchExpandedData,
},
});
@ -340,17 +342,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
findToggleButton().vm.$emit('click');
await waitForPromises();
// First fetches the collapsed data
expect(wrapper.emitted('input')[0][0]).toEqual({
collapsed: mockDataCollapsed.data,
expanded: null,
});
// Then fetches the expanded data
expect(wrapper.emitted('input')[1][0]).toEqual({
collapsed: null,
expanded: mockDataExpanded.data,
});
expect(fetchCollapsedData).toHaveBeenCalledTimes(1);
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
// Triggering a click does not call the expanded data again
findToggleButton().vm.$emit('click');
@ -371,14 +364,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
findToggleButton().vm.$emit('click');
await waitForPromises();
// First fetches the collapsed data
expect(wrapper.emitted('input')[0][0]).toEqual({
collapsed: undefined,
expanded: null,
});
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed
findToggleButton().vm.$emit('click');
await waitForPromises();

View File

@ -9,6 +9,8 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
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
let(:header) do
{ spec: { inputs: { website: nil } } }
@ -25,6 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
it 'correctly interpolates the config' do
subject.interpolate!
expect(subject).to be_interpolated
expect(subject).to be_valid
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
end
@ -90,28 +93,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
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
let(:header) do
{ spec: { inputs: { website: nil } } }
@ -129,7 +110,8 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
subject.interpolate!
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
@ -148,7 +130,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
it 'returns original content' do
subject.interpolate!
expect(subject).not_to be_interpolated
expect(subject.to_hash).to eq(content)
end
end
@ -169,9 +150,63 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
it 'correctly interpolates content' do
subject.interpolate!
expect(subject).to be_interpolated
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
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

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(
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.
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"
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
gridstack@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.3.0.tgz#7b32395edcd885bc39b84068ac86f2831f7a2451"
integrity sha512-JKZgsHzm1ljkn1NnBZpf8j4NDOBCXTuw0m1ZC0sr6NKUh0BFWzXAONIxtX1hWGUVeKLj5l1VcmnTwCXw5ypDNw==
gridstack@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-8.3.0.tgz#4c79f8b8c4cffeb3664266108e38ed91b3d0f7b4"
integrity sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw==
gzip-size@^6.0.0:
version "6.0.0"