Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-17 18:09:20 +00:00
parent 6535cf9c79
commit c663374b3d
71 changed files with 481 additions and 234 deletions

View File

@ -43,11 +43,8 @@ Graphql/ResolverType:
Exclude:
- 'app/graphql/resolvers/base_resolver.rb'
- 'app/graphql/resolvers/ci/pipeline_stages_resolver.rb'
- 'app/graphql/resolvers/commit_pipelines_resolver.rb'
- 'app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb'
- 'app/graphql/resolvers/merge_request_pipelines_resolver.rb'
- 'app/graphql/resolvers/merge_requests_resolver.rb'
- 'app/graphql/resolvers/project_pipelines_resolver.rb'
- 'app/graphql/resolvers/users/group_count_resolver.rb'
- 'ee/app/graphql/resolvers/ci/jobs_resolver.rb'
- 'ee/app/graphql/resolvers/geo/merge_request_diff_registries_resolver.rb'

View File

@ -1 +1 @@
b8a027e4ea96235545000da5ff35d05a91b4370b
f869e2716122b17ce78508aacf981a098406d2d7

View File

@ -1,11 +1,14 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { GlLoadingIcon, GlButton, GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
GlButton,
GlAlert,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
props: {
isLoading: {
@ -59,7 +62,9 @@ export default {
{{ __('Start Web Terminal') }}
</gl-button>
</p>
<div v-if="!isValid && message" class="bs-callout gl-text-left" v-html="message"></div>
<gl-alert v-if="!isValid && message" variant="tip" :dismissible="false">
<span v-safe-html="message"></span>
</gl-alert>
<p v-else>
<a
v-if="helpPath"

View File

@ -29,6 +29,7 @@ export default {
},
mounted() {
window.addEventListener('resize', this.handleWindowResize);
this.updatePageContainerClass();
},
beforeDestroy() {
window.removeEventListener('resize', this.handleWindowResize);

View File

@ -5,12 +5,12 @@ import ModalManager from './components/user_modal_manager.vue';
import DeleteUserModal from './components/delete_user_modal.vue';
import UserOperationConfirmationModal from './components/user_operation_confirmation_modal.vue';
import csrf from '~/lib/utils/csrf';
import initConfirmModal from '~/confirm_modal';
const MODAL_TEXTS_CONTAINER_SELECTOR = '#modal-texts';
const MODAL_MANAGER_SELECTOR = '#user-modal';
const ACTION_MODALS = {
deactivate: UserOperationConfirmationModal,
block: UserOperationConfirmationModal,
delete: DeleteUserModal,
'delete-with-contributions': DeleteUserModal,
};
@ -62,4 +62,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
initConfirmModal();
});

View File

@ -0,0 +1,3 @@
export const DOWNSTREAM = 'downstream';
export const MAIN = 'main';
export const UPSTREAM = 'upstream';

View File

@ -5,6 +5,7 @@ import StageColumnComponent from './stage_column_component.vue';
import GraphWidthMixin from '../../mixins/graph_width_mixin';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
export default {
name: 'PipelineGraph',
@ -35,11 +36,11 @@ export default {
type: {
type: String,
required: false,
default: 'main',
default: MAIN,
},
},
upstream: 'upstream',
downstream: 'downstream',
upstream: UPSTREAM,
downstream: DOWNSTREAM,
data() {
return {
downstreamMarginTop: null,
@ -54,41 +55,41 @@ export default {
graph() {
return this.pipeline.details?.stages;
},
hasTriggeredBy() {
hasUpstream() {
return (
this.type !== this.$options.downstream &&
this.triggeredByPipelines &&
this.upstreamPipelines &&
this.pipeline.triggered_by !== null
);
},
triggeredByPipelines() {
upstreamPipelines() {
return this.pipeline.triggered_by;
},
hasTriggered() {
hasDownstream() {
return (
this.type !== this.$options.upstream &&
this.triggeredPipelines &&
this.downstreamPipelines &&
this.pipeline.triggered.length > 0
);
},
triggeredPipelines() {
downstreamPipelines() {
return this.pipeline.triggered;
},
expandedTriggeredBy() {
expandedUpstream() {
return (
this.pipeline.triggered_by &&
Array.isArray(this.pipeline.triggered_by) &&
this.pipeline.triggered_by.find(el => el.isExpanded)
);
},
expandedTriggered() {
expandedDownstream() {
return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
},
pipelineTypeUpstream() {
return this.type !== this.$options.downstream && this.expandedTriggeredBy;
return this.type !== this.$options.downstream && this.expandedUpstream;
},
pipelineTypeDownstream() {
return this.type !== this.$options.upstream && this.expandedTriggered;
return this.type !== this.$options.upstream && this.expandedDownstream;
},
pipelineProjectId() {
return this.pipeline.project.id;
@ -142,11 +143,11 @@ export default {
* and we want to reset the pipeline store. Triggering the reset without
* this condition would mean not allowing downstreams of downstreams to expand
*/
if (this.expandedTriggered?.id !== pipeline.id) {
this.$emit('onResetTriggered', this.pipeline, pipeline);
if (this.expandedDownstream?.id !== pipeline.id) {
this.$emit('onResetDownstream', this.pipeline, pipeline);
}
this.$emit('onClickTriggered', pipeline);
this.$emit('onClickDownstreamPipeline', pipeline);
},
calculateMarginTop(downstreamNode, pixelDiff) {
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
@ -154,8 +155,8 @@ export default {
hasOnlyOneJob(stage) {
return stage.groups.length === 1;
},
hasUpstream(index) {
return index === 0 && this.hasTriggeredBy;
hasUpstreamColumn(index) {
return index === 0 && this.hasUpstream;
},
setJob(jobName) {
this.jobName = jobName;
@ -192,30 +193,30 @@ export default {
<pipeline-graph
v-if="pipelineTypeUpstream"
type="upstream"
:type="$options.upstream"
class="d-inline-block upstream-pipeline"
:class="`js-upstream-pipeline-${expandedTriggeredBy.id}`"
:class="`js-upstream-pipeline-${expandedUpstream.id}`"
:is-loading="false"
:pipeline="expandedTriggeredBy"
:pipeline="expandedUpstream"
:is-linked-pipeline="true"
:mediator="mediator"
@onClickTriggeredBy="clickTriggeredByPipeline"
@onClickUpstreamPipeline="clickUpstreamPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
<linked-pipelines-column
v-if="hasTriggeredBy"
:linked-pipelines="triggeredByPipelines"
v-if="hasUpstream"
:type="$options.upstream"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:project-id="pipelineProjectId"
graph-position="left"
@linkedPipelineClick="$emit('onClickTriggeredBy', $event)"
@linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
/>
<ul
v-if="!isLoading"
:class="{
'inline js-has-linked-pipelines': hasTriggered || hasTriggeredBy,
'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
}"
class="stage-column-list align-top"
>
@ -223,7 +224,7 @@ export default {
v-for="(stage, index) in graph"
:key="stage.name"
:class="{
'has-upstream gl-ml-11': hasUpstream(index),
'has-upstream gl-ml-11': hasUpstreamColumn(index),
'has-only-one-job': hasOnlyOneJob(stage),
'gl-mr-26': shouldAddRightMargin(index),
}"
@ -231,7 +232,7 @@ export default {
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
:has-upstream="hasUpstream"
:action="stage.status.action"
:job-hovered="jobName"
:pipeline-expanded="pipelineExpanded"
@ -240,11 +241,11 @@ export default {
</ul>
<linked-pipelines-column
v-if="hasTriggered"
:linked-pipelines="triggeredPipelines"
v-if="hasDownstream"
:type="$options.downstream"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:project-id="pipelineProjectId"
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
@downstreamHovered="setJob"
@pipelineExpandToggle="setPipelineExpanded"
@ -252,15 +253,15 @@ export default {
<pipeline-graph
v-if="pipelineTypeDownstream"
type="downstream"
:type="$options.downstream"
class="d-inline-block"
:class="`js-downstream-pipeline-${expandedTriggered.id}`"
:class="`js-downstream-pipeline-${expandedDownstream.id}`"
:is-loading="false"
:pipeline="expandedTriggered"
:pipeline="expandedDownstream"
:is-linked-pipeline="true"
:style="{ 'margin-top': downstreamMarginTop }"
:mediator="mediator"
@onClickTriggered="clickTriggeredPipeline"
@onClickDownstreamPipeline="clickDownstreamPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</div>

View File

@ -2,6 +2,7 @@
import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
import { UPSTREAM, DOWNSTREAM } from './constants';
export default {
directives: {
@ -14,6 +15,10 @@ export default {
GlLoadingIcon,
},
props: {
columnTitle: {
type: String,
required: true,
},
pipeline: {
type: Object,
required: true,
@ -22,7 +27,7 @@ export default {
type: Number,
required: true,
},
columnTitle: {
type: {
type: String,
required: true,
},
@ -50,12 +55,10 @@ export default {
return this.childPipeline ? __('child-pipeline') : this.pipeline.project.name;
},
parentPipeline() {
// Refactor string match when BE returns Upstream/Downstream indicators
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Upstream');
return this.isUpstream && this.isSameProject;
},
childPipeline() {
// Refactor string match when BE returns Upstream/Downstream indicators
return this.projectId === this.pipeline.project.id && this.isDownstream;
return this.isDownstream && this.isSameProject;
},
label() {
if (this.parentPipeline) {
@ -66,7 +69,13 @@ export default {
return __('Multi-project');
},
isDownstream() {
return this.columnTitle === __('Downstream');
return this.type === DOWNSTREAM;
},
isUpstream() {
return this.type === UPSTREAM;
},
isSameProject() {
return this.projectId === this.pipeline.project.id;
},
sourceJobInfo() {
return this.isDownstream
@ -74,13 +83,13 @@ export default {
: '';
},
expandedIcon() {
if (this.parentPipeline) {
if (this.isUpstream) {
return this.expanded ? 'angle-right' : 'angle-left';
}
return this.expanded ? 'angle-left' : 'angle-right';
},
expandButtonPosition() {
return this.parentPipeline ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!';
return this.isUpstream ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!';
},
},
methods: {
@ -116,7 +125,7 @@ export default {
>
<div
class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1"
:class="{ 'gl-pl-9': parentPipeline }"
:class="{ 'gl-pl-9': isUpstream }"
>
<div class="gl-display-flex">
<ci-status

View File

@ -1,6 +1,6 @@
<script>
import LinkedPipeline from './linked_pipeline.vue';
import { __ } from '~/locale';
import { UPSTREAM } from './constants';
export default {
components: {
@ -15,7 +15,7 @@ export default {
type: Array,
required: true,
},
graphPosition: {
type: {
type: String,
required: true,
},
@ -32,9 +32,12 @@ export default {
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
// Refactor string match when BE returns Upstream/Downstream indicators
isUpstream() {
return this.columnTitle === __('Upstream');
return this.type === UPSTREAM;
},
},
methods: {
@ -45,6 +48,11 @@ export default {
this.$emit('downstreamHovered', jobName);
},
onPipelineExpandToggle(jobName, expanded) {
// Highlighting only applies to downstream pipelines
if (this.isUpstream) {
return;
}
this.$emit('pipelineExpandToggle', jobName, expanded);
},
},
@ -66,6 +74,7 @@ export default {
:pipeline="pipeline"
:column-title="columnTitle"
:project-id="projectId"
:type="type"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
@pipelineExpandToggle="onPipelineExpandToggle"

View File

@ -41,13 +41,13 @@ export default {
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
}
},
resetTriggeredPipelines(parentPipeline, pipeline) {
resetDownstreamPipelines(parentPipeline, pipeline) {
this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline);
},
clickTriggeredByPipeline(pipeline) {
clickUpstreamPipeline(pipeline) {
this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
clickTriggeredPipeline(pipeline) {
clickDownstreamPipeline(pipeline) {
this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
requestRefreshPipelineGraph() {

View File

@ -1,6 +1,6 @@
export default {
props: {
hasTriggeredBy: {
hasUpstream: {
type: Boolean,
required: false,
default: false,
@ -8,7 +8,7 @@ export default {
},
methods: {
buildConnnectorClass(index) {
return index === 0 && (!this.isFirstColumn || this.hasTriggeredBy) ? 'left-connector' : '';
return index === 0 && (!this.isFirstColumn || this.hasUpstream) ? 'left-connector' : '';
},
},
};

View File

@ -45,10 +45,10 @@ const createLegacyPipelinesDetailApp = mediator => {
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
onResetTriggered: (parentPipeline, pipeline) =>
this.resetTriggeredPipelines(parentPipeline, pipeline),
onClickTriggeredBy: pipeline => this.clickTriggeredByPipeline(pipeline),
onClickTriggered: pipeline => this.clickTriggeredPipeline(pipeline),
onResetDownstream: (parentPipeline, pipeline) =>
this.resetDownstreamPipelines(parentPipeline, pipeline),
onClickUpstreamPipeline: pipeline => this.clickUpstreamPipeline(pipeline),
onClickDownstreamPipeline: pipeline => this.clickDownstreamPipeline(pipeline),
},
});
},

View File

@ -18,29 +18,32 @@ export default class PrometheusMetrics {
this.$monitoredMetricsList = this.$monitoredMetricsPanel.find('.js-metrics-list');
this.$missingEnvVarPanel = this.$wrapper.find('.js-panel-missing-env-vars');
this.$panelToggle = this.$missingEnvVarPanel.find('.js-panel-toggle');
this.$panelToggleRight = this.$missingEnvVarPanel.find('.js-panel-toggle-right');
this.$panelToggleDown = this.$missingEnvVarPanel.find('.js-panel-toggle-down');
this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count');
this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list');
this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('activeMetrics');
this.helpMetricsPath = this.$monitoredMetricsPanel.data('metrics-help-path');
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
this.$panelToggleRight.on('click', e => this.handlePanelToggle(e));
this.$panelToggleDown.on('click', e => this.handlePanelToggle(e));
}
init() {
this.loadActiveMetrics();
}
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
const $currentPanelBody = $toggleBtn.closest('.card').find('.card-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
} else {
$toggleBtn.removeClass('fa-caret-right').addClass('fa-caret-down');
if ($toggleBtn.hasClass('js-panel-toggle-right')) {
$toggleBtn.addClass('hidden');
this.$panelToggleDown.removeClass('hidden');
} else if ($toggleBtn.hasClass('js-panel-toggle-down')) {
$toggleBtn.addClass('hidden');
this.$panelToggleRight.removeClass('hidden');
}
}

View File

@ -6,11 +6,8 @@ import { GlIcon } from '@gitlab/ui';
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-foss/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:

View File

@ -1,5 +1,5 @@
<script>
import { GlModal } from '@gitlab/ui';
import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import csrf from '~/lib/utils/csrf';
@ -7,6 +7,9 @@ export default {
components: {
GlModal,
},
directives: {
SafeHtml,
},
props: {
selector: {
type: String,
@ -71,7 +74,8 @@ export default {
-->
<input type="hidden" name="_method" :value="method" />
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<div>{{ modalAttributes.message }}</div>
<div v-if="modalAttributes.messageHtml" v-safe-html="modalAttributes.messageHtml"></div>
<div v-else>{{ modalAttributes.message }}</div>
</form>
</gl-modal>
</template>

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
# rubocop: disable Graphql/ResolverType
module Resolvers
class CommitPipelinesResolver < BaseResolver
# The GraphQL type here gets defined in this include
include ::ResolvesPipelines
alias_method :commit, :object
@ -11,3 +13,4 @@ module Resolvers
end
end
end
# rubocop: enable Graphql/ResolverType

View File

@ -4,7 +4,7 @@ module ResolvesPipelines
extend ActiveSupport::Concern
included do
type [Types::Ci::PipelineType], null: false
type Types::Ci::PipelineType.connection_type, null: false
argument :status,
Types::Ci::PipelineStatusEnum,
required: false,

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
# rubocop: disable Graphql/ResolverType
module Resolvers
class MergeRequestPipelinesResolver < BaseResolver
# The GraphQL type here gets defined in this include
include ::ResolvesPipelines
alias_method :merge_request, :object
@ -18,3 +20,4 @@ module Resolvers
end
end
end
# rubocop: enable Graphql/ResolverType

View File

@ -1,4 +1,7 @@
# frozen_string_literal: true
# The GraphQL type here gets defined in
# https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/resolvers/concerns/resolves_pipelines.rb#L7
# rubocop: disable Graphql/ResolverType
module Resolvers
class ProjectPipelinesResolver < BaseResolver
@ -22,3 +25,4 @@ module Resolvers
end
end
end
# rubocop: enable Graphql/ResolverType

View File

@ -40,7 +40,7 @@ module Types
field :author, type: Types::UserType, null: true,
description: 'Author of the commit'
field :pipelines, Types::Ci::PipelineType.connection_type,
field :pipelines,
null: true,
description: 'Pipelines of the commit ordered latest first',
resolver: Resolvers::CommitPipelinesResolver

View File

@ -114,7 +114,7 @@ module Types
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline,
description: 'The pipeline running on the branch HEAD of the merge request'
field :pipelines, Types::Ci::PipelineType.connection_type,
field :pipelines,
null: true,
description: 'Pipelines for the merge request',
resolver: Resolvers::MergeRequestPipelinesResolver

View File

@ -187,7 +187,6 @@ module Types
resolver: Resolvers::PackagesResolver
field :pipelines,
Types::Ci::PipelineType.connection_type,
null: true,
description: 'Build pipelines of the project',
extras: [:lookahead],

View File

@ -110,6 +110,32 @@ module UsersHelper
!user.confirmed?
end
def user_block_data(user, message)
{
path: block_admin_user_path(user),
method: 'put',
modal_attributes: {
title: s_('AdminUsers|Block user %{username}?') % { username: sanitize_name(user.name) },
messageHtml: message,
okVariant: 'warning',
okTitle: s_('AdminUsers|Block')
}.to_json
}
end
def user_block_effects
header = tag.p s_('AdminUsers|Blocking user has the following effects:')
list = tag.ul do
concat tag.li s_('AdminUsers|User will not be able to login')
concat tag.li s_('AdminUsers|User will not be able to access git repositories')
concat tag.li s_('AdminUsers|Personal projects will be left')
concat tag.li s_('AdminUsers|Owned groups will be left')
end
header + list
end
private
def blocked_user_badge(user)

View File

@ -39,8 +39,6 @@ module Ci
end
def track_test_cases(build, test_suite)
return if Feature.disabled?(:track_unique_test_cases_parsed, build.project)
track_usage_event(EVENT_NAME, test_case_hashes(build, test_suite))
end

View File

@ -56,7 +56,7 @@ module Snippets
snippet_saved
rescue => e # Rescuing all because we can receive Creation exceptions, GRPC exceptions, Git exceptions, ...
log_error(e.message)
Gitlab::ErrorTracking.log_exception(e, service: 'Snippets::CreateService')
# If the commit action failed we need to remove the repository if exists
delete_repository(@snippet) if @snippet.repository_exists?

View File

@ -74,7 +74,7 @@ module Snippets
add_snippet_repository_error(snippet: snippet, error: e)
log_error(e.message)
Gitlab::ErrorTracking.log_exception(e, service: 'Snippets::UpdateService')
# If the commit action failed we remove it because
# we don't want to leave empty repositories

View File

@ -2,10 +2,7 @@
.card-header.bg-warning.text-white
= s_('AdminUsers|Block this user')
.card-body
= render partial: 'admin/users/user_block_effects'
= user_block_effects
%br
%button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'block',
content: s_('AdminUsers|You can always unblock their account, their data will remain intact.'),
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
%button.btn.gl-button.btn-warning.js-confirm-modal-button{ data: user_block_data(user, s_('AdminUsers|You can always unblock their account, their data will remain intact.')) }
= s_('AdminUsers|Block user')

View File

@ -5,11 +5,6 @@
action: s_("AdminUsers|Deactivate") } }
= render partial: 'admin/users/user_deactivation_effects'
%div{ data: { modal: "block",
title: s_("AdminUsers|Block user %{username}?"),
action: s_("AdminUsers|Block") } }
= render partial: 'admin/users/user_block_effects'
%div{ data: { modal: "delete",
title: s_("AdminUsers|Delete User %{username}?"),
action: s_('AdminUsers|Delete user'),

View File

@ -24,10 +24,10 @@
.table-action-buttons
= link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn gl-button btn-default'
- unless user == current_user
%button.dropdown-new.btn.gl-button.btn-default{ type: 'button', data: { toggle: 'dropdown' } }
%button.dropdown-new.btn.gl-button.btn-default{ type: 'button', data: { testid: "user-action-button-#{user.id}", toggle: 'dropdown' } }
= sprite_icon('settings')
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right
%ul.dropdown-menu.dropdown-menu-right{ data: { testid: "user-action-dropdown-#{user.id}" } }
%li.dropdown-header
= _('Settings')
%li
@ -37,16 +37,12 @@
- elsif user.blocked?
- if user.blocked_pending_approval?
= link_to s_('AdminUsers|Approve'), approve_admin_user_path(user), method: :put
%button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'block',
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
%button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_block_data(user, user_block_effects) }
= s_('AdminUsers|Block')
- else
= link_to _('Unblock'), unblock_admin_user_path(user), method: :put
- else
%button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'block',
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
%button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_block_data(user, user_block_effects) }
= s_('AdminUsers|Block')
- if user.can_be_deactivated?
%li

View File

@ -1,11 +0,0 @@
%p
= s_('AdminUsers|Blocking user has the following effects:')
%ul
%li
= s_('AdminUsers|User will not be able to login')
%li
= s_('AdminUsers|User will not be able to access git repositories')
%li
= s_('AdminUsers|Personal projects will be left')
%li
= s_('AdminUsers|Owned groups will be left')

View File

@ -6,5 +6,5 @@
= form_for @project, url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' } do |f|
%p
%strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.')
%strong= _('Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.')
= button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }

View File

@ -25,7 +25,8 @@
.card.hidden.js-panel-missing-env-vars
.card-header
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
= sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right' )
= sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden' )
= s_('PrometheusService|Missing environment variable')
%span.badge.badge-pill.js-env-var-count 0
.card-body.hidden

View File

@ -119,6 +119,9 @@
%li
pytest-cov (Python) -
%code ^TOTAL.+?(\d+\%)$
%li
Scoverage (Scala) -
%code Statement coverage[A-Za-z\.*]\s*:\s*([^%]+)
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%

View File

@ -0,0 +1,5 @@
---
title: Remove columns no longer used for replicating terraform state
merge_request: 46742
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Change wording on the project remove fork page
merge_request: 47878
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Remove feature flag to enable tracking unique test cases parsed globally.
merge_request: 47662
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Added code coverage regex for Scala Scoverage
merge_request: 46638
author: opensorceror
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace font-awesome icons in prometheus config
merge_request: 47713
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update terminal empty state alert to gl component
merge_request: 47340
author:
type: other

View File

@ -1,8 +0,0 @@
---
name: track_unique_test_cases_parsed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918
rollout_issue_url:
milestone: '13.5'
type: development
group: group::testing
default_enabled: false

View File

@ -557,10 +557,6 @@ module.exports = {
port: DEV_SERVER_PORT,
public: DEV_SERVER_PUBLIC_ADDR,
https: DEV_SERVER_HTTPS,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
},
contentBase: false,
stats: 'errors-only',
hot: DEV_SERVER_LIVERELOAD,

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class RemoveTerraformStateVerificationColumns < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
transaction do
remove_column :terraform_states, :verification_retry_at, :datetime_with_timezone
remove_column :terraform_states, :verified_at, :datetime_with_timezone
remove_column :terraform_states, :verification_retry_count, :integer, limit: 2
remove_column :terraform_states, :verification_checksum, :binary, using: 'verification_checksum::bytea'
remove_column :terraform_states, :verification_failure, :text
end
end
def down
add_column(:terraform_states, :verification_retry_at, :datetime_with_timezone) unless column_exists?(:terraform_states, :verification_retry_at)
add_column(:terraform_states, :verified_at, :datetime_with_timezone) unless column_exists?(:terraform_states, :verified_at)
add_column(:terraform_states, :verification_retry_count, :integer, limit: 2) unless column_exists?(:terraform_states, :verification_retry_count)
add_column(:terraform_states, :verification_checksum, :binary, using: 'verification_checksum::bytea') unless column_exists?(:terraform_states, :verification_checksum)
add_column(:terraform_states, :verification_failure, :text) unless column_exists?(:terraform_states, :verification_failure)
add_text_limit :terraform_states, :verification_failure, 255
end
end

View File

@ -0,0 +1 @@
8a30cf20f71e0cb198caf6f77a19e4da55b83eb38bdb5888cd2674373d94cede

View File

@ -16623,13 +16623,7 @@ CREATE TABLE terraform_states (
locked_by_user_id bigint,
uuid character varying(32) NOT NULL,
name character varying(255),
verification_retry_at timestamp with time zone,
verified_at timestamp with time zone,
verification_retry_count smallint,
verification_checksum bytea,
verification_failure text,
versioning_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT check_21a47163ea CHECK ((char_length(verification_failure) <= 255))
versioning_enabled boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE terraform_states_id_seq

View File

@ -127,8 +127,11 @@ and verification status on a **secondary** node.
You can keep track of the progress to implement the missing items in
these epics/issues:
- [Unreplicated Data Types](https://gitlab.com/groups/gitlab-org/-/epics/893)
- [Verify all replicated data](https://gitlab.com/groups/gitlab-org/-/epics/1430)
- [Geo: Build a scalable, self-service Geo replication and verification framework](https://gitlab.com/groups/gitlab-org/-/epics/2161)
- [Geo: Improve the self-service Geo replication framework](https://gitlab.com/groups/gitlab-org/-/epics/3761)
- [Geo: Move existing blobs to framework](https://gitlab.com/groups/gitlab-org/-/epics/3588)
- [Geo: Add unreplicated data types](https://gitlab.com/groups/gitlab-org/-/epics/893)
- [Geo: Support GitLab Pages](https://gitlab.com/groups/gitlab-org/-/epics/589)
### Replicated data types behind a feature flag

View File

@ -553,7 +553,6 @@ database encryption. Proceed with caution.
1. On the **GitLab server**, to enable Pages, add the following to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_pages['enable'] = true
pages_external_url "http://<pages_server_URL>"
```
@ -584,18 +583,11 @@ database encryption. Proceed with caution.
to include:
```ruby
external_url 'http://<gitlab_server_IP_or_URL>'
roles ['pages_role']
pages_external_url "http://<pages_server_URL>"
postgresql['enable'] = false
redis['enable'] = false
prometheus['enable'] = false
puma['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitaly['enable'] = false
alertmanager['enable'] = false
node_exporter['enable'] = false
gitlab_rails['auto_migrate'] = false
gitlab_pages['gitlab_server'] = 'http://<gitlab_server_IP_or_URL>'
```
1. Create a backup of the secrets file on the **Pages server**:

View File

@ -72,9 +72,11 @@ Only API version v4 is available. Version v3 was removed in
API requests should be prefixed with both `api` and the API version. The API
version is defined in [`lib/api.rb`](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/api/api.rb).
For example, the root of the v4 API is at `/api/v4`.
For example, the root of the v4 API is at `/api/v4`. The following sections illustrate different uses:
Example of a valid API request using cURL:
### Valid API request
If you have a GitLab instance at `gitlab.example.com`:
```shell
curl "https://gitlab.example.com/api/v4/projects"
@ -83,6 +85,29 @@ curl "https://gitlab.example.com/api/v4/projects"
The API uses JSON to serialize data. You don't need to specify `.json` at the
end of an API URL.
### API request to expose HTTP response headers
If you want to expose HTTP response headers, use the `--include` option:
```shell
curl --include "https://gitlab.example.com/api/v4/projects"
HTTP/2 200
...
```
This can help you investigate an unexpected response.
### API Request that includes the exit code
If you want to expose the HTTP exit code, include the `--fail` option:
```shell script
curl --fail "https://gitlab.example.com/api/v4/does-not-exist"
curl: (22) The requested URL returned error: 404
```
The HTTP exit code can help you diagnose the success or failure of your REST call.
## Authentication
Most API requests require authentication, or will return public data only when

View File

@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/269) in GitLab 13.2.
Each Sidekiq worker, controller action, or (eventually) API endpoint
Each Sidekiq worker, controller action, or API endpoint
must declare a `feature_category` attribute. This attribute maps each
of these to a [feature
category](https://about.gitlab.com/handbook/product/product-categories/). This
@ -118,3 +118,42 @@ assigned to all actions.
The spec also validates if the used feature categories are known. And if
the actions used in configuration still exist as routes.
## API endpoints
Grape API endpoints can use the `feature_category` class method, like
[Rails controllers](#rails-controllers) do:
```ruby
module API
class Issues < ::API::Base
feature_category :issue_tracking
end
end
```
The second argument can be used to specify feature categories for
specific routes:
```ruby
module API
class Users < ::API::Base
feature_category :users, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key']
end
end
```
Or the feature category can be specified in the action itself:
```ruby
module API
class Users < ::API::Base
get ':id', feature_category: :users do
end
end
end
```
As with Rails controllers, an API class must specify the category for
every single action unless the same category is used for every action
within that class.

View File

@ -735,7 +735,8 @@ In order to add data for aggregated metrics into Usage Ping payload you should a
- name: unique name under which aggregate metric will be added to Usage Ping payload
- operator: operator that defines how aggregated metric data will be counted. Available operators are:
- `ANY`: removes duplicates and counts all entries that triggered any of listed events
- `OR`: removes duplicates and counts all entries that triggered any of listed events
- `AND`: removes duplicates and counts all elements that were observed triggering all of following events
- events: list of events names (from [`known_events.yml`](#known-events-in-usage-data-payload)) to aggregate into metric. All events in this list must have the same `redis_slot` and `aggregation` attributes.
- feature_flag: name of [development feature flag](../feature_flags/development.md#development-type) that will be checked before
metrics aggregation is performed. Corresponding feature flag should have `default_enabled` attribute set to `false`.
@ -744,11 +745,11 @@ metrics aggregation is performed. Corresponding feature flag should have `defaul
Example aggregated metric entries:
```yaml
- name: example_aggregated_metric
operator: ANY
events: ['i_search_advanced', 'i_search_paid']
- name: example_aggregated_metric_with_feautre_flag
operator: ANY
- name: product_analytics_test_metrics_union
operator: OR
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
- name: product_analytics_test_metrics_intersection_with_feautre_flag
operator: AND
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
feature_flag: example_aggregated_metric
```
@ -766,7 +767,8 @@ Aggregated metrics will be added under `aggregated_metrics` key in both `counts_
:project_snippets => 407,
:promoted_issues => 719,
:aggregated_metrics => {
:example_aggregated_metric => 7
:product_analytics_test_metrics_union => 7,
:product_analytics_test_metrics_intersection_with_feautre_flag => 2
},
:snippets => 2513
}

View File

@ -4,7 +4,10 @@ group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# SAST Analyzers **(ULTIMATE)**
# SAST Analyzers **(CORE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3775) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3.
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/2098) to GitLab Core in 13.3.
SAST relies on underlying third party tools that are wrapped into what we call
"Analyzers". An analyzer is a
@ -129,11 +132,11 @@ The [Security Scanner Integration](../../../development/integrations/secure.md)
| End line | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| Start column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 |
| End column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| External ID (e.g. CVE) | 𐄂 | 𐄂 | ⚠ | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| External ID (for example, CVE) | 𐄂 | 𐄂 | ⚠ | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| URLs | ✓ | 𐄂 | ✓ | 𐄂 | ⚠ | 𐄂 | ⚠ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| Internal doc/explanation | ✓ | ⚠ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ |
| Solution | ✓ | 𐄂 | 𐄂 | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| Affected item (e.g. class or package) | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| Affected item (for example, class or package) | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| Confidence | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | x | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ |
| Source code extract | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 |
| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | ✓ | ✓ | ✓ |
@ -143,4 +146,4 @@ The [Security Scanner Integration](../../../development/integrations/secure.md)
- 𐄂 => we don't have that data or it would need to develop specific or inefficient/unreliable logic to obtain it.
The values provided by these tools are heterogeneous so they are sometimes
normalized into common values (e.g., `severity`, `confidence`, etc).
normalized into common values (for example, `severity`, `confidence`, and so on).

View File

@ -160,6 +160,25 @@ gitops:
...
```
GitLab [versions 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) also
supports manifest projects containing multiple directories (or subdirectories)
of YAML files. To use multiple YAML files, specify a `paths` attribute:
```yaml
gitops:
manifest_projects:
- id: "path-to/your-manifest-project-number1"
paths:
# Read all .yaml files from team1/app1 directory.
# See https://github.com/bmatcuk/doublestar#about and
# https://pkg.go.dev/github.com/bmatcuk/doublestar/v2#Match for globbing rules.
- glob: '/team1/app1/*.yaml'
# Read all .yaml files from team2/apps and all subdirectories
- glob: '/team2/apps/**/*.yaml'
# If 'paths' is not specified or is an empty list, the configuration below is used
- glob: '/**/*.{yaml,yml,json}'
```
### Create an Agent record in GitLab
Next, create an GitLab Rails Agent record so the Agent can associate itself with

View File

@ -107,7 +107,7 @@ The chart used to install this application depends on the version of GitLab used
- GitLab 12.3 and newer, the [`jetstack/cert-manager`](https://github.com/jetstack/cert-manager)
chart is used with a [`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/cert_manager/values.yaml)
file.
- GitLab 12.2 and older, the [`stable/cert-manager`](https://gi2wthub.com/helm/charts/tree/master/stable/cert-manager)
- GitLab 12.2 and older, the [`stable/cert-manager`](https://github.com/helm/charts/tree/master/stable/cert-manager)
chart was used.
If you installed cert-manager prior to GitLab 12.3, Let's Encrypt
@ -775,7 +775,7 @@ certManager:
You can customize the installation of cert-manager by defining a
`.gitlab/managed-apps/cert-manager/values.yaml` file in your cluster
management project. Refer to the
[chart](https://hub.helm.sh/charts/jetstack/cert-manager) for the
[chart](https://github.com/jetstack/cert-manager) for the
available configuration options.
Support for installing the Cert Manager managed application is provided by the
@ -855,7 +855,7 @@ least 2 people from the
### Install PostHog using GitLab CI/CD
[PostHog](https://www.posthog.com) 🦔 is a developer-friendly, open-source product analytics platform.
[PostHog](https://posthog.com) 🦔 is a developer-friendly, open-source product analytics platform.
To install PostHog into the `gitlab-managed-apps` namespace of your cluster,
define the `.gitlab/managed-apps/config.yaml` file with:
@ -1012,15 +1012,15 @@ CAUTION: **Caution:**
Installation and removal of the Cilium requires a **manual**
[restart](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-gke/#restart-unmanaged-pods)
of all affected pods in all namespaces to ensure that they are
[managed](https://docs.cilium.io/en/stable/troubleshooting/#ensure-pod-is-managed-by-cilium)
[managed](https://docs.cilium.io/en/v1.8/operations/troubleshooting/#ensure-managed-pod)
by the correct networking plugin.
NOTE: **Note:**
Major upgrades might require additional setup steps. For more information, see
the official [upgrade guide](https://docs.cilium.io/en/stable/install/upgrade/).
the official [upgrade guide](https://docs.cilium.io/en/v1.8/operations/upgrade/).
By default, Cilium's
[audit mode](https://docs.cilium.io/en/v1.8/gettingstarted/policy-creation/?highlight=policy-audit#enable-policy-audit-mode)
[audit mode](https://docs.cilium.io/en/v1.8/gettingstarted/policy-creation/#enable-policy-audit-mode)
is enabled. In audit mode, Cilium doesn't drop disallowed packets. You
can use `policy-verdict` log to observe policy-related decisions. You
can disable audit mode by adding the following to

View File

@ -152,7 +152,7 @@ Shared runners provided by GitLab are **not** configurable. Consider [installing
Linux shared runners on GitLab.com run in [autoscale mode](https://docs.gitlab.com/runner/configuration/autoscale.html) and are powered by Google Cloud Platform.
Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security. They're free to use for public open source projects and limited
to 2000 CI minutes per month per group for private projects. More minutes
to 400 CI minutes per month per group for private projects. More minutes
[can be purchased](../../subscriptions/gitlab_com/index.md#purchase-additional-ci-minutes), if
needed. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/).

View File

@ -112,7 +112,7 @@ name or a referenced merge request or your project has an active
fork relationship.
If you would like to make this button appear, a possible workaround is to [remove your project's
fork relationship](../settings/index.md#removing-a-fork-relationship). Once removed, the fork
relationship cannot be restored, and you will no longer be able to send merge requests to the source.
relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.
![Create Button](img/web_editor_new_branch_from_issue_create_button_v12_6.png)

View File

@ -13,9 +13,8 @@ module Gitlab
'repositories/git_http' => %w{git_upload_pack}
}.freeze
ALLOWLISTED_GIT_LFS_ROUTES = {
'repositories/lfs_api' => %w{batch},
'repositories/lfs_locks_api' => %w{verify create unlock}
ALLOWLISTED_GIT_LFS_BATCH_ROUTES = {
'repositories/lfs_api' => %w{batch}
}.freeze
ALLOWLISTED_GIT_REVISION_ROUTES = {
@ -88,7 +87,7 @@ module Gitlab
# Overridden in EE module
def allowlisted_routes
workhorse_passthrough_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || session_route? || graphql_query?
workhorse_passthrough_route? || internal_route? || lfs_batch_route? || compare_git_revisions_route? || sidekiq_route? || session_route? || graphql_query?
end
# URL for requests passed through gitlab-workhorse to rails-web
@ -112,15 +111,13 @@ module Gitlab
ALLOWLISTED_GIT_REVISION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
def lfs_route?
# Batch upload requests are blocked in:
# https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/repositories/lfs_api_controller.rb#L106
def lfs_batch_route?
# Calling route_hash may be expensive. Only do it if we think there's a possible match
unless request.path.end_with?('/info/lfs/objects/batch',
'/info/lfs/locks', '/info/lfs/locks/verify') ||
%r{/info/lfs/locks/\d+/unlock\z}.match?(request.path)
return false
end
return unless request.path.end_with?('/info/lfs/objects/batch')
ALLOWLISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
ALLOWLISTED_GIT_LFS_BATCH_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
def session_route?

View File

@ -1,18 +1,17 @@
#- name: unique name of aggregated metric
# operator: aggregation operator. Valid values are:
# - "ANY": counts unique elements that were observed triggering any of following events
# - "ALL": counts unique elements that were observed triggering all of following events
# - "OR": counts unique elements that were observed triggering any of following events
# - "AND": counts unique elements that were observed triggering all of following events
# events: list of events names to aggregate into metric. All events in this list must have the same 'redis_slot' and 'aggregation' attributes
# see from lib/gitlab/usage_data_counters/known_events/ for the list of valid events.
# feature_flag: name of development feature flag that will be checked before metrics aggregation is performed.
# Corresponding feature flag should have `default_enabled` attribute set to `false`.
# This attribute is OPTIONAL and can be omitted, when `feature_flag` is missing no feature flag will be checked.
---
- name: product_analytics_test_aggregated_metrics
operator: ANY
- name: product_analytics_test_metrics_union
operator: OR
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
feature_flag: product_analytics_aggregated_metrics
- name: product_analytics_test_combined_events
operator: ALL
- name: product_analytics_test_metrics_intersection
operator: AND
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
feature_flag: product_analytics_aggregated_metrics

View File

@ -18,8 +18,8 @@ module Gitlab
KNOWN_EVENTS_PATH = File.expand_path('known_events/*.yml', __dir__)
ALLOWED_AGGREGATIONS = %i(daily weekly).freeze
UNION_OF_AGGREGATED_METRICS = 'ANY'
INTERSECTION_OF_AGGREGATED_METRICS = 'ALL'
UNION_OF_AGGREGATED_METRICS = 'OR'
INTERSECTION_OF_AGGREGATED_METRICS = 'AND'
ALLOWED_METRICS_AGGREGATIONS = [UNION_OF_AGGREGATED_METRICS, INTERSECTION_OF_AGGREGATED_METRICS].freeze
AGGREGATED_METRICS_PATH = File.expand_path('aggregated_metrics/*.yml', __dir__)

View File

@ -18967,7 +18967,7 @@ msgstr ""
msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}."
msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgid "Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks."
msgstr ""
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."

View File

@ -20,13 +20,21 @@ cp Dockerfile.assets assets_container.build/
COMMIT_REF_SLUG_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG}
COMMIT_SHA_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA}
COMMIT_REF_NAME_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
DESTINATIONS="--destination=$COMMIT_REF_SLUG_DESTINATION --destination=$COMMIT_SHA_DESTINATION"
# For EE branch builds, add a truncated SHA destination for later use by Omnibus
# auto-deploy builds
if [[ "${ASSETS_IMAGE_NAME}" == "gitlab-assets-ee" ]] && [ -n "$CI_COMMIT_BRANCH" ]
then
COMMIT_SHORT_SHA_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA:0:11}
DESTINATIONS="$DESTINATIONS --destination=$COMMIT_SHORT_SHA_DESTINATION"
fi
# Also tag the image with GitLab version, if running on a tag pipeline, so
# other projects can simply use that instead of computing the slug.
if [ -n "$CI_COMMIT_TAG" ]; then
COMMIT_REF_NAME_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
DESTINATIONS="$DESTINATIONS --destination=$COMMIT_REF_NAME_DESTINATION"
fi

View File

@ -204,6 +204,32 @@ RSpec.describe "Admin::Users" do
expect(page).to have_content(user.email)
end
end
context 'when blocking a user' do
it 'shows confirmation and allows blocking', :js do
expect(page).to have_content(user.email)
find("[data-testid='user-action-button-#{user.id}']").click
within find("[data-testid='user-action-dropdown-#{user.id}']") do
find('li button', text: 'Block').click
end
wait_for_requests
expect(page).to have_content('Block user')
expect(page).to have_content('Blocking user has the following effects')
expect(page).to have_content('User will not be able to login')
expect(page).to have_content('Owned groups will be left')
find('.modal-footer button', text: 'Block').click
wait_for_requests
expect(page).to have_content('Successfully blocked')
expect(page).not_to have_content(user.email)
end
end
end
describe "GET /admin/users/new" do
@ -362,6 +388,26 @@ RSpec.describe "Admin::Users" do
end
end
context 'when blocking the user' do
it 'shows confirmation and allows blocking', :js do
visit admin_user_path(user)
find('button', text: 'Block user').click
wait_for_requests
expect(page).to have_content('Block user')
expect(page).to have_content('You can always unblock their account, their data will remain intact.')
find('.modal-footer button', text: 'Block').click
wait_for_requests
expect(page).to have_content('Successfully blocked')
expect(page).to have_content('This user is blocked')
end
end
describe 'Impersonation' do
let(:another_user) { create(:user) }

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue';
@ -101,6 +101,6 @@ describe('IDE TerminalEmptyState', () => {
});
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
expect(wrapper.find('.bs-callout').element.innerHTML).toBe(TEST_HTML_MESSAGE);
expect(wrapper.find(GlAlert).html()).toContain(TEST_HTML_MESSAGE);
});
});

View File

@ -159,13 +159,13 @@ describe('graph component', () => {
describe('triggered by', () => {
describe('on click', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
it('should emit `onClickUpstreamPipeline` when triggered by linked pipeline is clicked', () => {
const btnWrapper = findExpandPipelineBtn();
btnWrapper.trigger('click');
btnWrapper.vm.$nextTick(() => {
expect(wrapper.emitted().onClickTriggeredBy).toEqual([
expect(wrapper.emitted().onClickUpstreamPipeline).toEqual([
store.state.pipeline.triggered_by,
]);
});

View File

@ -2,11 +2,10 @@ import { mount } from '@vue/test-utils';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockData from './linked_pipelines_mock_data';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
const mockPipeline = mockData.triggered[0];
const validTriggeredPipelineId = mockPipeline.project.id;
const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
@ -40,6 +39,7 @@ describe('Linked pipeline', () => {
pipeline: mockPipeline,
projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
};
beforeEach(() => {
@ -104,11 +104,13 @@ describe('Linked pipeline', () => {
pipeline: mockPipeline,
projectId: validTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
};
const upstreamProps = {
...downstreamProps,
columnTitle: 'Upstream',
type: UPSTREAM,
};
it('parent/child label container should exist', () => {
@ -182,6 +184,7 @@ describe('Linked pipeline', () => {
pipeline: { ...mockPipeline, isLoading: true },
projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
};
beforeEach(() => {
@ -198,6 +201,7 @@ describe('Linked pipeline', () => {
pipeline: mockPipeline,
projectId: validTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
};
beforeEach(() => {

View File

@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import { UPSTREAM } from '~/pipelines/components/graph/constants';
import mockData from './linked_pipelines_mock_data';
describe('Linked Pipelines Column', () => {
@ -9,6 +10,7 @@ describe('Linked Pipelines Column', () => {
linkedPipelines: mockData.triggered,
graphPosition: 'right',
projectId: 19,
type: UPSTREAM,
};
let wrapper;

View File

@ -27,7 +27,8 @@ describe('PrometheusMetrics', () => {
expect(prometheusMetrics.$monitoredMetricsEmpty).toBeDefined();
expect(prometheusMetrics.$monitoredMetricsList).toBeDefined();
expect(prometheusMetrics.$missingEnvVarPanel).toBeDefined();
expect(prometheusMetrics.$panelToggle).toBeDefined();
expect(prometheusMetrics.$panelToggleRight).toBeDefined();
expect(prometheusMetrics.$panelToggleDown).toBeDefined();
expect(prometheusMetrics.$missingEnvVarMetricCount).toBeDefined();
expect(prometheusMetrics.$missingEnvVarMetricsList).toBeDefined();
});

View File

@ -62,7 +62,7 @@ describe('vue_shared/components/confirm_modal', () => {
wrapper.vm.modalAttributes = MOCK_MODAL_DATA.modalAttributes;
});
it('renders GlModal wtih data', () => {
it('renders GlModal with data', () => {
expect(findModal().exists()).toBeTruthy();
expect(findModal().attributes()).toEqual(
expect.objectContaining({
@ -72,6 +72,24 @@ describe('vue_shared/components/confirm_modal', () => {
);
});
});
describe.each`
desc | attrs | expectation
${'when message is simple text'} | ${{}} | ${`<div>${MOCK_MODAL_DATA.modalAttributes.message}</div>`}
${'when message has html'} | ${{ messageHtml: '<p>Header</p><ul onhover="alert(1)"><li>First</li></ul>' }} | ${'<p>Header</p><ul><li>First</li></ul>'}
`('$desc', ({ attrs, expectation }) => {
beforeEach(() => {
createComponent();
wrapper.vm.modalAttributes = {
...MOCK_MODAL_DATA.modalAttributes,
...attrs,
};
});
it('renders message', () => {
expect(findForm().element.innerHTML).toContain(expectation);
});
});
});
describe('methods', () => {

View File

@ -400,13 +400,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics)
end
context 'with ALL operator' do
context 'with AND operator' do
let(:aggregated_metrics) do
[
{ name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "ALL" },
{ name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "ALL" },
{ name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "ALL" },
{ name: 'gmau_4', events: %w[event4], operator: "ALL" }
{ name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "AND" },
{ name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "AND" },
{ name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" },
{ name: 'gmau_4', events: %w[event4], operator: "AND" }
].map(&:with_indifferent_access)
end
@ -422,12 +422,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
context 'with ANY operator' do
context 'with OR operator' do
let(:aggregated_metrics) do
[
{ name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "ANY" },
{ name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "ANY" },
{ name: 'gmau_3', events: %w[event4], operator: "ANY" }
{ name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "OR" },
{ name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "OR" },
{ name: 'gmau_3', events: %w[event4], operator: "OR" }
].map(&:with_indifferent_access)
end
@ -448,11 +448,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:aggregated_metrics) do
[
# represents stable aggregated metrics that has been fully released
{ name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "ANY" },
{ name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "OR" },
# represents new aggregated metric that is under performance testing on gitlab.com
{ name: 'gmau_enabled', events: %w[event4], operator: "ALL", feature_flag: enabled_feature_flag },
{ name: 'gmau_enabled', events: %w[event4], operator: "AND", feature_flag: enabled_feature_flag },
# represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com
{ name: 'gmau_disabled', events: %w[event4], operator: "ALL", feature_flag: disabled_feature_flag }
{ name: 'gmau_disabled', events: %w[event4], operator: "AND", feature_flag: disabled_feature_flag }
].map(&:with_indifferent_access)
end
@ -520,7 +520,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
context 'Redis calls' do
let(:aggregated_metrics) do
[
{ name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "ALL" }
{ name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }
].map(&:with_indifferent_access)
end

View File

@ -26,18 +26,6 @@ RSpec.describe Ci::BuildReportResultService do
expect(unique_test_cases_parsed).to eq(4)
end
context 'when feature flag for tracking is disabled' do
before do
stub_feature_flags(track_unique_test_cases_parsed: false)
end
it 'creates the report but does not track the event' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
expect(build_report_result.tests_name).to eq("test")
expect(Ci::BuildReportResult.count).to eq(1)
end
end
context 'when data has already been persisted' do
it 'raises an error and do not persist the same data twice' do
expect { 2.times { described_class.new.execute(build) } }.to raise_error(ActiveRecord::RecordNotUnique)

View File

@ -147,9 +147,11 @@ RSpec.describe Snippets::CreateService do
end
context 'when the commit action fails' do
let(:error) { SnippetRepository::CommitError.new('foobar') }
before do
allow_next_instance_of(SnippetRepository) do |instance|
allow(instance).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError.new('foobar'))
allow(instance).to receive(:multi_files_action).and_raise(error)
end
end
@ -172,7 +174,7 @@ RSpec.describe Snippets::CreateService do
end
it 'logs the error' do
expect(Gitlab::AppLogger).to receive(:error).with('foobar')
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(error, service: 'Snippets::CreateService')
subject
end

View File

@ -277,14 +277,14 @@ RSpec.describe Snippets::UpdateService do
end
context 'when an error is raised' do
let(:error_message) { 'foobar' }
let(:error) { SnippetRepository::CommitError.new('foobar') }
before do
allow(snippet.snippet_repository).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError, error_message)
allow(snippet.snippet_repository).to receive(:multi_files_action).and_raise(error)
end
it 'logs the error' do
expect(Gitlab::AppLogger).to receive(:error).with(error_message)
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(error, service: 'Snippets::UpdateService')
subject
end

View File

@ -124,9 +124,6 @@ RSpec.shared_examples 'write access for a read-only GitLab instance' do
where(:description, :path) do
'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch'
'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify'
'LFS request to locks create' | '/root/rouge.git/info/lfs/locks'
'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock'
'request to git-upload-pack' | '/root/rouge.git/git-upload-pack'
end
@ -139,6 +136,21 @@ RSpec.shared_examples 'write access for a read-only GitLab instance' do
expect(subject).not_to disallow_request
end
end
where(:description, :path) do
'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify'
'LFS request to locks create' | '/root/rouge.git/info/lfs/locks'
'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock'
end
with_them do
it "expects a POST #{description} URL not to be allowed" do
response = request.post(path)
expect(response).to be_redirect
expect(subject).to disallow_request
end
end
end
end