Commit 4dda2b2b authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 35b61afd fe283d6f
...@@ -15,7 +15,7 @@ export default class GlFieldErrors { ...@@ -15,7 +15,7 @@ export default class GlFieldErrors {
initValidators() { initValidators() {
// register selectors here as needed // register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]'] const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]']
.map(selector => `input${selector}`) .map(selector => `input${selector}`)
.join(','); .join(',');
......
...@@ -24,7 +24,13 @@ export default { ...@@ -24,7 +24,13 @@ export default {
...mapState(['pipelinesEmptyStateSvgPath', 'links']), ...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']), ...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']), ...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']), ...mapState('pipelines', [
'isLoadingPipeline',
'hasLoadedPipeline',
'latestPipeline',
'stages',
'isLoadingJobs',
]),
ciLintText() { ciLintText() {
return sprintf( return sprintf(
__('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'), __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
...@@ -36,7 +42,7 @@ export default { ...@@ -36,7 +42,7 @@ export default {
); );
}, },
showLoadingIcon() { showLoadingIcon() {
return this.isLoadingPipeline && this.latestPipeline === null; return this.isLoadingPipeline && !this.hasLoadedPipeline;
}, },
}, },
created() { created() {
...@@ -51,7 +57,7 @@ export default { ...@@ -51,7 +57,7 @@ export default {
<template> <template>
<div class="ide-pipeline"> <div class="ide-pipeline">
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" /> <gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
<template v-else-if="latestPipeline !== null"> <template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header"> <header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" /> <ci-icon :status="latestPipeline.details.status" :size="24" />
<span class="prepend-left-8"> <span class="prepend-left-8">
...@@ -62,7 +68,7 @@ export default { ...@@ -62,7 +68,7 @@ export default {
</span> </span>
</header> </header>
<empty-state <empty-state
v-if="latestPipeline === false" v-if="!latestPipeline"
:help-page-path="links.ciHelpPagePath" :help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath" :empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true" :can-set-ci="true"
......
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
}, },
[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) { [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) {
state.isLoadingPipeline = false; state.isLoadingPipeline = false;
state.hasLoadedPipeline = true;
if (pipeline) { if (pipeline) {
state.latestPipeline = { state.latestPipeline = {
...@@ -34,7 +35,7 @@ export default { ...@@ -34,7 +35,7 @@ export default {
}; };
}); });
} else { } else {
state.latestPipeline = false; state.latestPipeline = null;
} }
}, },
[types.REQUEST_JOBS](state, id) { [types.REQUEST_JOBS](state, id) {
......
export default () => ({ export default () => ({
isLoadingPipeline: true, isLoadingPipeline: true,
hasLoadedPipeline: false,
isLoadingJobs: false, isLoadingJobs: false,
latestPipeline: null, latestPipeline: null,
stages: [], stages: [],
......
...@@ -204,8 +204,10 @@ label { ...@@ -204,8 +204,10 @@ label {
margin-top: #{$grid-size / 2}; margin-top: #{$grid-size / 2};
} }
.gl-field-error { .gl-field-error,
.invalid-feedback {
color: $red-500; color: $red-500;
font-size: $gl-font-size;
} }
.gl-show-field-errors { .gl-show-field-errors {
......
...@@ -46,7 +46,10 @@ class DashboardController < Dashboard::ApplicationController ...@@ -46,7 +46,10 @@ class DashboardController < Dashboard::ApplicationController
end end
def check_filters_presence! def check_filters_presence!
@no_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) } no_scalar_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) }
no_array_filters_set = finder_type.array_params.none? { |k, _| params.key?(k) }
@no_filters_set = no_scalar_filters_set && no_array_filters_set
return unless @no_filters_set return unless @no_filters_set
......
...@@ -124,8 +124,8 @@ class GroupsController < Groups::ApplicationController ...@@ -124,8 +124,8 @@ class GroupsController < Groups::ApplicationController
flash[:notice] = "Group '#{@group.name}' was successfully transferred." flash[:notice] = "Group '#{@group.name}' was successfully transferred."
redirect_to group_path(@group) redirect_to group_path(@group)
else else
flash.now[:alert] = service.error flash[:alert] = service.error
render :edit redirect_to edit_group_path(@group)
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -117,7 +117,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -117,7 +117,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
terminal = environment.terminals.try(:first) terminal = environment.terminals.try(:first)
if terminal if terminal
set_workhorse_internal_api_content_type set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal) render json: Gitlab::Workhorse.channel_websocket(terminal)
else else
render html: 'Not found', status: :not_found render html: 'Not found', status: :not_found
end end
......
...@@ -157,7 +157,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -157,7 +157,7 @@ class Projects::JobsController < Projects::ApplicationController
# GET .../terminal.ws : implemented in gitlab-workhorse # GET .../terminal.ws : implemented in gitlab-workhorse
def terminal_websocket_authorize def terminal_websocket_authorize
set_workhorse_internal_api_content_type set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification) render json: Gitlab::Workhorse.channel_websocket(@build.terminal_specification)
end end
private private
......
...@@ -16,7 +16,10 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -16,7 +16,10 @@ class Projects::WikisController < Projects::ApplicationController
end end
def pages def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]) @wiki_pages = Kaminari.paginate_array(
@project_wiki.pages(sort: params[:sort], direction: params[:direction])
).page(params[:page])
@wiki_entries = WikiPage.group_by_directory(@wiki_pages) @wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end end
......
...@@ -53,7 +53,6 @@ class IssuableFinder ...@@ -53,7 +53,6 @@ class IssuableFinder
assignee_username assignee_username
author_id author_id
author_username author_username
label_name
milestone_title milestone_title
my_reaction_emoji my_reaction_emoji
search search
......
# frozen_string_literal: true # frozen_string_literal: true
class GitlabSchema < GraphQL::Schema class GitlabSchema < GraphQL::Schema
# Took our current most complicated query in use, issues.graphql,
# with a complexity of 19, and added a 20 point buffer to it.
# These values will evolve over time.
DEFAULT_MAX_COMPLEXITY = 40
AUTHENTICATED_COMPLEXITY = 50
ADMIN_COMPLEXITY = 60
use BatchLoader::GraphQL use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections use Gitlab::Graphql::Connections
use Gitlab::Graphql::Tracing use Gitlab::Graphql::Tracing
query_analyzer Gitlab::Graphql::QueryAnalyzers::LogQueryComplexity.analyzer
query(Types::QueryType) query(Types::QueryType)
default_max_page_size 100 default_max_page_size 100
max_complexity DEFAULT_MAX_COMPLEXITY
mutation(Types::MutationType) mutation(Types::MutationType)
def self.execute(query_str = nil, **kwargs)
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
super(query_str, **kwargs)
end
def self.max_query_complexity(ctx)
current_user = ctx&.fetch(:current_user, nil)
if current_user&.admin
ADMIN_COMPLEXITY
elsif current_user
AUTHENTICATED_COMPLEXITY
else
DEFAULT_MAX_COMPLEXITY
end
end
end end
...@@ -3,5 +3,14 @@ ...@@ -3,5 +3,14 @@
module Types module Types
class BaseField < GraphQL::Schema::Field class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize prepend Gitlab::Graphql::Authorize
DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block)
# complexity is already defaulted to 1, but let's make it explicit
kwargs[:complexity] ||= DEFAULT_COMPLEXITY
super(*args, **kwargs, &block)
end
end end
end end
...@@ -77,7 +77,7 @@ module BlobHelper ...@@ -77,7 +77,7 @@ module BlobHelper
project, project,
ref, ref,
path, path,
label: "Replace", label: _("Replace"),
action: "replace", action: "replace",
btn_class: "default", btn_class: "default",
modal_type: "upload" modal_type: "upload"
...@@ -89,7 +89,7 @@ module BlobHelper ...@@ -89,7 +89,7 @@ module BlobHelper
project, project,
ref, ref,
path, path,
label: "Delete", label: _("Delete"),
action: "delete", action: "delete",
btn_class: "remove", btn_class: "remove",
modal_type: "remove" modal_type: "remove"
...@@ -101,14 +101,14 @@ module BlobHelper ...@@ -101,14 +101,14 @@ module BlobHelper
end end
def leave_edit_message def leave_edit_message
"Leave edit mode?\nAll unsaved changes will be lost." _("Leave edit mode? All unsaved changes will be lost.")
end end
def editing_preview_title(filename) def editing_preview_title(filename)
if Gitlab::MarkupHelper.previewable?(filename) if Gitlab::MarkupHelper.previewable?(filename)
'Preview' _('Preview')
else else
'Preview changes' _('Preview changes')
end end
end end
...@@ -201,14 +201,14 @@ module BlobHelper ...@@ -201,14 +201,14 @@ module BlobHelper
return if blob.empty? return if blob.empty?
return if blob.binary? || blob.stored_externally? return if blob.binary? || blob.stored_externally?
title = 'Open raw' title = _('Open raw')
link_to icon('file-code-o'), blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' } link_to icon('file-code-o'), blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end end
def download_blob_button(blob) def download_blob_button(blob)
return if blob.empty? return if blob.empty?
title = 'Download' title = _('Download')
link_to sprite_icon('download'), blob_raw_path(inline: false), download: @path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' } link_to sprite_icon('download'), blob_raw_path(inline: false), download: @path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end end
......
...@@ -4,12 +4,12 @@ module BuildsHelper ...@@ -4,12 +4,12 @@ module BuildsHelper
def build_summary(build, skip: false) def build_summary(build, skip: false)
if build.has_trace? if build.has_trace?
if skip if skip
link_to "View job trace", pipeline_job_url(build.pipeline, build) link_to _("View job trace"), pipeline_job_url(build.pipeline, build)
else else
build.trace.html(last_lines: 10).html_safe build.trace.html(last_lines: 10).html_safe
end end
else else
"No job trace" _("No job trace")
end end
end end
...@@ -31,7 +31,7 @@ module BuildsHelper ...@@ -31,7 +31,7 @@ module BuildsHelper
def build_failed_issue_options def build_failed_issue_options
{ {
title: "Job Failed ##{@build.id}", title: _("Job Failed #%{build_id}") % { build_id: @build.id },
description: project_job_url(@project, @build) description: project_job_url(@project, @build)
} }
end end
......
...@@ -21,7 +21,7 @@ module ButtonHelper ...@@ -21,7 +21,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage # See http://clipboardjs.com/#usage
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to clipboard' title = data[:title] || _('Copy to clipboard')
button_text = data[:button_text] || '' button_text = data[:button_text] || ''
hide_tooltip = data[:hide_tooltip] || false hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false hide_button_icon = data[:hide_button_icon] || false
......
...@@ -4,8 +4,7 @@ module FormHelper ...@@ -4,8 +4,7 @@ module FormHelper
def form_errors(model, type: 'form') def form_errors(model, type: 'form')
return unless model.errors.any? return unless model.errors.any?
pluralized = 'error'.pluralize(model.errors.count) headline = n_('The %{type} contains the following error:', 'The %{type} contains the following errors:', model.errors.count) % { type: type }
headline = "The #{type} contains the following #{pluralized}:"
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) << content_tag(:h4, headline) <<
...@@ -24,7 +23,7 @@ module FormHelper ...@@ -24,7 +23,7 @@ module FormHelper
title: 'Select assignee', title: 'Select assignee',
filter: true, filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee',
placeholder: 'Search users', placeholder: _('Search users'),
data: { data: {
first_user: current_user&.username, first_user: current_user&.username,
null_user: true, null_user: true,
......
...@@ -181,8 +181,8 @@ module LabelsHelper ...@@ -181,8 +181,8 @@ module LabelsHelper
def label_deletion_confirm_text(label) def label_deletion_confirm_text(label)
case label case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?' when GroupLabel then _('Remove this label? This will affect all projects within the group. Are you sure?')
when ProjectLabel then 'Remove this label? Are you sure?' when ProjectLabel then _('Remove this label? Are you sure?')
end end
end end
......
...@@ -30,7 +30,7 @@ module SearchHelper ...@@ -30,7 +30,7 @@ module SearchHelper
to = collection.offset_value + collection.to_a.size to = collection.offset_value + collection.to_a.size
count = collection.total_count count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\"" s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"") % { from: from, to: to, count: count, scope: scope.humanize(capitalize: false), term: term }
end end
def find_project_for_result_blob(projects, result) def find_project_for_result_blob(projects, result)
...@@ -59,31 +59,31 @@ module SearchHelper ...@@ -59,31 +59,31 @@ module SearchHelper
# Autocomplete results for various settings pages # Autocomplete results for various settings pages
def default_autocomplete def default_autocomplete
[ [
{ category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: _("User settings"), url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: _("SSH Keys"), url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path } { category: "Settings", label: _("Dashboard"), url: root_path }
] ]
end end
# Autocomplete results for settings pages, for admins # Autocomplete results for settings pages, for admins
def default_autocomplete_admin def default_autocomplete_admin
[ [
{ category: "Settings", label: "Admin Section", url: admin_root_path } { category: "Settings", label: _("Admin Section"), url: admin_root_path }
] ]
end end
# Autocomplete results for internal help pages # Autocomplete results for internal help pages
def help_autocomplete def help_autocomplete
[ [
{ category: "Help", label: "API Help", url: help_page_path("api/README") }, { category: "Help", label: _("API Help"), url: help_page_path("api/README") },
{ category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") }, { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
{ category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") }, { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
{ category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") }, { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") }, { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") }, { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") }, { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") }, { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") } { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") }
] ]
end end
...@@ -93,16 +93,16 @@ module SearchHelper ...@@ -93,16 +93,16 @@ module SearchHelper
ref = @ref || @project.repository.root_ref ref = @ref || @project.repository.root_ref
[ [
{ category: "In this project", label: "Files", url: project_tree_path(@project, ref) }, { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
{ category: "In this project", label: "Commits", url: project_commits_path(@project, ref) }, { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) },
{ category: "In this project", label: "Network", url: project_network_path(@project, ref) }, { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
{ category: "In this project", label: "Graph", url: project_graph_path(@project, ref) }, { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) },
{ category: "In this project", label: "Issues", url: project_issues_path(@project) }, { category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
{ category: "In this project", label: "Merge Requests", url: project_merge_requests_path(@project) }, { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) },
{ category: "In this project", label: "Milestones", url: project_milestones_path(@project) }, { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
{ category: "In this project", label: "Snippets", url: project_snippets_path(@project) }, { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
{ category: "In this project", label: "Members", url: project_project_members_path(@project) }, { category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
{ category: "In this project", label: "Wiki", url: project_wikis_path(@project) } { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
] ]
else else
[] []
...@@ -162,7 +162,7 @@ module SearchHelper ...@@ -162,7 +162,7 @@ module SearchHelper
opts = opts =
{ {
id: "filtered-search-#{type}", id: "filtered-search-#{type}",
placeholder: 'Search or filter results...', placeholder: _('Search or filter results...'),
data: { data: {
'username-params' => UserSerializer.new.represent(@users) 'username-params' => UserSerializer.new.represent(@users)
}, },
......
...@@ -86,17 +86,17 @@ module TreeHelper ...@@ -86,17 +86,17 @@ module TreeHelper
end end
def edit_in_new_fork_notice_now def edit_in_new_fork_notice_now
"You're not allowed to make changes to this project directly." + _("You're not allowed to make changes to this project directly. "\
" A fork of this project is being created that you can make changes in, so you can submit a merge request." "A fork of this project is being created that you can make changes in, so you can submit a merge request.")
end end
def edit_in_new_fork_notice def edit_in_new_fork_notice
"You're not allowed to make changes to this project directly." + _("You're not allowed to make changes to this project directly. "\
" A fork of this project has been created that you can make changes in, so you can submit a merge request." "A fork of this project has been created that you can make changes in, so you can submit a merge request.")
end end
def edit_in_new_fork_notice_action(action) def edit_in_new_fork_notice_action(action)
edit_in_new_fork_notice + " Try to #{action} this file again." edit_in_new_fork_notice + _(" Try to %{action} this file again.") % { action: action }
end end
def commit_in_fork_help def commit_in_fork_help
......
...@@ -42,11 +42,11 @@ module VisibilityLevelHelper ...@@ -42,11 +42,11 @@ module VisibilityLevelHelper
def group_visibility_level_description(level) def group_visibility_level_description(level)
case level case level
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
"The group and its projects can only be viewed by members." _("The group and its projects can only be viewed by members.")
when Gitlab::VisibilityLevel::INTERNAL when Gitlab::VisibilityLevel::INTERNAL
"The group and any internal projects can be viewed by any logged in user." _("The group and any internal projects can be viewed by any logged in user.")
when Gitlab::VisibilityLevel::PUBLIC when Gitlab::VisibilityLevel::PUBLIC
"The group and any public projects can be viewed without any authentication." _("The group and any public projects can be viewed without any authentication.")
end end
end end
...@@ -54,20 +54,20 @@ module VisibilityLevelHelper ...@@ -54,20 +54,20 @@ module VisibilityLevelHelper
case level case level
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
if snippet.is_a? ProjectSnippet if snippet.is_a? ProjectSnippet
"The snippet is visible only to project members." _("The snippet is visible only to project members.")
else else
"The snippet is visible only to me." _("The snippet is visible only to me.")
end end
when Gitlab::VisibilityLevel::INTERNAL when Gitlab::VisibilityLevel::INTERNAL
"The snippet is visible to any logged in user." _("The snippet is visible to any logged in user.")
when Gitlab::VisibilityLevel::PUBLIC when Gitlab::VisibilityLevel::PUBLIC
"The snippet can be accessed without any authentication." _("The snippet can be accessed without any authentication.")
end end
end end
def restricted_visibility_level_description(level) def restricted_visibility_level_description(level)
level_name = Gitlab::VisibilityLevel.level_name(level) level_name = Gitlab::VisibilityLevel.level_name(level)
"#{level_name.capitalize} visibility has been restricted by the administrator." _("%{level_name} visibility has been restricted by the administrator.") % { level_name: level_name.capitalize }
end end
def disallowed_visibility_level_description(level, form_model) def disallowed_visibility_level_description(level, form_model)
......
...@@ -47,4 +47,24 @@ module WikiHelper ...@@ -47,4 +47,24 @@ module WikiHelper
def wiki_attachment_upload_url def wiki_attachment_upload_url
expose_url(api_v4_projects_wikis_attachments_path(id: @project.id)) expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
end end
def wiki_sort_controls(project, sort, direction)
sort ||= ProjectWiki::TITLE_ORDER
link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
link_to(project_wikis_pages_path(project, sort: sort, direction: reversed_direction),
type: 'button', class: link_class, title: _('Sort direction')) do
sprite_icon("sort-#{icon_class}", size: 16)
end
end
def wiki_sort_title(key)
if key == ProjectWiki::CREATED_AT_ORDER
s_("Wiki|Created date")
else
s_("Wiki|Title")
end
end
end end
...@@ -83,8 +83,13 @@ module Ci ...@@ -83,8 +83,13 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do scope :with_artifacts_archive, ->() do
if Feature.enabled?(:ci_enable_legacy_artifacts)
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
else
where('EXISTS (?)',
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
end end
scope :with_existing_job_artifacts, ->(query) do scope :with_existing_job_artifacts, ->(query) do
...@@ -135,6 +140,8 @@ module Ci ...@@ -135,6 +140,8 @@ module Ci
where("EXISTS (?)", matcher) where("EXISTS (?)", matcher)
end end
##
# TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
...@@ -775,7 +782,7 @@ module Ci ...@@ -775,7 +782,7 @@ module Ci
private private
def erase_old_artifacts! def erase_old_artifacts!
# TODO: To be removed once we get rid of # TODO: To be removed once we get rid of ci_enable_legacy_artifacts feature flag
remove_artifacts_file! remove_artifacts_file!
remove_artifacts_metadata! remove_artifacts_metadata!
save save
......
...@@ -6,6 +6,8 @@ module Ci ...@@ -6,6 +6,8 @@ module Ci
class BuildRunnerSession < ApplicationRecord class BuildRunnerSession < ApplicationRecord
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'.freeze
self.table_name = 'ci_builds_runner_session' self.table_name = 'ci_builds_runner_session'
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
...@@ -14,11 +16,21 @@ module Ci ...@@ -14,11 +16,21 @@ module Ci
validates :url, url: { protocols: %w(https) } validates :url, url: { protocols: %w(https) }
def terminal_specification def terminal_specification
return {} unless url.present? wss_url = Gitlab::UrlHelpers.as_wss(self.url)
return {} unless wss_url.present?
wss_url = "#{wss_url}/exec"
channel_specification(wss_url, TERMINAL_SUBPROTOCOL)
end
private
def channel_specification(url, subprotocol)
return {} if subprotocol.blank? || url.blank?
{ {
subprotocols: ['terminal.gitlab.com'].freeze, subprotocols: Array(subprotocol),
url: "#{url}/exec".sub("https://", "wss://"), url: url,
headers: { Authorization: [authorization.presence] }.compact, headers: { Authorization: [authorization.presence] }.compact,
ca_pem: certificate.presence ca_pem: certificate.presence
} }
......
...@@ -13,7 +13,7 @@ module ArtifactMigratable ...@@ -13,7 +13,7 @@ module ArtifactMigratable
end end
def artifacts? def artifacts?
!artifacts_expired? && artifacts_file.exists? !artifacts_expired? && artifacts_file&.exists?
end end
def artifacts_metadata? def artifacts_metadata?
...@@ -43,4 +43,16 @@ module ArtifactMigratable ...@@ -43,4 +43,16 @@ module ArtifactMigratable
def artifacts_size def artifacts_size
read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
end end
def legacy_artifacts_file
return unless Feature.enabled?(:ci_enable_legacy_artifacts)
super
end
def legacy_artifacts_metadata
return unless Feature.enabled?(:ci_enable_legacy_artifacts)
super
end
end end
...@@ -29,6 +29,40 @@ ...@@ -29,6 +29,40 @@
# However, it will enqueue a background worker to call `#calculate_reactive_cache` # However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes. # and set an initial cache lifetime of ten minutes.
# #
# The background worker needs to find or generate the object on which
# `with_reactive_cache` was called.
# The default behaviour can be overridden by defining a custom
# `reactive_cache_worker_finder`.
# Otherwise the background worker will use the class name and primary key to get
# the object using the ActiveRecord find_by method.
#
# class Bar
# include ReactiveCaching
#
# self.reactive_cache_key = ->() { ["bar", "thing"] }
# self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
#
# def self.from_cache(var1, var2)
# # This method will be called by the background worker with "bar1" and
# # "bar2" as arguments.
# new(var1, var2)
# end
#
# def initialize(var1, var2)
# # ...
# end
#
# def calculate_reactive_cache
# # Expensive operation here. The return value of this method is cached
# end
#
# def result
# with_reactive_cache("bar1", "bar2") do |data|
# # ...
# end
# end
# end
#
# Each time the background job completes, it stores the return value of # Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after # `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date. # `reactive_cache_refresh_interval`, so keeping the stored value up to date.
...@@ -52,6 +86,7 @@ module ReactiveCaching ...@@ -52,6 +86,7 @@ module ReactiveCaching
class_attribute :reactive_cache_key class_attribute :reactive_cache_key
class_attribute :reactive_cache_lifetime class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_refresh_interval class_attribute :reactive_cache_refresh_interval
class_attribute :reactive_cache_worker_finder
# defaults # defaults
self.reactive_cache_lease_timeout = 2.minutes self.reactive_cache_lease_timeout = 2.minutes
...@@ -59,6 +94,10 @@ module ReactiveCaching ...@@ -59,6 +94,10 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
def calculate_reactive_cache(*args) def calculate_reactive_cache(*args)
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -13,6 +13,11 @@ class ProjectWiki ...@@ -13,6 +13,11 @@ class ProjectWiki
CouldNotCreateWikiError = Class.new(StandardError) CouldNotCreateWikiError = Class.new(StandardError)
SIDEBAR = '_sidebar' SIDEBAR = '_sidebar'
TITLE_ORDER = 'title'
CREATED_AT_ORDER = 'created_at'
DIRECTION_DESC = 'desc'
DIRECTION_ASC = 'asc'
# Returns a string describing what went wrong after # Returns a string describing what went wrong after
# an operation fails. # an operation fails.
attr_reader :error_message attr_reader :error_message
...@@ -82,8 +87,15 @@ class ProjectWiki ...@@ -82,8 +87,15 @@ class ProjectWiki
# Returns an Array of GitLab WikiPage instances or an # Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages. # empty Array if this Wiki has no pages.
def pages(limit: 0) def pages(limit: 0, sort: nil, direction: DIRECTION_ASC)
wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) } sort ||= TITLE_ORDER
direction_desc = direction == DIRECTION_DESC
wiki.pages(
limit: limit, sort: sort, direction_desc: direction_desc
).map do |page|
WikiPage.new(self, page, true)
end
end end
# Finds a page within the repository based on a tile # Finds a page within the repository based on a tile
......
...@@ -28,16 +28,15 @@ class WikiPage ...@@ -28,16 +28,15 @@ class WikiPage
def self.group_by_directory(pages) def self.group_by_directory(pages)
return [] if pages.blank? return [] if pages.blank?
pages.sort_by { |page| [page.directory, page.slug] } pages.each_with_object([]) do |page, grouped_pages|
.group_by(&:directory) next grouped_pages << page unless page.directory.present?
.map do |dir, pages|
if dir.present? directory = grouped_pages.find { |dir| dir.slug == page.directory }
WikiDirectory.new(dir, pages)
else next directory.pages << page if directory
pages
end grouped_pages << WikiDirectory.new(page.directory, [page])
end end
.flatten
end end
def self.unhyphenize(name) def self.unhyphenize(name)
......
# frozen_string_literal: true # frozen_string_literal: true
##
# TODO: Remove this uploader when we remove :ci_enable_legacy_artifacts feature flag
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/58595
class LegacyArtifactUploader < GitlabUploader class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
...@@ -7,25 +7,27 @@ ...@@ -7,25 +7,27 @@
- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon } - help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
%p %p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - link_to_help_page = link_to(s_('ClusterIntegration|help page'),
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page }
%p= link_to('Select a different Google account', @authorize_url) %p= link_to('Select a different Google account', @authorize_url)
= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| = bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20',
= form_errors(@gcp_cluster) data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
.form-group = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- if has_multiple_clusters? - if has_multiple_clusters?
.form-group = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' class: 'label-bold' } do
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, required: true, class: 'form-control',
title: 'Environment scope is required.', wrapper: false
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group .form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-bold' = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'),
class: 'label-bold'
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id = provider_gcp_field.hidden_field :gcp_project_id
.dropdown .dropdown
...@@ -47,9 +49,9 @@ ...@@ -47,9 +49,9 @@
%p.form-text.text-muted %p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end } = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
.form-group = provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3',
= provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-bold' title: s_('ClusterIntegration|Number of nodes must be a numerical value.'),
= provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold'
.form-group .form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold' = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold'
...@@ -64,13 +66,14 @@ ...@@ -64,13 +66,14 @@
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group .form-group
.form-check = provider_gcp_field.check_box :legacy_abac, { label: s_('ClusterIntegration|RBAC-enabled cluster'),
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true label_class: 'label-bold' }, false, true
= provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.form-text.text-muted .form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' = link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
.form-group .form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true = field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field| = bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' },
= form_errors(@user_cluster) url: clusterable.create_user_clusters_path, as: :cluster do |field|
.form-group = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- if has_multiple_clusters? - if has_multiple_clusters?
.form-group = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' class: 'label-bold' } do
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, required: true,
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") title: 'Environment scope is required.', wrapper: false
.form-text.text-muted
= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group = platform_kubernetes_field.url_field :api_url, required: true,
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold' title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') label: s_('ClusterIntegration|API URL'), label_class: 'label-bold'
= platform_kubernetes_field.text_area :ca_cert,
.form-group placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
= platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold' label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') = platform_kubernetes_field.text_field :token, required: true,
title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'),
.form-group autocomplete: 'off', label_class: 'label-bold'
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
- if @user_cluster.allow_user_defined_namespace? - if @user_cluster.allow_user_defined_namespace?
.form-group = platform_kubernetes_field.text_field :namespace,
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group = platform_kubernetes_field.form_group :authorization_type do
.form-check = platform_kubernetes_field.check_box :authorization_type,
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' label_class: 'label-bold', inline: true }, 'rbac', 'abac'
.form-text.text-muted .form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' = link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
.form-group .form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
= form_for cluster, url: update_cluster_url_path, as: :cluster do |field| = bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' },
= form_errors(cluster) as: :cluster do |field|
- copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
.form-group class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
- if cluster.read_only_kubernetes_platform_fields? = field.text_field :name, class: 'js-select-on-focus cluster-name', required: true,
%label.append-bottom-10{ for: 'cluster-name' } title: s_('ClusterIntegration|Cluster name is required.'),
= s_('ClusterIntegration|Kubernetes cluster name') readonly: cluster.read_only_kubernetes_platform_fields?,
.input-group label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold',
%input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true } input_group_class: 'gl-field-error-anchor', append: copy_name_btn
%span.input-group-append
= clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
- else
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
.input-group
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= field.fields_for :platform_kubernetes, platform do |platform_field| = field.fields_for :platform_kubernetes, platform do |platform_field|
.form-group - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
= platform_field.label :api_url, s_('ClusterIntegration|API URL') class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
.input-group = platform_field.text_field :api_url, class: 'js-select-on-focus', required: true,
= platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.read_only_kubernetes_platform_fields? title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- if cluster.read_only_kubernetes_platform_fields? readonly: cluster.read_only_kubernetes_platform_fields?,
%span.input-group-append label: s_('ClusterIntegration|API URL'), label_class: 'label-bold',
= clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default') input_group_class: 'gl-field-error-anchor', append: copy_api_url
.form-group - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
= platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
.input-group = platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5',
= platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.read_only_kubernetes_platform_fields? readonly: cluster.read_only_kubernetes_platform_fields?,
- if cluster.read_only_kubernetes_platform_fields? placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
%span.input-group-append.clipboard-addon label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
= clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank') input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn
.form-group - show_token_btn = (platform_field.button s_('ClusterIntegration|Show'),
= platform_field.label :token, s_('ClusterIntegration|Token') type: 'button', class: 'js-show-cluster-token btn btn-default')
.input-group - copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'),
= platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.read_only_kubernetes_platform_fields? class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
%span.input-group-append
%button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' } = platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
= s_('ClusterIntegration|Show') required: true, title: s_('ClusterIntegration|Service token is required.'),
- if cluster.read_only_kubernetes_platform_fields? readonly: cluster.read_only_kubernetes_platform_fields?,
= clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') label: s_('ClusterIntegration|Service Token'), label_class: 'label-bold',
input_group_class: 'gl-field-error-anchor', append: show_token_btn + copy_token_btn
- if cluster.allow_user_defined_namespace? - if cluster.allow_user_defined_namespace?
.form-group = platform_field.text_field :namespace, label: s_('ClusterIntegration|Project namespace (optional, unique)'),
= platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') label_class: 'label-bold'
= platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group = platform_field.form_group :authorization_type do
.form-check = platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'),
= platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' label_class: 'label-bold', inline: true }, 'rbac', 'abac'
= platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.form-text.text-muted .form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
This Route Map is invalid: This Route Map is invalid:
= viewer.validation_message = viewer.validation_message
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') = link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages')
= icon('spinner spin fw') = icon('spinner spin fw')
Validating Route Map… Validating Route Map…
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') = link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages')
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home) - add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
- breadcrumb_title s_("Wiki|Pages") - breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki") - page_title s_("Wiki|Pages"), _("Wiki")
- sort_title = wiki_sort_title(params[:sort])
%div{ class: container_class } %div{ class: container_class }
.wiki-page-header .wiki-page-header
...@@ -15,6 +16,18 @@ ...@@ -15,6 +16,18 @@
= icon('cloud-download') = icon('cloud-download')
= _("Clone repository") = _("Clone repository")
.dropdown.inline.wiki-sort-dropdown
.btn-group{ role: 'group' }
.btn-group{ role: 'group' }
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
= sort_title
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(s_("Wiki|Title"), project_wikis_pages_path(@project, sort: ProjectWiki::TITLE_ORDER), sort_title)
= sortable_item(s_("Wiki|Created date"), project_wikis_pages_path(@project, sort: ProjectWiki::CREATED_AT_ORDER), sort_title)
= wiki_sort_controls(@project, params[:sort], params[:direction])
%ul.wiki-pages-list.content-list %ul.wiki-pages-list.content-list
= render @wiki_entries, context: 'pages' = render @wiki_entries, context: 'pages'
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
class ReactiveCachingWorker class ReactiveCachingWorker
include ApplicationWorker include ApplicationWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(class_name, id, *args) def perform(class_name, id, *args)
klass = begin klass = begin
class_name.constantize class_name.constantize
...@@ -12,7 +11,9 @@ class ReactiveCachingWorker ...@@ -12,7 +11,9 @@ class ReactiveCachingWorker
end end
return unless klass return unless klass
klass.find_by(klass.primary_key => id).try(:exclusively_update_reactive_cache!, *args) klass
.reactive_cache_worker_finder
.call(id, *args)
.try(:exclusively_update_reactive_cache!, *args)
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
---
title: Display cluster form validation error messages inline
merge_request: 26502
author:
type: changed
---
title: Allow reactive caching to be used in services
merge_request: 26839
author:
type: added
---
title: Add initial complexity limits to GraphQL queries
merge_request: 26629
author:
type: performance
---
title: Automatically set Prometheus step interval
merge_request: 26441
author:
type: changed
---
title: Allow to use untrusted Regexp via feature flag
merge_request: 26905
author:
type: deprecated
---
title: Drop legacy artifacts usage as there are no leftovers
merge_request: 24294
author:
type: performance
---
title: Fix UI anchor links after docs refactor
merge_request: 26890
author:
type: fixed
---
title: Allow to sort wiki pages by date and title
merge_request: 25365
author:
type: added
---
title: Update GitLab Shell to v9.0.0
merge_request: 27002
author:
type: other
---
title: Group transfer now properly redirects to edit on failure
merge_request: 26837
author:
type: fixed
...@@ -362,6 +362,10 @@ configuring a different storage driver. By default the GitLab Container Registry ...@@ -362,6 +362,10 @@ configuring a different storage driver. By default the GitLab Container Registry
is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path) is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path)
configuration. configuration.
NOTE: **Note:** Enabling a storage driver other than `filesystem` would mean
that your Docker client needs to be able to access the storage backend directly.
In that case, you must use an address that resolves and is accessible outside GitLab server.
The different supported drivers are: The different supported drivers are:
| Driver | Description | | Driver | Description |
...@@ -369,20 +373,16 @@ The different supported drivers are: ...@@ -369,20 +373,16 @@ The different supported drivers are:
| filesystem | Uses a path on the local filesystem | | filesystem | Uses a path on the local filesystem |
| azure | Microsoft Azure Blob Storage | | azure | Microsoft Azure Blob Storage |
| gcs | Google Cloud Storage | | gcs | Google Cloud Storage |
| s3 | Amazon Simple Storage Service | | s3 | Amazon Simple Storage Service. Be sure to configure your storage bucket with the correct [S3 Permission Scopes](https://docs.docker.com/registry/storage-drivers/s3/#s3-permission-scopes). |
| swift | OpenStack Swift Object Storage | | swift | OpenStack Swift Object Storage |
| oss | Aliyun OSS | | oss | Aliyun OSS |
Read more about the individual driver's config options in the Read more about the individual driver's config options in the
[Docker Registry docs][storage-config]. [Docker Registry docs][storage-config].
> **Warning** GitLab will not backup Docker images that are not stored on the CAUTION: **Warning:** GitLab will not backup Docker images that are not stored on the
filesystem. Remember to enable backups with your object storage provider if filesystem. Remember to enable backups with your object storage provider if
desired. desired.
>
> **Important** Enabling storage driver other than `filesystem` would mean
that your Docker client needs to be able to access the storage backend directly.
So you must use an address that resolves and is accessible outside GitLab server.
--- ---
......
...@@ -414,6 +414,27 @@ job: ...@@ -414,6 +414,27 @@ job:
only: ['branches', 'tags'] only: ['branches', 'tags']
``` ```
### Supported `only`/`except` regexp syntax
CAUTION: **Warning:**
This is a breaking change that was introduced with GitLab 11.9.4.
In GitLab 11.9.4, GitLab begun internally converting regexp used
in `only` and `except` parameters to [RE2](https://github.com/google/re2/wiki/Syntax).
This means that only subset of features provided by [Ruby Regexp](https://ruby-doc.org/core/Regexp.html)
is supported. [RE2](https://github.com/google/re2/wiki/Syntax) limits the set of features
provided due to computational complexity, which means some features became unavailable in GitLab 11.9.4.
For example, negative lookaheads.
For GitLab versions from 11.9.7 and up to GitLab 12.0, GitLab provides a feature flag that can be
enabled by administrators that allows users to use unsafe regexp syntax. This brings compatibility
with previously allowed syntax version and allows users to gracefully migrate to the new syntax.
```ruby
Feature.enable(:allow_unsafe_ruby_regexp)
```
### `only`/`except` (advanced) ### `only`/`except` (advanced)
> - `refs` and `kubernetes` policies introduced in GitLab 10.0. > - `refs` and `kubernetes` policies introduced in GitLab 10.0.
......
...@@ -130,9 +130,9 @@ secure note named **gitlab-{ce,ee} Review App's root password**. ...@@ -130,9 +130,9 @@ secure note named **gitlab-{ce,ee} Review App's root password**.
1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`. 1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`.
1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`. 1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`. 1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
1. Replace `-c task-runner -- ls` with `-- /srv/gitlab/bin/rails c` from the 1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
default command or default command or
- Run `kubectl exec --namespace review-apps-ce -it review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -- /srv/gitlab/bin/rails c` - Run `kubectl exec --namespace review-apps-ce review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console`
and and
- Replace `review-apps-ce` with `review-apps-ee` if the Review App - Replace `review-apps-ce` with `review-apps-ee` if the Review App
is running EE, and is running EE, and
......
...@@ -35,7 +35,7 @@ module Gitlab ...@@ -35,7 +35,7 @@ module Gitlab
# patterns can be matched only when branch or tag is used # patterns can be matched only when branch or tag is used
# the pattern matching does not work for merge requests pipelines # the pattern matching does not work for merge requests pipelines
if pipeline.branch? || pipeline.tag? if pipeline.branch? || pipeline.tag?
if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern) if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern, fallback: true)
regexp.match?(pipeline.ref) regexp.match?(pipeline.ref)
else else
pattern == pipeline.ref pattern == pipeline.ref
......
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
validations do validations do
validates :config, array_of_strings_or_regexps: true validates :config, array_of_strings_or_regexps_with_fallback: true
end end
def value def value
...@@ -38,7 +38,7 @@ module Gitlab ...@@ -38,7 +38,7 @@ module Gitlab
validate :variables_expressions_syntax validate :variables_expressions_syntax
with_options allow_nil: true do with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true validates :refs, array_of_strings_or_regexps_with_fallback: true
validates :kubernetes, allowed_values: %w[active] validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true validates :variables, array_of_strings: true
validates :changes, array_of_strings: true validates :changes, array_of_strings: true
......
...@@ -129,6 +129,12 @@ module Gitlab ...@@ -129,6 +129,12 @@ module Gitlab
end end
end end
protected
def fallback
false
end
private private
def matches_syntax?(value) def matches_syntax?(value)
...@@ -137,7 +143,7 @@ module Gitlab ...@@ -137,7 +143,7 @@ module Gitlab
def validate_regexp(value) def validate_regexp(value)
matches_syntax?(value) && matches_syntax?(value) &&
Gitlab::UntrustedRegexp::RubySyntax.valid?(value) Gitlab::UntrustedRegexp::RubySyntax.valid?(value, fallback: fallback)
end end
end end
...@@ -162,6 +168,14 @@ module Gitlab ...@@ -162,6 +168,14 @@ module Gitlab
end end
end end
class ArrayOfStringsOrRegexpsWithFallbackValidator < ArrayOfStringsOrRegexpsValidator
protected
def fallback
true
end
end
class ArrayOfStringsOrStringValidator < RegexpValidator class ArrayOfStringsOrStringValidator < RegexpValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless validate_array_of_strings_or_string(value) unless validate_array_of_strings_or_string(value)
......
...@@ -47,7 +47,7 @@ module Gitlab ...@@ -47,7 +47,7 @@ module Gitlab
user: build.user.try(:hook_attrs), user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner), runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: { artifacts_file: {
filename: build.artifacts_file.filename, filename: build.artifacts_file&.filename,
size: build.artifacts_size size: build.artifacts_size
} }
} }
......
...@@ -86,9 +86,9 @@ module Gitlab ...@@ -86,9 +86,9 @@ module Gitlab
end end
end end
def pages(limit: 0) def pages(limit: 0, sort: nil, direction_desc: false)
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_get_all_pages(limit: limit) gitaly_get_all_pages(limit: limit, sort: sort, direction_desc: direction_desc)
end end
end end
...@@ -168,8 +168,10 @@ module Gitlab ...@@ -168,8 +168,10 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file) Gitlab::Git::WikiFile.new(wiki_file)
end end
def gitaly_get_all_pages(limit: 0) def gitaly_get_all_pages(limit: 0, sort: nil, direction_desc: false)
gitaly_wiki_client.get_all_pages(limit: limit).map do |wiki_page, version| gitaly_wiki_client.get_all_pages(
limit: limit, sort: sort, direction_desc: direction_desc
).map do |wiki_page, version|
Gitlab::Git::WikiPage.new(wiki_page, version) Gitlab::Git::WikiPage.new(wiki_page, version)
end end
end end
......
...@@ -87,8 +87,13 @@ module Gitlab ...@@ -87,8 +87,13 @@ module Gitlab
wiki_page_from_iterator(response) wiki_page_from_iterator(response)
end end
def get_all_pages(limit: 0) def get_all_pages(limit: 0, sort: nil, direction_desc: false)
request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo, limit: limit) sort_value = Gitaly::WikiGetAllPagesRequest::SortBy.resolve(sort.to_s.upcase.to_sym)
params = { repository: @gitaly_repo, limit: limit, direction_desc: direction_desc }
params[:sort] = sort_value if sort_value
request = Gitaly::WikiGetAllPagesRequest.new(params)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout) response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout)
pages = [] pages = []
......
...@@ -14,9 +14,10 @@ module Gitlab ...@@ -14,9 +14,10 @@ module Gitlab
end end
def authorized_resolve def authorized_resolve
proc do |obj, args, ctx| proc do |parent_typed_object, args, ctx|
resolved_obj = @old_resolve_proc.call(obj, args, ctx) resolved_obj = @old_resolve_proc.call(parent_typed_object, args, ctx)
checker = build_checker(ctx[:current_user]) authorizing_obj = authorize_against(parent_typed_object)
checker = build_checker(ctx[:current_user], authorizing_obj)
if resolved_obj.respond_to?(:then) if resolved_obj.respond_to?(:then)
resolved_obj.then(&checker) resolved_obj.then(&checker)
...@@ -51,22 +52,28 @@ module Gitlab ...@@ -51,22 +52,28 @@ module Gitlab
Array.wrap(@field.metadata[:authorize]) Array.wrap(@field.metadata[:authorize])
end end
def build_checker(current_user) # If it's a built-in/scalar type, authorize using its parent object.
lambda do |value| # nil means authorize using the resolved object
def authorize_against(parent_typed_object)
parent_typed_object.object if built_in_type? && parent_typed_object.respond_to?(:object)
end
def build_checker(current_user, authorizing_obj)
lambda do |resolved_obj|
# Load the elements if they were not loaded by BatchLoader yet # Load the elements if they were not loaded by BatchLoader yet
value = value.sync if value.respond_to?(:sync) resolved_obj = resolved_obj.sync if resolved_obj.respond_to?(:sync)
check = lambda do |object| check = lambda do |object|
authorizations.all? do |ability| authorizations.all? do |ability|
Ability.allowed?(current_user, ability, object) Ability.allowed?(current_user, ability, authorizing_obj || object)
end end
end end
case value case resolved_obj
when Array, ActiveRecord::Relation when Array, ActiveRecord::Relation
value.select(&check) resolved_obj.select(&check)
else else
value if check.call(value) resolved_obj if check.call(resolved_obj)
end end
end end
end end
...@@ -88,6 +95,10 @@ module Gitlab ...@@ -88,6 +95,10 @@ module Gitlab
def node_type_for_basic_connection(type) def node_type_for_basic_connection(type)
type.unwrap type.unwrap
end end
def built_in_type?
GraphQL::Schema::BUILT_IN_TYPES.has_value?(node_type_for_basic_connection(@field.type))
end
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Graphql
module QueryAnalyzers
class LogQueryComplexity
class << self
def analyzer
GraphQL::Analysis::QueryComplexity.new do |query, complexity|
# temporary until https://gitlab.com/gitlab-org/gitlab-ce/issues/59587
Rails.logger.info("[GraphQL Query Complexity] #{complexity} | admin? #{query.context[:current_user]&.admin?}")
end
end
end
end
end
end
end
...@@ -6,6 +6,14 @@ module Gitlab ...@@ -6,6 +6,14 @@ module Gitlab
Error = Class.new(StandardError) Error = Class.new(StandardError)
QueryError = Class.new(Gitlab::PrometheusClient::Error) QueryError = Class.new(Gitlab::PrometheusClient::Error)
# Target number of data points for `query_range`.
# Please don't exceed the limit of 11000 data points
# See https://github.com/prometheus/prometheus/blob/91306bdf24f5395e2601773316945a478b4b263d/web/api/v1/api.go#L347
QUERY_RANGE_DATA_POINTS = 600
# Minimal value of the `step` parameter for `query_range` in seconds.
QUERY_RANGE_MIN_STEP = 60
attr_reader :rest_client, :headers attr_reader :rest_client, :headers
def initialize(rest_client) def initialize(rest_client)
...@@ -23,12 +31,18 @@ module Gitlab ...@@ -23,12 +31,18 @@ module Gitlab
end end
def query_range(query, start: 8.hours.ago, stop: Time.now) def query_range(query, start: 8.hours.ago, stop: Time.now)
start = start.to_f
stop = stop.to_f
step = self.class.compute_step(start, stop)
get_result('matrix') do get_result('matrix') do
json_api_get('query_range', json_api_get(
'query_range',
query: query, query: query,
start: start.to_f, start: start,
end: stop.to_f, end: stop,
step: 1.minute.to_i) step: step
)
end end
end end
...@@ -40,6 +54,14 @@ module Gitlab ...@@ -40,6 +54,14 @@ module Gitlab
json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f) json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f)
end end
def self.compute_step(start, stop)
diff = stop - start
step = (diff / QUERY_RANGE_DATA_POINTS).ceil
[QUERY_RANGE_MIN_STEP, step].max
end
private private
def json_api_get(type, args = {}) def json_api_get(type, args = {})
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
# and converts that to RE2 representation: # and converts that to RE2 representation:
# /<regexp>/<flags> # /<regexp>/<flags>
class RubySyntax class RubySyntax
PATTERN = %r{^/(?<regexp>.+)/(?<flags>[ismU]*)$}.freeze PATTERN = %r{^/(?<regexp>.*)/(?<flags>[ismU]*)$}.freeze
# Checks if pattern matches a regexp pattern # Checks if pattern matches a regexp pattern
# but does not enforce it's validity # but does not enforce it's validity
...@@ -16,28 +16,47 @@ module Gitlab ...@@ -16,28 +16,47 @@ module Gitlab
# The regexp can match the pattern `/.../`, but may not be fabricatable: # The regexp can match the pattern `/.../`, but may not be fabricatable:
# it can be invalid or incomplete: `/match ( string/` # it can be invalid or incomplete: `/match ( string/`
def self.valid?(pattern) def self.valid?(pattern, fallback: false)
!!self.fabricate(pattern) !!self.fabricate(pattern, fallback: fallback)
end end
def self.fabricate(pattern) def self.fabricate(pattern, fallback: false)
self.fabricate!(pattern) self.fabricate!(pattern, fallback: fallback)
rescue RegexpError rescue RegexpError
nil nil
end end
def self.fabricate!(pattern) def self.fabricate!(pattern, fallback: false)
raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String) raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String)
matches = pattern.match(PATTERN) matches = pattern.match(PATTERN)
raise RegexpError, 'Invalid regular expression!' if matches.nil? raise RegexpError, 'Invalid regular expression!' if matches.nil?
expression = matches[:regexp] begin
flags = matches[:flags] create_untrusted_regexp(matches[:regexp], matches[:flags])
expression.prepend("(?#{flags})") if flags.present? rescue RegexpError
raise unless fallback &&
Feature.enabled?(:allow_unsafe_ruby_regexp, default_enabled: false)
create_ruby_regexp(matches[:regexp], matches[:flags])
end
end
def self.create_untrusted_regexp(pattern, flags)
pattern.prepend("(?#{flags})") if flags.present?
UntrustedRegexp.new(pattern, multiline: false)
end
private_class_method :create_untrusted_regexp
def self.create_ruby_regexp(pattern, flags)
options = 0
options += Regexp::IGNORECASE if flags&.include?('i')
options += Regexp::MULTILINE if flags&.include?('m')
UntrustedRegexp.new(expression, multiline: false) Regexp.new(pattern, options)
end end
private_class_method :create_ruby_regexp
end end
end end
end end
# frozen_string_literal: true
module Gitlab
class UrlHelpers
WSS_PROTOCOL = "wss".freeze
def self.as_wss(url)
return unless url.present?
URI.parse(url).tap do |uri|
uri.scheme = WSS_PROTOCOL
end.to_s
rescue URI::InvalidURIError
nil
end
end
end
...@@ -162,16 +162,16 @@ module Gitlab ...@@ -162,16 +162,16 @@ module Gitlab
] ]
end end
def terminal_websocket(terminal) def channel_websocket(channel)
details = { details = {
'Terminal' => { 'Channel' => {
'Subprotocols' => terminal[:subprotocols], 'Subprotocols' => channel[:subprotocols],
'Url' => terminal[:url], 'Url' => channel[:url],
'Header' => terminal[:headers], 'Header' => channel[:headers],
'MaxSessionTime' => terminal[:max_session_time] 'MaxSessionTime' => channel[:max_session_time]
} }
} }
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.key?(:ca_pem) details['Channel']['CAPem'] = channel[:ca_pem] if channel.key?(:ca_pem)
details details
end end
......
...@@ -19,6 +19,9 @@ msgstr "" ...@@ -19,6 +19,9 @@ msgstr ""
msgid " Status" msgid " Status"
msgstr "" msgstr ""
msgid " Try to %{action} this file again."
msgstr ""
msgid " You need to do this before %{grace_period_deadline}." msgid " You need to do this before %{grace_period_deadline}."
msgstr "" msgstr ""
...@@ -126,6 +129,9 @@ msgstr "" ...@@ -126,6 +129,9 @@ msgstr ""
msgid "%{label_for_message} unavailable" msgid "%{label_for_message} unavailable"
msgstr "" msgstr ""
msgid "%{level_name} visibility has been restricted by the administrator."
msgstr ""
msgid "%{link_start}Read more%{link_end} about role permissions" msgid "%{link_start}Read more%{link_end} about role permissions"
msgstr "" msgstr ""
...@@ -351,6 +357,9 @@ msgstr "" ...@@ -351,6 +357,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option" msgid "A user with write access to the source branch selected this option"
msgstr "" msgstr ""
msgid "API Help"
msgstr ""
msgid "About GitLab" msgid "About GitLab"
msgstr "" msgstr ""
...@@ -480,6 +489,9 @@ msgstr "" ...@@ -480,6 +489,9 @@ msgstr ""
msgid "Admin Overview" msgid "Admin Overview"
msgstr "" msgstr ""
msgid "Admin Section"
msgstr ""
msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered." msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr "" msgstr ""
...@@ -1719,6 +1731,9 @@ msgstr "" ...@@ -1719,6 +1731,9 @@ msgstr ""
msgid "ClusterIntegration|API URL" msgid "ClusterIntegration|API URL"
msgstr "" msgstr ""
msgid "ClusterIntegration|API URL should be a valid http/https url."
msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster" msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr "" msgstr ""
...@@ -1779,6 +1794,9 @@ msgstr "" ...@@ -1779,6 +1794,9 @@ msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr "" msgstr ""
msgid "ClusterIntegration|Cluster name is required."
msgstr ""
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters." msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
msgstr "" msgstr ""
...@@ -1800,7 +1818,7 @@ msgstr "" ...@@ -1800,7 +1818,7 @@ msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name" msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Token" msgid "ClusterIntegration|Copy Service Token"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster" msgid "ClusterIntegration|Create Kubernetes cluster"
...@@ -1992,6 +2010,9 @@ msgstr "" ...@@ -1992,6 +2010,9 @@ msgstr ""
msgid "ClusterIntegration|Number of nodes" msgid "ClusterIntegration|Number of nodes"
msgstr "" msgstr ""
msgid "ClusterIntegration|Number of nodes must be a numerical value."
msgstr ""
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes" msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
msgstr "" msgstr ""
...@@ -2004,9 +2025,6 @@ msgstr "" ...@@ -2004,9 +2025,6 @@ msgstr ""
msgid "ClusterIntegration|Project cluster" msgid "ClusterIntegration|Project cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Project namespace"
msgstr ""
msgid "ClusterIntegration|Project namespace (optional, unique)" msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "" msgstr ""
...@@ -2073,7 +2091,10 @@ msgstr "" ...@@ -2073,7 +2091,10 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type" msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "" msgstr ""
msgid "ClusterIntegration|Service token" msgid "ClusterIntegration|Service Token"
msgstr ""
msgid "ClusterIntegration|Service token is required."
msgstr "" msgstr ""
msgid "ClusterIntegration|Show" msgid "ClusterIntegration|Show"
...@@ -2106,9 +2127,6 @@ msgstr "" ...@@ -2106,9 +2127,6 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster" msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr "" msgstr ""
...@@ -4529,6 +4547,9 @@ msgstr "" ...@@ -4529,6 +4547,9 @@ msgstr ""
msgid "Job" msgid "Job"
msgstr "" msgstr ""
msgid "Job Failed #%{build_id}"
msgstr ""
msgid "Job ID" msgid "Job ID"
msgstr "" msgstr ""
...@@ -4774,6 +4795,9 @@ msgstr "" ...@@ -4774,6 +4795,9 @@ msgstr ""
msgid "Leave" msgid "Leave"
msgstr "" msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost."
msgstr ""
msgid "Leave group" msgid "Leave group"
msgstr "" msgstr ""
...@@ -4923,6 +4947,9 @@ msgstr "" ...@@ -4923,6 +4947,9 @@ msgstr ""
msgid "Markdown" msgid "Markdown"
msgstr "" msgstr ""
msgid "Markdown Help"
msgstr ""
msgid "Markdown enabled" msgid "Markdown enabled"
msgstr "" msgstr ""
...@@ -5393,6 +5420,9 @@ msgstr "" ...@@ -5393,6 +5420,9 @@ msgstr ""
msgid "No files found." msgid "No files found."
msgstr "" msgstr ""
msgid "No job trace"
msgstr ""
msgid "No labels with such name or description" msgid "No labels with such name or description"
msgstr "" msgstr ""
...@@ -5617,6 +5647,9 @@ msgstr "" ...@@ -5617,6 +5647,9 @@ msgstr ""
msgid "Open in Xcode" msgid "Open in Xcode"
msgstr "" msgstr ""
msgid "Open raw"
msgstr ""
msgid "Open sidebar" msgid "Open sidebar"
msgstr "" msgstr ""
...@@ -5740,6 +5773,9 @@ msgstr "" ...@@ -5740,6 +5773,9 @@ msgstr ""
msgid "Permissions" msgid "Permissions"
msgstr "" msgstr ""
msgid "Permissions Help"
msgstr ""
msgid "Permissions, LFS, 2FA" msgid "Permissions, LFS, 2FA"
msgstr "" msgstr ""
...@@ -6016,6 +6052,9 @@ msgstr "" ...@@ -6016,6 +6052,9 @@ msgstr ""
msgid "Preview" msgid "Preview"
msgstr "" msgstr ""
msgid "Preview changes"
msgstr ""
msgid "Preview payload" msgid "Preview payload"
msgstr "" msgstr ""
...@@ -6547,6 +6586,9 @@ msgstr "" ...@@ -6547,6 +6586,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication." msgid "Public - The project can be accessed without any authentication."
msgstr "" msgstr ""
msgid "Public Access Help"
msgstr ""
msgid "Public deploy keys (%{deploy_keys_count})" msgid "Public deploy keys (%{deploy_keys_count})"
msgstr "" msgstr ""
...@@ -6577,6 +6619,9 @@ msgstr "" ...@@ -6577,6 +6619,9 @@ msgstr ""
msgid "README" msgid "README"
msgstr "" msgstr ""
msgid "Rake Tasks Help"
msgstr ""
msgid "Read more" msgid "Read more"
msgstr "" msgstr ""
...@@ -6672,6 +6717,12 @@ msgstr "" ...@@ -6672,6 +6717,12 @@ msgstr ""
msgid "Remove project" msgid "Remove project"
msgstr "" msgstr ""
msgid "Remove this label? Are you sure?"
msgstr ""
msgid "Remove this label? This will affect all projects within the group. Are you sure?"
msgstr ""
msgid "Removed group can not be restored!" msgid "Removed group can not be restored!"
msgstr "" msgstr ""
...@@ -6690,6 +6741,9 @@ msgstr "" ...@@ -6690,6 +6741,9 @@ msgstr ""
msgid "Reopen milestone" msgid "Reopen milestone"
msgstr "" msgstr ""
msgid "Replace"
msgstr ""
msgid "Reply to comment" msgid "Reply to comment"
msgstr "" msgstr ""
...@@ -6914,6 +6968,9 @@ msgstr "" ...@@ -6914,6 +6968,9 @@ msgstr ""
msgid "SSH Keys" msgid "SSH Keys"
msgstr "" msgstr ""
msgid "SSH Keys Help"
msgstr ""
msgid "SSH host keys" msgid "SSH host keys"
msgstr "" msgstr ""
...@@ -7040,6 +7097,9 @@ msgstr "" ...@@ -7040,6 +7097,9 @@ msgstr ""
msgid "SearchAutocomplete|in this project" msgid "SearchAutocomplete|in this project"
msgstr "" msgstr ""
msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\""
msgstr ""
msgid "Secret" msgid "Secret"
msgstr "" msgstr ""
...@@ -7417,6 +7477,9 @@ msgstr "" ...@@ -7417,6 +7477,9 @@ msgstr ""
msgid "Sort by" msgid "Sort by"
msgstr "" msgstr ""
msgid "Sort direction"
msgstr ""
msgid "SortOptions|Access level, ascending" msgid "SortOptions|Access level, ascending"
msgstr "" msgstr ""
...@@ -7735,6 +7798,9 @@ msgstr "" ...@@ -7735,6 +7798,9 @@ msgstr ""
msgid "System Hooks" msgid "System Hooks"
msgstr "" msgstr ""
msgid "System Hooks Help"
msgstr ""
msgid "System Info" msgid "System Info"
msgstr "" msgstr ""
...@@ -7867,6 +7933,11 @@ msgstr "" ...@@ -7867,6 +7933,11 @@ msgstr ""
msgid "Test failed." msgid "Test failed."
msgstr "" msgstr ""
msgid "The %{type} contains the following error:"
msgid_plural "The %{type} contains the following errors:"
msgstr[0] ""
msgstr[1] ""
msgid "The Git LFS objects will <strong>not</strong> be synced." msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr "" msgstr ""
...@@ -7909,6 +7980,15 @@ msgstr "" ...@@ -7909,6 +7980,15 @@ msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account." msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr "" msgstr ""
msgid "The group and any internal projects can be viewed by any logged in user."
msgstr ""
msgid "The group and any public projects can be viewed without any authentication."
msgstr ""
msgid "The group and its projects can only be viewed by members."
msgstr ""
msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}." msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}."
msgstr "" msgstr ""
...@@ -7987,6 +8067,18 @@ msgstr "" ...@@ -7987,6 +8067,18 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "" msgstr ""
msgid "The snippet can be accessed without any authentication."
msgstr ""
msgid "The snippet is visible only to me."
msgstr ""
msgid "The snippet is visible only to project members."
msgstr ""
msgid "The snippet is visible to any logged in user."
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "" msgstr ""
...@@ -8837,6 +8929,9 @@ msgstr "" ...@@ -8837,6 +8929,9 @@ msgstr ""
msgid "User map" msgid "User map"
msgstr "" msgstr ""
msgid "User settings"
msgstr ""
msgid "User was successfully created." msgid "User was successfully created."
msgstr "" msgstr ""
...@@ -8990,6 +9085,9 @@ msgstr "" ...@@ -8990,6 +9085,9 @@ msgstr ""
msgid "View it on GitLab" msgid "View it on GitLab"
msgstr "" msgstr ""
msgid "View job trace"
msgstr ""
msgid "View jobs" msgid "View jobs"
msgstr "" msgstr ""
...@@ -9065,6 +9163,9 @@ msgstr "" ...@@ -9065,6 +9163,9 @@ msgstr ""
msgid "Web terminal" msgid "Web terminal"
msgstr "" msgstr ""
msgid "Webhooks Help"
msgstr ""
msgid "When a runner is locked, it cannot be assigned to other projects" msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr "" msgstr ""
...@@ -9197,6 +9298,9 @@ msgstr "" ...@@ -9197,6 +9298,9 @@ msgstr ""
msgid "Wiki|Create page" msgid "Wiki|Create page"
msgstr "" msgstr ""
msgid "Wiki|Created date"
msgstr ""
msgid "Wiki|Edit Page" msgid "Wiki|Edit Page"
msgstr "" msgstr ""
...@@ -9215,6 +9319,9 @@ msgstr "" ...@@ -9215,6 +9319,9 @@ msgstr ""
msgid "Wiki|Pages" msgid "Wiki|Pages"
msgstr "" msgstr ""
msgid "Wiki|Title"
msgstr ""
msgid "Wiki|Wiki Pages" msgid "Wiki|Wiki Pages"
msgstr "" msgstr ""
...@@ -9224,6 +9331,9 @@ msgstr "" ...@@ -9224,6 +9331,9 @@ msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
msgid "Workflow Help"
msgstr ""
msgid "Write" msgid "Write"
msgstr "" msgstr ""
...@@ -9428,6 +9538,12 @@ msgstr "" ...@@ -9428,6 +9538,12 @@ msgstr ""
msgid "You'll need to use different branch names to get a valid comparison." msgid "You'll need to use different branch names to get a valid comparison."
msgstr "" msgstr ""
msgid "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
msgstr ""
msgid "You're not allowed to make changes to this project directly. A fork of this project is being created that you can make changes in, so you can submit a merge request."
msgstr ""
msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options." msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options."
msgstr "" msgstr ""
......
...@@ -6,7 +6,7 @@ module QA ...@@ -6,7 +6,7 @@ module QA
view 'app/helpers/blob_helper.rb' do view 'app/helpers/blob_helper.rb' do
element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
element :delete_button, /label:\s+"Delete"/ # rubocop:disable QA/ElementWithPattern element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern
end end
view 'app/views/projects/blob/_remove.html.haml' do view 'app/views/projects/blob/_remove.html.haml' do
......
...@@ -6,7 +6,7 @@ module QA ...@@ -6,7 +6,7 @@ module QA
class AddExisting < Page::Base class AddExisting < Page::Base
view 'app/views/clusters/clusters/user/_form.html.haml' do view 'app/views/clusters/clusters/user/_form.html.haml' do
element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern
element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
......
...@@ -8,11 +8,11 @@ module QA ...@@ -8,11 +8,11 @@ module QA
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
deploy_token_name = 'deploy token name' deploy_token_name = 'deploy token name'
deploy_token_expires_at = Date.today + 7 # 1 Week from now one_week_from_now = Date.today + 7
deploy_token = Resource::DeployToken.fabricate! do |resource| deploy_token = Resource::DeployToken.fabricate! do |resource|
resource.name = deploy_token_name resource.name = deploy_token_name
resource.expires_at = deploy_token_expires_at resource.expires_at = one_week_from_now
end end
expect(deploy_token.username.length).to be > 0 expect(deploy_token.username.length).to be > 0
......
...@@ -117,7 +117,7 @@ describe IssuableCollections do ...@@ -117,7 +117,7 @@ describe IssuableCollections do
due_date: '2017-01-01', due_date: '2017-01-01',
group_id: '3', group_id: '3',
iids: '4', iids: '4',
label_name: 'foo', label_name: ['foo'],
milestone_title: 'bar', milestone_title: 'bar',
my_reaction_emoji: 'thumbsup', my_reaction_emoji: 'thumbsup',
non_archived: 'true', non_archived: 'true',
...@@ -142,7 +142,7 @@ describe IssuableCollections do ...@@ -142,7 +142,7 @@ describe IssuableCollections do
'author_id' => '2', 'author_id' => '2',
'author_username' => 'user2', 'author_username' => 'user2',
'confidential' => true, 'confidential' => true,
'label_name' => 'foo', 'label_name' => ['foo'],
'milestone_title' => 'bar', 'milestone_title' => 'bar',
'my_reaction_emoji' => 'thumbsup', 'my_reaction_emoji' => 'thumbsup',
'due_date' => '2017-01-01', 'due_date' => '2017-01-01',
......
...@@ -23,4 +23,37 @@ describe DashboardController do ...@@ -23,4 +23,37 @@ describe DashboardController do
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
describe "#check_filters_presence!" do
let(:user) { create(:user) }
before do
sign_in(user)
get :merge_requests, params: params
end
context "no filters" do
let(:params) { {} }
it 'sets @no_filters_set to false' do
expect(assigns[:no_filters_set]).to eq(true)
end
end
context "scalar filters" do
let(:params) { { author_id: user.id } }
it 'sets @no_filters_set to false' do
expect(assigns[:no_filters_set]).to eq(false)
end
end
context "array filters" do
let(:params) { { label_name: ['bug'] } }
it 'sets @no_filters_set to false' do
expect(assigns[:no_filters_set]).to eq(false)
end
end
end
end end
...@@ -616,7 +616,7 @@ describe GroupsController do ...@@ -616,7 +616,7 @@ describe GroupsController do
end end
it 'should redirect to the current path' do it 'should redirect to the current path' do
expect(response).to render_template(:edit) expect(response).to redirect_to(edit_group_path(group))
end end
end end
......
...@@ -283,7 +283,7 @@ describe Projects::EnvironmentsController do ...@@ -283,7 +283,7 @@ describe Projects::EnvironmentsController do
.and_return([:fake_terminal]) .and_return([:fake_terminal])
expect(Gitlab::Workhorse) expect(Gitlab::Workhorse)
.to receive(:terminal_websocket) .to receive(:channel_websocket)
.with(:fake_terminal) .with(:fake_terminal)
.and_return(workhorse: :response) .and_return(workhorse: :response)
......
...@@ -989,7 +989,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -989,7 +989,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'and valid id' do context 'and valid id' do
it 'returns the terminal for the job' do it 'returns the terminal for the job' do
expect(Gitlab::Workhorse) expect(Gitlab::Workhorse)
.to receive(:terminal_websocket) .to receive(:channel_websocket)
.and_return(workhorse: :response) .and_return(workhorse: :response)
get_terminal_websocket(id: job.id) get_terminal_websocket(id: job.id)
......
...@@ -44,6 +44,8 @@ describe 'Dashboard Merge Requests' do ...@@ -44,6 +44,8 @@ describe 'Dashboard Merge Requests' do
end end
context 'merge requests exist' do context 'merge requests exist' do
let(:label) { create(:label) }
let!(:assigned_merge_request) do let!(:assigned_merge_request) do
create(:merge_request, create(:merge_request,
assignee: current_user, assignee: current_user,
...@@ -72,6 +74,14 @@ describe 'Dashboard Merge Requests' do ...@@ -72,6 +74,14 @@ describe 'Dashboard Merge Requests' do
target_project: public_project, source_project: forked_project) target_project: public_project, source_project: forked_project)
end end
let!(:labeled_merge_request) do
create(:labeled_merge_request,
source_branch: 'labeled',
labels: [label],
author: current_user,
source_project: project)
end
let!(:other_merge_request) do let!(:other_merge_request) do
create(:merge_request, create(:merge_request,
source_branch: 'fix', source_branch: 'fix',
...@@ -90,6 +100,7 @@ describe 'Dashboard Merge Requests' do ...@@ -90,6 +100,7 @@ describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(authored_merge_request.title) expect(page).not_to have_content(authored_merge_request.title)
expect(page).not_to have_content(authored_merge_request_from_fork.title) expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title) expect(page).not_to have_content(other_merge_request.title)
expect(page).not_to have_content(labeled_merge_request.title)
end end
it 'shows authored merge requests', :js do it 'shows authored merge requests', :js do
...@@ -98,7 +109,21 @@ describe 'Dashboard Merge Requests' do ...@@ -98,7 +109,21 @@ describe 'Dashboard Merge Requests' do
expect(page).to have_content(authored_merge_request.title) expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title) expect(page).to have_content(authored_merge_request_from_fork.title)
expect(page).to have_content(labeled_merge_request.title)
expect(page).not_to have_content(assigned_merge_request.title)
expect(page).not_to have_content(assigned_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
end
it 'shows labeled merge requests', :js do
reset_filters
input_filtered_search("label:#{label.name}")
expect(page).to have_content(labeled_merge_request.title)
expect(page).not_to have_content(authored_merge_request.title)
expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(assigned_merge_request.title) expect(page).not_to have_content(assigned_merge_request.title)
expect(page).not_to have_content(assigned_merge_request_from_fork.title) expect(page).not_to have_content(assigned_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title) expect(page).not_to have_content(other_merge_request.title)
......
...@@ -69,7 +69,7 @@ describe 'User Cluster', :js do ...@@ -69,7 +69,7 @@ describe 'User Cluster', :js do
end end
it 'user sees a validation error' do it 'user sees a validation error' do
expect(page).to have_css('#error_explanation') expect(page).to have_css('.gl-field-error')
end end
end end
end end
......
...@@ -43,6 +43,7 @@ describe 'Issues > User uses quick actions', :js do ...@@ -43,6 +43,7 @@ describe 'Issues > User uses quick actions', :js do
describe 'issue-only commands' do describe 'issue-only commands' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -55,6 +56,8 @@ describe 'Issues > User uses quick actions', :js do ...@@ -55,6 +56,8 @@ describe 'Issues > User uses quick actions', :js do
wait_for_requests wait_for_requests
end end
it_behaves_like 'confidential quick action'
describe 'adding a due date from note' do describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
...@@ -137,42 +140,6 @@ describe 'Issues > User uses quick actions', :js do ...@@ -137,42 +140,6 @@ describe 'Issues > User uses quick actions', :js do
end end
end end
describe 'make issue confidential' do
let(:issue) { create(:issue, project: project) }
let(:original_issue) { create(:issue, project: project) }
context 'when the current user can update issues' do
it 'does not create a note, and marks the issue as confidential' do
add_note("/confidential")
expect(page).not_to have_content "/confidential"
expect(page).to have_content 'Commands applied'
expect(page).to have_content "made the issue confidential"
expect(issue.reload).to be_confidential
end
end
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
end
it 'does not create a note, and does not mark the issue as confidential' do
add_note("/confidential")
expect(page).not_to have_content 'Commands applied'
expect(page).not_to have_content "made the issue confidential"
expect(issue.reload).not_to be_confidential
end
end
end
describe 'move the issue to another project' do describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
......
...@@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do ...@@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do
end end
it 'user sees a validation error' do it 'user sees a validation error' do
expect(page).to have_css('#error_explanation') expect(page).to have_css('.gl-field-error')
end end
end end
end end
......
...@@ -53,7 +53,7 @@ describe 'User Cluster', :js do ...@@ -53,7 +53,7 @@ describe 'User Cluster', :js do
end end
it 'user sees a validation error' do it 'user sees a validation error' do
expect(page).to have_css('#error_explanation') expect(page).to have_css('.gl-field-error')
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'User views wiki pages' do
include WikiHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page1) do
create(:wiki_page, wiki: project.wiki, attrs: { title: '3 home', content: '3' })
end
let!(:wiki_page2) do
create(:wiki_page, wiki: project.wiki, attrs: { title: '1 home', content: '1' })
end
let!(:wiki_page3) do
create(:wiki_page, wiki: project.wiki, attrs: { title: '2 home', content: '2' })
end
let(:pages) do
page.find('.wiki-pages-list').all('li').map { |li| li.find('a') }
end
before do
project.add_maintainer(user)
sign_in(user)
visit(project_wikis_pages_path(project))
end
context 'ordered by title' do
let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] }
context 'asc' do
it 'pages are displayed in direct order' do
pages.each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_title[index].title)
end
end
end
context 'desc' do
before do
page.within('.wiki-sort-dropdown') do
page.find('.qa-reverse-sort').click
end
end
it 'pages are displayed in reversed order' do
pages.reverse_each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_title[index].title)
end
end
end
end
context 'ordered by created_at' do
let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] }
before do
page.within('.wiki-sort-dropdown') do
click_button('Title')
click_link('Created date')
end
end
context 'asc' do
it 'pages are displayed in direct order' do
pages.each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
end
end
end
context 'desc' do
before do
page.within('.wiki-sort-dropdown') do
page.find('.qa-reverse-sort').click
end
end
it 'pages are displayed in reversed order' do
pages.reverse_each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
end
end
end
end
end
...@@ -75,6 +75,59 @@ describe 'Gitlab::Graphql::Authorization' do ...@@ -75,6 +75,59 @@ describe 'Gitlab::Graphql::Authorization' do
end end
end end
describe 'Field authorizations when field is a built in type' do
let(:query_type) do
query_factory do |query|
query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
end
end
describe 'with a single permission' do
let(:type) do
type_factory do |type|
type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single
end
end
it 'returns the protected field when user has permission' do
permit(permission_single)
expect(subject).to eq('name' => test_object.name)
end
it 'returns nil when user is not authorized' do
expect(subject).to eq('name' => nil)
end
end
describe 'with a collection of permissions' do
let(:type) do
permissions = permission_collection
type_factory do |type|
type.field :name, GraphQL::STRING_TYPE, null: true do
authorize permissions
end
end
end
it 'returns the protected field when user has all permissions' do
permit(*permission_collection)
expect(subject).to eq('name' => test_object.name)
end
it 'returns nil when user only has one of the permissions' do
permit(permission_collection.first)
expect(subject).to eq('name' => nil)
end
it 'returns nil when user only has none of the permissions' do
expect(subject).to eq('name' => nil)
end
end
end
describe 'Type authorizations' do describe 'Type authorizations' do
let(:query_type) do let(:query_type) do
query_factory do |query| query_factory do |query|
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe GitlabSchema do describe GitlabSchema do
...@@ -31,6 +33,46 @@ describe GitlabSchema do ...@@ -31,6 +33,46 @@ describe GitlabSchema do
expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection) expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection)
end end
context 'for different types of users' do
it 'returns DEFAULT_MAX_COMPLEXITY for no context' do
expect(GraphQL::Schema)
.to receive(:execute)
.with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
described_class.execute('query')
end
it 'returns DEFAULT_MAX_COMPLEXITY for no user' do
expect(GraphQL::Schema)
.to receive(:execute)
.with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
described_class.execute('query', context: {})
end
it 'returns AUTHENTICATED_COMPLEXITY for a logged in user' do
user = build :user
expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::AUTHENTICATED_COMPLEXITY))
described_class.execute('query', context: { current_user: user })
end
it 'returns ADMIN_COMPLEXITY for an admin user' do
user = build :user, :admin
expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::ADMIN_COMPLEXITY))
described_class.execute('query', context: { current_user: user })
end
it 'returns what was passed on the query' do
expect(GraphQL::Schema).to receive(:execute).with('query', { max_complexity: 1234 })
described_class.execute('query', max_complexity: 1234)
end
end
def field_instrumenters def field_instrumenters
described_class.instrumenters[:field] described_class.instrumenters[:field]
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Types::BaseField do
context 'when considering complexity' do
it 'defaults to 1' do
field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
expect(field.to_graphql.complexity).to eq 1
end
it 'has specified value' do
field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
expect(field.to_graphql.complexity).to eq 12
end
end
end
...@@ -18,4 +18,56 @@ describe WikiHelper do ...@@ -18,4 +18,56 @@ describe WikiHelper do
end end
end end
end end
describe '#wiki_sort_controls' do
let(:project) { create(:project) }
let(:wiki_link) { helper.wiki_sort_controls(project, sort, direction) }
let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort" }
def expected_link(sort, direction, icon_class)
path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}"
helper.link_to(path, type: 'button', class: classes, title: 'Sort direction') do
helper.sprite_icon("sort-#{icon_class}", size: 16)
end
end
context 'initial call' do
let(:sort) { nil }
let(:direction) { nil }
it 'renders with default values' do
expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
end
end
context 'sort by title' do
let(:sort) { 'title' }
let(:direction) { 'asc' }
it 'renders a link with opposite direction' do
expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
end
end
context 'sort by created_at' do
let(:sort) { 'created_at' }
let(:direction) { 'desc' }
it 'renders a link with opposite direction' do
expect(wiki_link).to eq(expected_link('created_at', 'asc', 'highest'))
end
end
end
describe '#wiki_sort_title' do
it 'returns a title corresponding to a key' do
expect(helper.wiki_sort_title('created_at')).to eq('Created date')
expect(helper.wiki_sort_title('title')).to eq('Title')
end
it 'defaults to Title if a key is unknown' do
expect(helper.wiki_sort_title('unknown')).to eq('Title')
end
end
end end
...@@ -11,6 +11,8 @@ describe('IDE pipelines list', () => { ...@@ -11,6 +11,8 @@ describe('IDE pipelines list', () => {
let vm; let vm;
let mock; let mock;
const findLoadingState = () => vm.$el.querySelector('.loading-container');
beforeEach(done => { beforeEach(done => {
const store = createStore(); const store = createStore();
...@@ -95,7 +97,7 @@ describe('IDE pipelines list', () => { ...@@ -95,7 +97,7 @@ describe('IDE pipelines list', () => {
describe('empty state', () => { describe('empty state', () => {
it('renders pipelines empty state', done => { it('renders pipelines empty state', done => {
vm.$store.state.pipelines.latestPipeline = false; vm.$store.state.pipelines.latestPipeline = null;
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelector('.empty-state')).not.toBe(null); expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
...@@ -106,15 +108,30 @@ describe('IDE pipelines list', () => { ...@@ -106,15 +108,30 @@ describe('IDE pipelines list', () => {
}); });
describe('loading state', () => { describe('loading state', () => {
it('renders loading state when there is no latest pipeline', done => { beforeEach(() => {
vm.$store.state.pipelines.latestPipeline = null;
vm.$store.state.pipelines.isLoadingPipeline = true; vm.$store.state.pipelines.isLoadingPipeline = true;
});
vm.$nextTick(() => { it('does not render when pipeline has loaded before', done => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null); vm.$store.state.pipelines.hasLoadedPipeline = true;
done(); vm.$nextTick()
.then(() => {
expect(findLoadingState()).toBe(null);
})
.then(done)
.catch(done.fail);
}); });
it('renders loading state when there is no latest pipeline', done => {
vm.$store.state.pipelines.hasLoadedPipeline = false;
vm.$nextTick()
.then(() => {
expect(findLoadingState()).not.toBe(null);
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -27,21 +27,27 @@ describe('IDE pipelines mutations', () => { ...@@ -27,21 +27,27 @@ describe('IDE pipelines mutations', () => {
}); });
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => { describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
it('sets loading to false on success', () => { const itSetsPipelineLoadingStates = () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( it('sets has loaded to true', () => {
mockedState, expect(mockedState.hasLoadedPipeline).toBe(true);
fullPipelinesResponse.data.pipelines[0], });
);
it('sets loading to false on success', () => {
expect(mockedState.isLoadingPipeline).toBe(false); expect(mockedState.isLoadingPipeline).toBe(false);
}); });
};
it('sets latestPipeline', () => { describe('with pipeline', () => {
beforeEach(() => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
mockedState, mockedState,
fullPipelinesResponse.data.pipelines[0], fullPipelinesResponse.data.pipelines[0],
); );
});
itSetsPipelineLoadingStates();
it('sets latestPipeline', () => {
expect(mockedState.latestPipeline).toEqual({ expect(mockedState.latestPipeline).toEqual({
id: '51', id: '51',
path: 'test', path: 'test',
...@@ -51,18 +57,7 @@ describe('IDE pipelines mutations', () => { ...@@ -51,18 +57,7 @@ describe('IDE pipelines mutations', () => {
}); });
}); });
it('does not set latest pipeline if pipeline is null', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
expect(mockedState.latestPipeline).toEqual(false);
});
it('sets stages', () => { it('sets stages', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
mockedState,
fullPipelinesResponse.data.pipelines[0],
);
expect(mockedState.stages.length).toBe(2); expect(mockedState.stages.length).toBe(2);
expect(mockedState.stages).toEqual([ expect(mockedState.stages).toEqual([
{ {
...@@ -87,6 +82,19 @@ describe('IDE pipelines mutations', () => { ...@@ -87,6 +82,19 @@ describe('IDE pipelines mutations', () => {
}); });
}); });
describe('with null', () => {
beforeEach(() => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
});
itSetsPipelineLoadingStates();
it('does not set latest pipeline if pipeline is null', () => {
expect(mockedState.latestPipeline).toEqual(null);
});
});
});
describe(types.REQUEST_JOBS, () => { describe(types.REQUEST_JOBS, () => {
beforeEach(() => { beforeEach(() => {
mockedState.stages = stages.map((stage, i) => ({ mockedState.stages = stages.map((stage, i) => ({
......
...@@ -101,6 +101,32 @@ describe Gitlab::Ci::Build::Policy::Refs do ...@@ -101,6 +101,32 @@ describe Gitlab::Ci::Build::Policy::Refs do
expect(described_class.new(['/fix-.*/'])) expect(described_class.new(['/fix-.*/']))
.not_to be_satisfied_by(pipeline) .not_to be_satisfied_by(pipeline)
end end
context 'when unsafe regexp is used' do
let(:subject) { described_class.new(['/^(?!master).+/']) }
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'ignores invalid regexp' do
expect(subject)
.not_to be_satisfied_by(pipeline)
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is satisfied by regexp' do
expect(subject)
.to be_satisfied_by(pipeline)
end
end
end
end end
context 'malicious regexp' do context 'malicious regexp' do
......
require 'fast_spec_helper' require 'fast_spec_helper'
require 'support/helpers/stub_feature_flags'
require_dependency 'active_model' require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do describe Gitlab::Ci::Config::Entry::Policy do
...@@ -33,6 +34,44 @@ describe Gitlab::Ci::Config::Entry::Policy do ...@@ -33,6 +34,44 @@ describe Gitlab::Ci::Config::Entry::Policy do
end end
end end
context 'when config is an empty regexp' do
let(:config) { ['//'] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when using unsafe regexp' do
include StubFeatureFlags
let(:config) { ['/^(?!master).+/'] }
subject { described_class.new([regexp]) }
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'is not valid' do
expect(entry).not_to be_valid
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is a special keyword' do context 'when config is a special keyword' do
let(:config) { %w[tags triggers branches] } let(:config) { %w[tags triggers branches] }
...@@ -67,6 +106,34 @@ describe Gitlab::Ci::Config::Entry::Policy do ...@@ -67,6 +106,34 @@ describe Gitlab::Ci::Config::Entry::Policy do
end end
end end
context 'when using unsafe regexp' do
include StubFeatureFlags
let(:config) { { refs: ['/^(?!master).+/'] } }
subject { described_class.new([regexp]) }
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'is not valid' do
expect(entry).not_to be_valid
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when specifying kubernetes policy' do context 'when specifying kubernetes policy' do
let(:config) { { kubernetes: 'active' } } let(:config) { { kubernetes: 'active' } }
......
...@@ -9,10 +9,11 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do ...@@ -9,10 +9,11 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
let(:current_user) { double(:current_user) } let(:current_user) { double(:current_user) }
let(:abilities) { [double(:first_ability), double(:last_ability)] } let(:abilities) { [double(:first_ability), double(:last_ability)] }
context 'when authorizing against the object' do
let(:checker) do let(:checker) do
service = described_class.new(double(resolve_proc: proc {})) service = described_class.new(double(resolve_proc: proc {}))
allow(service).to receive(:authorizations).and_return(abilities) allow(service).to receive(:authorizations).and_return(abilities)
service.__send__(:build_checker, current_user) service.__send__(:build_checker, current_user, nil)
end end
it 'returns a checker which checks for a single object' do it 'returns a checker which checks for a single object' do
...@@ -56,8 +57,40 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do ...@@ -56,8 +57,40 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
spy_ability_check_for(ability, allowed, passed: true) spy_ability_check_for(ability, allowed, passed: true)
end end
expect(checker.call([disallowed, allowed])) expect(checker.call([disallowed, allowed])).to contain_exactly(allowed)
.to contain_exactly(allowed) end
end
end
context 'when authorizing against another object' do
let(:authorizing_obj) { double(:object) }
let(:checker) do
service = described_class.new(double(resolve_proc: proc {}))
allow(service).to receive(:authorizations).and_return(abilities)
service.__send__(:build_checker, current_user, authorizing_obj)
end
it 'returns a checker which checks for a single object' do
object = double(:object)
abilities.each do |ability|
spy_ability_check_for(ability, authorizing_obj, passed: true)
end
expect(checker.call(object)).to eq(object)
end
it 'returns a checker which checks for all objects' do
objects = [double(:first), double(:last)]
abilities.each do |ability|
objects.each do |object|
spy_ability_check_for(ability, authorizing_obj, passed: true)
end
end
expect(checker.call(objects)).to eq(objects)
end end
end end
end end
......
...@@ -230,4 +230,32 @@ describe Gitlab::PrometheusClient do ...@@ -230,4 +230,32 @@ describe Gitlab::PrometheusClient do
let(:execute_query) { subject.query_range(prometheus_query) } let(:execute_query) { subject.query_range(prometheus_query) }
end end
end end
describe '.compute_step' do
using RSpec::Parameterized::TableSyntax
let(:now) { Time.now.utc }
subject { described_class.compute_step(start, stop) }
where(:time_interval_in_seconds, :step) do
0 | 60
10.hours | 60
10.hours + 1 | 61
# frontend options
30.minutes | 60
3.hours | 60
8.hours | 60
1.day | 144
3.days | 432
1.week | 1008
end
with_them do
let(:start) { now - time_interval_in_seconds }
let(:stop) { now }
it { is_expected.to eq(step) }
end
end
end end
require 'fast_spec_helper' require 'fast_spec_helper'
require 'support/shared_examples/malicious_regexp_shared_examples' require 'support/shared_examples/malicious_regexp_shared_examples'
require 'support/helpers/stub_feature_flags'
describe Gitlab::UntrustedRegexp::RubySyntax do describe Gitlab::UntrustedRegexp::RubySyntax do
describe '.matches_syntax?' do describe '.matches_syntax?' do
...@@ -33,6 +34,12 @@ describe Gitlab::UntrustedRegexp::RubySyntax do ...@@ -33,6 +34,12 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
end end
end end
context 'when regexp is empty' do
it 'fabricates regexp correctly' do
expect(described_class.fabricate('//')).not_to be_nil
end
end
context 'when regexp is a raw pattern' do context 'when regexp is a raw pattern' do
it 'returns error' do it 'returns error' do
expect(described_class.fabricate('some .* thing')).to be_nil expect(described_class.fabricate('some .* thing')).to be_nil
...@@ -41,6 +48,7 @@ describe Gitlab::UntrustedRegexp::RubySyntax do ...@@ -41,6 +48,7 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
end end
describe '.fabricate!' do describe '.fabricate!' do
context 'safe regexp is used' do
context 'when regexp is using /regexp/ scheme with flags' do context 'when regexp is using /regexp/ scheme with flags' do
it 'fabricates regexp with a single flag' do it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate!('/something/i') regexp = described_class.fabricate!('/something/i')
...@@ -61,6 +69,44 @@ describe Gitlab::UntrustedRegexp::RubySyntax do ...@@ -61,6 +69,44 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
expect(regexp).to eq Gitlab::UntrustedRegexp.new('something') expect(regexp).to eq Gitlab::UntrustedRegexp.new('something')
end end
end end
end
context 'when unsafe regexp is used' do
include StubFeatureFlags
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError)
end
context 'when no fallback is enabled' do
it 'raises an exception' do
expect { described_class.fabricate!('/something/') }
.to raise_error(RegexpError)
end
end
context 'when fallback is used' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate!('/something/i', fallback: true)
expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE)
end
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate!('/something/im', fallback: true)
expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE | Regexp::MULTILINE)
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate!('/something/', fallback: true)
expect(regexp).to eq Regexp.new('something')
end
end
end
context 'when regexp is a raw pattern' do context 'when regexp is a raw pattern' do
it 'raises an error' do it 'raises an error' do
......
...@@ -94,7 +94,7 @@ describe Gitlab::Workhorse do ...@@ -94,7 +94,7 @@ describe Gitlab::Workhorse do
end end
end end
describe '.terminal_websocket' do describe '.channel_websocket' do
def terminal(ca_pem: nil) def terminal(ca_pem: nil)
out = { out = {
subprotocols: ['foo'], subprotocols: ['foo'],
...@@ -108,25 +108,25 @@ describe Gitlab::Workhorse do ...@@ -108,25 +108,25 @@ describe Gitlab::Workhorse do
def workhorse(ca_pem: nil) def workhorse(ca_pem: nil)
out = { out = {
'Terminal' => { 'Channel' => {
'Subprotocols' => ['foo'], 'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws', 'Url' => 'wss://example.com/terminal.ws',
'Header' => { 'Authorization' => ['Token x'] }, 'Header' => { 'Authorization' => ['Token x'] },
'MaxSessionTime' => 600 'MaxSessionTime' => 600
} }
} }
out['Terminal']['CAPem'] = ca_pem if ca_pem out['Channel']['CAPem'] = ca_pem if ca_pem
out out
end end
context 'without ca_pem' do context 'without ca_pem' do
subject { described_class.terminal_websocket(terminal) } subject { described_class.channel_websocket(terminal) }
it { is_expected.to eq(workhorse) } it { is_expected.to eq(workhorse) }
end end
context 'with ca_pem' do context 'with ca_pem' do
subject { described_class.terminal_websocket(terminal(ca_pem: "foo")) } subject { described_class.channel_websocket(terminal(ca_pem: "foo")) }
it { is_expected.to eq(workhorse(ca_pem: "foo")) } it { is_expected.to eq(workhorse(ca_pem: "foo")) }
end end
......
...@@ -13,25 +13,33 @@ describe Ci::BuildRunnerSession, model: true do ...@@ -13,25 +13,33 @@ describe Ci::BuildRunnerSession, model: true do
it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') } it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') }
describe '#terminal_specification' do describe '#terminal_specification' do
let(:terminal_specification) { subject.terminal_specification } let(:specification) { subject.terminal_specification }
it 'returns terminal.gitlab.com protocol' do
expect(specification[:subprotocols]).to eq ['terminal.gitlab.com']
end
it 'returns a wss url' do
expect(specification[:url]).to start_with('wss://')
end
it 'returns empty hash if no url' do it 'returns empty hash if no url' do
subject.url = '' subject.url = ''
expect(terminal_specification).to be_empty expect(specification).to be_empty
end end
context 'when url is present' do context 'when url is present' do
it 'returns ca_pem nil if empty certificate' do it 'returns ca_pem nil if empty certificate' do
subject.certificate = '' subject.certificate = ''
expect(terminal_specification[:ca_pem]).to be_nil expect(specification[:ca_pem]).to be_nil
end end
it 'adds Authorization header if authorization is present' do it 'adds Authorization header if authorization is present' do
subject.authorization = 'whatever' subject.authorization = 'whatever'
expect(terminal_specification[:headers]).to include(Authorization: ['whatever']) expect(specification[:headers]).to include(Authorization: ['whatever'])
end end
end end
end end
......
...@@ -117,6 +117,16 @@ describe Ci::Build do ...@@ -117,6 +117,16 @@ describe Ci::Build do
it 'returns the job' do it 'returns the job' do
is_expected.to include(job) is_expected.to include(job)
end end
context 'when ci_enable_legacy_artifacts feature flag is disabled' do
before do
stub_feature_flags(ci_enable_legacy_artifacts: false)
end
it 'does not return the job' do
is_expected.not_to include(job)
end
end
end end
context 'when job has a job artifact archive' do context 'when job has a job artifact archive' do
...@@ -471,6 +481,14 @@ describe Ci::Build do ...@@ -471,6 +481,14 @@ describe Ci::Build do
let(:build) { create(:ci_build, :legacy_artifacts) } let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
context 'when ci_enable_legacy_artifacts feature flag is disabled' do
before do
stub_feature_flags(ci_enable_legacy_artifacts: false)
end
it { is_expected.to be_falsy }
end
end end
end end
end end
......
...@@ -16,6 +16,10 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -16,6 +16,10 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
attr_reader :id attr_reader :id
def self.primary_key
:id
end
def initialize(id, &blk) def initialize(id, &blk)
@id = id @id = id
@calculator = blk @calculator = blk
...@@ -106,6 +110,46 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -106,6 +110,46 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end end
end end
describe '.reactive_cache_worker_finder' do
context 'with default reactive_cache_worker_finder' do
let(:args) { %w(other args) }
before do
allow(instance.class).to receive(:find_by).with(id: instance.id)
.and_return(instance)
end
it 'calls the activerecord find_by method' do
result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
expect(result).to eq(instance)
expect(instance.class).to have_received(:find_by).with(id: instance.id)
end
end
context 'with custom reactive_cache_worker_finder' do
let(:args) { %w(arg1 arg2) }
let(:instance) { CustomFinderCacheTest.new(666, &calculation) }
class CustomFinderCacheTest < CacheTest
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
def self.from_cache(*args); end
end
before do
allow(instance.class).to receive(:from_cache).with(*args).and_return(instance)
end
it 'overrides the default reactive_cache_worker_finder' do
result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
expect(result).to eq(instance)
expect(instance.class).to have_received(:from_cache).with(*args)
end
end
end
describe '#clear_reactive_cache!' do describe '#clear_reactive_cache!' do
before do before do
stub_reactive_cache(instance, 4) stub_reactive_cache(instance, 4)
......
...@@ -20,12 +20,16 @@ describe WikiPage do ...@@ -20,12 +20,16 @@ describe WikiPage do
context 'when there are pages' do context 'when there are pages' do
before do before do
create_page('dir_1/dir_1_1/page_3', 'content') create_page('dir_1/dir_1_1/page_3', 'content')
create_page('page_1', 'content')
create_page('dir_1/page_2', 'content') create_page('dir_1/page_2', 'content')
create_page('dir_2/page_5', 'content') create_page('dir_2/page_5', 'content')
create_page('page_6', 'content')
create_page('dir_2/page_4', 'content') create_page('dir_2/page_4', 'content')
create_page('page_1', 'content')
end end
let(:page_1) { wiki.find_page('page_1') } let(:page_1) { wiki.find_page('page_1') }
let(:page_6) { wiki.find_page('page_6') }
let(:dir_1) do let(:dir_1) do
WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
end end
...@@ -38,11 +42,26 @@ describe WikiPage do ...@@ -38,11 +42,26 @@ describe WikiPage do
WikiDirectory.new('dir_2', pages) WikiDirectory.new('dir_2', pages)
end end
context 'sort by title' do
let(:grouped_entries) { described_class.group_by_directory(wiki.pages) }
let(:expected_grouped_entries) { [dir_1_1, dir_1, dir_2, page_1, page_6] }
it 'returns an array with pages and directories' do it 'returns an array with pages and directories' do
expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2] grouped_entries.each_with_index do |page_or_dir, i|
expected_page_or_dir = expected_grouped_entries[i]
expected_slugs = get_slugs(expected_page_or_dir)
slugs = get_slugs(page_or_dir)
grouped_entries = described_class.group_by_directory(wiki.pages) expect(slugs).to match_array(expected_slugs)
end
end
end
context 'sort by created_at' do
let(:grouped_entries) { described_class.group_by_directory(wiki.pages(sort: 'created_at')) }
let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, dir_2, page_6] }
it 'returns an array with pages and directories' do
grouped_entries.each_with_index do |page_or_dir, i| grouped_entries.each_with_index do |page_or_dir, i|
expected_page_or_dir = expected_grouped_entries[i] expected_page_or_dir = expected_grouped_entries[i]
expected_slugs = get_slugs(expected_page_or_dir) expected_slugs = get_slugs(expected_page_or_dir)
...@@ -51,12 +70,10 @@ describe WikiPage do ...@@ -51,12 +70,10 @@ describe WikiPage do
expect(slugs).to match_array(expected_slugs) expect(slugs).to match_array(expected_slugs)
end end
end end
end
it 'returns an array sorted by alphabetical position' do it 'returns an array with retained order with directories at the top' do
# Directories and pages within directories are sorted alphabetically. expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
# Pages at root come before everything.
expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3',
'dir_2/page_4', 'dir_2/page_5']
grouped_entries = described_class.group_by_directory(wiki.pages) grouped_entries = described_class.group_by_directory(wiki.pages)
......
require 'spec_helper'
describe 'GitlabSchema configurations' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let!(:query) { graphql_query_for('project', 'fullPath' => project.full_path) }
it 'shows an error if complexity it too high' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
post_graphql(query, current_user: nil)
expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1')
end
end
...@@ -93,6 +93,8 @@ module GraphqlHelpers ...@@ -93,6 +93,8 @@ module GraphqlHelpers
end end
def all_graphql_fields_for(class_name, parent_types = Set.new) def all_graphql_fields_for(class_name, parent_types = Set.new)
allow_unlimited_graphql_complexity
type = GitlabSchema.types[class_name.to_s] type = GitlabSchema.types[class_name.to_s]
return "" unless type return "" unless type
...@@ -170,4 +172,10 @@ module GraphqlHelpers ...@@ -170,4 +172,10 @@ module GraphqlHelpers
field_type field_type
end end
# for most tests, we want to allow unlimited complexity
def allow_unlimited_graphql_complexity
allow_any_instance_of(GitlabSchema).to receive(:max_complexity).and_return nil
allow(GitlabSchema).to receive(:max_query_complexity).with(any_args).and_return nil
end
end end
...@@ -25,12 +25,16 @@ module PrometheusHelpers ...@@ -25,12 +25,16 @@ module PrometheusHelpers
"https://prometheus.example.com/api/v1/query?#{query}" "https://prometheus.example.com/api/v1/query?#{query}"
end end
def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f) def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now, step: nil)
start = start.to_f
stop = stop.to_f
step ||= Gitlab::PrometheusClient.compute_step(start, stop)
query = { query = {
query: prometheus_query, query: prometheus_query,
start: start.to_f, start: start,
end: stop, end: stop,
step: 1.minute.to_i step: step
}.to_query }.to_query
"https://prometheus.example.com/api/v1/query_range?#{query}" "https://prometheus.example.com/api/v1/query_range?#{query}"
......
# frozen_string_literal: true # frozen_string_literal: true
shared_examples 'confidential quick action' do shared_examples 'confidential quick action' do
context 'when the current user can update issues' do
it 'does not create a note, and marks the issue as confidential' do
add_note('/confidential')
expect(page).not_to have_content '/confidential'
expect(page).to have_content 'Commands applied'
expect(page).to have_content 'made the issue confidential'
expect(issue.reload).to be_confidential
end
end
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
end
it 'does not create a note, and does not mark the issue as confidential' do
add_note('/confidential')
expect(page).not_to have_content 'Commands applied'
expect(page).not_to have_content 'made the issue confidential'
expect(issue.reload).not_to be_confidential
end
end
end end
...@@ -645,17 +645,17 @@ ...@@ -645,17 +645,17 @@
dependencies: dependencies:
bootstrap "^4.1.3" bootstrap "^4.1.3"
"@gitlab/eslint-config@^1.4.0": "@gitlab/eslint-config@^1.5.0":
version "1.4.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.4.0.tgz#2e59e55a7cd024e3a450d2a896060ec4d763a5dc" resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.5.0.tgz#0c8c3ae74f276eb6671bd7c60f331bc0f2d2e5cf"
integrity sha512-nkecTWRNS/KD9q5lHFSc3J6zO/g1/OV9DaKiay+0nLjnGO9jQVRArRIYpnzgbUz2p15jOMVToVafW0YbbHZkwg== integrity sha512-KgJgoIZNpGauFpCV1iCptesYN7I8abtYRBLU9xcH0oocC/xp3JmbLfsZ+lEtrk8pl99Q2mKiAuaPpzxjXr6hBw==
dependencies: dependencies:
babel-eslint "^10.0.1" babel-eslint "^10.0.1"
eslint-config-airbnb-base "^13.1.0" eslint-config-airbnb-base "^13.1.0"
eslint-config-prettier "^3.3.0" eslint-config-prettier "^3.3.0"
eslint-plugin-filenames "^1.3.2" eslint-plugin-filenames "^1.3.2"
eslint-plugin-import "^2.14.0" eslint-plugin-import "^2.16.0"
eslint-plugin-promise "^4.0.1" eslint-plugin-promise "^4.1.1"
eslint-plugin-vue "^5.0.0" eslint-plugin-vue "^5.0.0"
"@gitlab/svgs@^1.58.0": "@gitlab/svgs@^1.58.0":
...@@ -3665,7 +3665,7 @@ eslint-import-resolver-jest@^2.1.1: ...@@ -3665,7 +3665,7 @@ eslint-import-resolver-jest@^2.1.1:
micromatch "^3.1.6" micromatch "^3.1.6"
resolve "^1.5.0" resolve "^1.5.0"
eslint-import-resolver-node@^0.3.1: eslint-import-resolver-node@^0.3.1, eslint-import-resolver-node@^0.3.2:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==
...@@ -3697,6 +3697,14 @@ eslint-module-utils@^2.2.0: ...@@ -3697,6 +3697,14 @@ eslint-module-utils@^2.2.0:
debug "^2.6.8" debug "^2.6.8"
pkg-dir "^1.0.0" pkg-dir "^1.0.0"
eslint-module-utils@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49"
integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==
dependencies:
debug "^2.6.8"
pkg-dir "^2.0.0"
eslint-plugin-filenames@^1.3.2: eslint-plugin-filenames@^1.3.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz#7094f00d7aefdd6999e3ac19f72cea058e590cf7" resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz#7094f00d7aefdd6999e3ac19f72cea058e590cf7"
...@@ -3730,6 +3738,22 @@ eslint-plugin-import@^2.14.0: ...@@ -3730,6 +3738,22 @@ eslint-plugin-import@^2.14.0:
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
resolve "^1.6.0" resolve "^1.6.0"
eslint-plugin-import@^2.16.0:
version "2.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f"
integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==
dependencies:
contains-path "^0.1.0"
debug "^2.6.9"
doctrine "1.5.0"
eslint-import-resolver-node "^0.3.2"
eslint-module-utils "^2.3.0"
has "^1.0.3"
lodash "^4.17.11"
minimatch "^3.0.4"
read-pkg-up "^2.0.0"
resolve "^1.9.0"
eslint-plugin-jasmine@^2.10.1: eslint-plugin-jasmine@^2.10.1:
version "2.10.1" version "2.10.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.10.1.tgz#5733b709e751f4bc40e31e1c16989bd2cdfbec97" resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.10.1.tgz#5733b709e751f4bc40e31e1c16989bd2cdfbec97"
...@@ -3740,10 +3764,10 @@ eslint-plugin-jest@^22.3.0: ...@@ -3740,10 +3764,10 @@ eslint-plugin-jest@^22.3.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2"
integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA== integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA==
eslint-plugin-promise@^4.0.1: eslint-plugin-promise@^4.1.1:
version "4.0.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
integrity sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg== integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==
eslint-plugin-vue@^5.0.0: eslint-plugin-vue@^5.0.0:
version "5.0.0" version "5.0.0"
...@@ -4909,6 +4933,13 @@ has@^1.0.1: ...@@ -4909,6 +4933,13 @@ has@^1.0.1:
dependencies: dependencies:
function-bind "^1.0.2" function-bind "^1.0.2"
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
hash-base@^2.0.0: hash-base@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
...@@ -8087,6 +8118,13 @@ pkg-dir@^1.0.0: ...@@ -8087,6 +8118,13 @@ pkg-dir@^1.0.0:
dependencies: dependencies:
find-up "^1.0.0" find-up "^1.0.0"
pkg-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
dependencies:
find-up "^2.1.0"
pkg-dir@^3.0.0: pkg-dir@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
...@@ -9094,7 +9132,7 @@ resolve@1.1.7, resolve@1.1.x: ...@@ -9094,7 +9132,7 @@ resolve@1.1.7, resolve@1.1.x:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.9.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
......
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