Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-12 15:10:33 +00:00
parent ef7cfec30c
commit 8ff63012e9
79 changed files with 971 additions and 547 deletions

View File

@ -40,10 +40,7 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
};
};
if (gon.features?.diffCompareWithHead) {
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
}
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
};
export const diffCompareDropdownSourceVersions = (state, getters) => {

View File

@ -440,7 +440,6 @@ export default {
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
:selected-dashboard="selectedDashboard"
@selectDashboard="selectDashboard($event)"
/>
</div>

View File

@ -1,5 +1,5 @@
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapActions, mapGetters } from 'vuex';
import {
GlAlert,
GlIcon,
@ -36,11 +36,6 @@ export default {
GlModal: GlModalDirective,
},
props: {
selectedDashboard: {
type: Object,
required: false,
default: () => ({}),
},
defaultBranch: {
type: String,
required: true,
@ -56,11 +51,15 @@ export default {
},
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
...mapGetters('monitoringDashboard', ['selectedDashboard']),
isSystemDashboard() {
return this.selectedDashboard.system_dashboard;
return this.selectedDashboard?.system_dashboard;
},
selectedDashboardText() {
return this.selectedDashboard.display_name;
return this.selectedDashboard?.display_name;
},
selectedDashboardPath() {
return this.selectedDashboard?.path;
},
filteredDashboards() {
@ -145,7 +144,7 @@ export default {
<gl-dropdown-item
v-for="dashboard in starredDashboards"
:key="dashboard.path"
:active="dashboard.path === selectedDashboard.path"
:active="dashboard.path === selectedDashboardPath"
active-class="is-active"
@click="selectDashboard(dashboard)"
>
@ -163,7 +162,7 @@ export default {
<gl-dropdown-item
v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
:active="dashboard.path === selectedDashboard.path"
:active="dashboard.path === selectedDashboardPath"
active-class="is-active"
@click="selectDashboard(dashboard)"
>

View File

@ -345,6 +345,35 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
// Dashboard manipulation
export const toggleStarredValue = ({ commit, state, getters }) => {
const { selectedDashboard } = getters;
if (state.isUpdatingStarredValue) {
// Prevent repeating requests for the same change
return;
}
if (!selectedDashboard) {
return;
}
const method = selectedDashboard.starred ? 'DELETE' : 'POST';
const url = selectedDashboard.user_starred_path;
const newStarredValue = !selectedDashboard.starred;
commit(types.REQUEST_DASHBOARD_STARRING);
axios({
url,
method,
})
.then(() => {
commit(types.RECEIVE_DASHBOARD_STARRING_SUCCESS, newStarredValue);
})
.catch(() => {
commit(types.RECEIVE_DASHBOARD_STARRING_FAILURE);
});
};
/**
* Set a new array of metrics to a panel group
* @param {*} data An object containing

View File

@ -3,6 +3,19 @@ import { NOT_IN_DB_PREFIX } from '../constants';
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
* Returns a reference to the currently selected dashboard
* from the list of dashboards.
*
* @param {Object} state
*/
export const selectedDashboard = state => {
const { allDashboards } = state;
return (
allDashboards.find(({ path }) => path === state.currentDashboard) || allDashboards[0] || null
);
};
/**
* Get all state for metric in the dashboard or a group. The
* states are not repeated so the dashboard or group can show

View File

@ -5,6 +5,10 @@ export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAIL
export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES';
export const UPDATE_VARIABLE_DATA = 'UPDATE_VARIABLE_DATA';
export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
export const RECEIVE_DASHBOARD_STARRING_FAILURE = 'RECEIVE_DASHBOARD_STARRING_FAILURE';
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';

View File

@ -1,5 +1,7 @@
import Vue from 'vue';
import { pick } from 'lodash';
import * as types from './mutation_types';
import { selectedDashboard } from './getters';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { endpointKeys, initialStateKeys, metricStates } from '../constants';
@ -71,6 +73,23 @@ export default {
state.showEmptyState = true;
},
[types.REQUEST_DASHBOARD_STARRING](state) {
state.isUpdatingStarredValue = true;
},
[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, newStarredValue) {
const dashboard = selectedDashboard(state);
const index = state.allDashboards.findIndex(d => d === dashboard);
state.isUpdatingStarredValue = false;
// Trigger state updates in the reactivity system for this change
// https://vuejs.org/v2/guide/reactivity.html#For-Arrays
Vue.set(state.allDashboards, index, { ...dashboard, starred: newStarredValue });
},
[types.RECEIVE_DASHBOARD_STARRING_FAILURE](state) {
state.isUpdatingStarredValue = false;
},
/**
* Deployments and environments
*/

View File

@ -14,6 +14,7 @@ export default () => ({
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
isUpdatingStarredValue: false,
dashboard: {
panelGroups: [],
},

View File

@ -1,18 +1,68 @@
<script>
import { GlFormTextarea } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from '../components/publish_toolbar.vue';
import EditHeader from '../components/edit_header.vue';
export default {
components: {
GlFormTextarea,
RichContentEditor,
PublishToolbar,
EditHeader,
},
mixins: [glFeatureFlagsMixin()],
props: {
value: {
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
savingChanges: {
type: Boolean,
required: true,
},
returnUrl: {
type: String,
required: false,
default: '',
},
},
data() {
return {
editableContent: this.content,
saveable: false,
};
},
computed: {
modified() {
return this.content !== this.editableContent;
},
},
methods: {
onSubmit() {
this.$emit('submit', { content: this.editableContent });
},
},
};
</script>
<template>
<gl-form-textarea :value="value" v-on="$listeners" />
<div class="d-flex flex-grow-1 flex-column">
<edit-header class="py-2" :title="title" />
<rich-content-editor v-if="glFeatures.richContentEditor" v-model="editableContent" />
<gl-form-textarea v-else v-model="editableContent" class="h-100 shadow-none" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:return-url="returnUrl"
:saveable="modified"
:saving-changes="savingChanges"
@submit="onSubmit"
/>
</div>
</template>

View File

@ -0,0 +1,19 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
export default {
components: {
GlSkeletonLoader,
},
};
</script>
<template>
<gl-skeleton-loader :width="500" :height="102">
<rect width="500" height="16" rx="4" />
<rect y="20" width="375" height="16" rx="4" />
<rect x="380" y="20" width="120" height="16" rx="4" />
<rect y="40" width="250" height="16" rx="4" />
<rect x="255" y="40" width="150" height="16" rx="4" />
<rect x="410" y="40" width="90" height="16" rx="4" />
</gl-skeleton-loader>
</template>

View File

@ -1,13 +1,9 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoader } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import SkeletonLoader from '../components/skeleton_loader.vue';
import EditArea from '../components/edit_area.vue';
import EditHeader from '../components/edit_header.vue';
import SavedChangesMessage from '../components/saved_changes_message.vue';
import PublishToolbar from '../components/publish_toolbar.vue';
import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
@ -20,16 +16,12 @@ import { LOAD_CONTENT_ERROR } from '../constants';
export default {
components: {
RichContentEditor,
SkeletonLoader,
EditArea,
EditHeader,
InvalidContentMessage,
GlSkeletonLoader,
SavedChangesMessage,
PublishToolbar,
SubmitChangesError,
},
mixins: [glFeatureFlagsMixin()],
apollo: {
appData: {
query: appDataQuery,
@ -58,80 +50,51 @@ export default {
},
},
computed: {
...mapState([
'content',
'isLoadingContent',
'isSavingChanges',
'isContentLoaded',
'returnUrl',
'title',
'submitChangesError',
'savedContentMeta',
]),
...mapGetters(['contentChanged']),
},
mounted() {
if (this.appData.isSupportedContent) {
this.loadContent();
}
...mapState(['isSavingChanges', 'submitChangesError', 'savedContentMeta']),
isLoadingContent() {
return this.$apollo.queries.sourceContent.loading;
},
isContentLoaded() {
return Boolean(this.sourceContent);
},
},
methods: {
...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
...mapActions(['setContent', 'submitChanges', 'dismissSubmitChangesError']),
onSubmit({ content }) {
this.setContent(content);
this.submitChanges();
},
},
};
</script>
<template>
<div class="d-flex justify-content-center h-100 pt-2">
<div class="container d-flex gl-flex-direction-column pt-2 h-100">
<!-- Success view -->
<saved-changes-message
v-if="savedContentMeta"
class="w-75"
:branch="savedContentMeta.branch"
:commit="savedContentMeta.commit"
:merge-request="savedContentMeta.mergeRequest"
:return-url="returnUrl"
:return-url="appData.returnUrl"
/>
<!-- Main view -->
<template v-else-if="appData.isSupportedContent">
<div v-if="isLoadingContent" class="w-50 h-50">
<gl-skeleton-loader :width="500" :height="102">
<rect width="500" height="16" rx="4" />
<rect y="20" width="375" height="16" rx="4" />
<rect x="380" y="20" width="120" height="16" rx="4" />
<rect y="40" width="250" height="16" rx="4" />
<rect x="255" y="40" width="150" height="16" rx="4" />
<rect x="410" y="40" width="90" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
<submit-changes-error
v-if="submitChangesError"
class="w-75 align-self-center"
:error="submitChangesError"
@retry="submitChanges"
@dismiss="dismissSubmitChangesError"
/>
<edit-header class="w-75 align-self-center py-2" :title="title" />
<rich-content-editor
v-if="glFeatures.richContentEditor"
class="w-75 gl-align-self-center"
:value="content"
@input="setContent"
/>
<edit-area
v-else
class="w-75 h-100 shadow-none align-self-center"
:value="content"
@input="setContent"
/>
<publish-toolbar
:return-url="returnUrl"
:saveable="contentChanged"
:saving-changes="isSavingChanges"
@submit="submitChanges"
/>
</div>
<skeleton-loader v-if="isLoadingContent" class="w-75 gl-align-self-center gl-mt-5" />
<submit-changes-error
v-if="submitChangesError"
:error="submitChangesError"
@retry="submitChanges"
@dismiss="dismissSubmitChangesError"
/>
<edit-area
v-if="isContentLoaded"
:title="sourceContent.title"
:content="sourceContent.content"
:saving-changes="isSavingChanges"
:return-url="appData.returnUrl"
@submit="onSubmit"
/>
</template>
<!-- Error view -->

View File

@ -27,9 +27,9 @@ export default {
class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
@click="handleButtonClick"
>
<span class="dropdown-toggle-text" :class="{ 'flex-fill': isDropdownVariantStandalone }">{{
dropdownButtonText
}}</span>
<span class="dropdown-toggle-text flex-fill">
{{ dropdownButtonText }}
</span>
<gl-icon name="chevron-down" class="pull-right" />
</gl-button>
</template>

View File

@ -1,16 +1,18 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import LabelItem from './label_item.vue';
export default {
components: {
GlLoadingIcon,
GlButton,
GlIcon,
GlSearchBoxByType,
GlLink,
LabelItem,
},
data() {
return {
@ -60,11 +62,6 @@ export default {
'updateSelectedLabels',
'toggleDropdownContents',
]),
getDropdownLabelBoxStyle(label) {
return {
backgroundColor: label.color,
};
},
isLabelSelected(label) {
return this.selectedLabelsList.includes(label.id);
},
@ -138,24 +135,19 @@ export default {
@click="toggleDropdownContents"
/>
</div>
<div class="dropdown-input">
<div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
</div>
<div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
<div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
<ul class="list-unstyled mb-0">
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
<gl-link
class="d-flex align-items-baseline text-break-word label-item"
:class="{ 'is-focused': index === currentHighlightItem }"
@click="handleLabelClick(label)"
>
<gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" />
<span v-show="!label.set" class="mr-3 pr-2"></span>
<span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span>
<span>{{ label.title }}</span>
</gl-link>
<label-item
:label="label"
:highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)"
/>
</li>
<li v-if="!visibleLabels.length" class="p-2 text-center">
<li v-show="!visibleLabels.length" class="p-2 text-center">
{{ __('No matching results') }}
</li>
</ul>
@ -170,9 +162,9 @@ export default {
>
</li>
<li>
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
{{ footerManageLabelTitle }}
</gl-link>
<gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{
footerManageLabelTitle
}}</gl-link>
</li>
</ul>
</div>

View File

@ -0,0 +1,52 @@
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlLink,
},
props: {
label: {
type: Object,
required: true,
},
highlight: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isSet: this.label.set,
};
},
computed: {
labelBoxStyle() {
return {
backgroundColor: this.label.color,
};
},
},
methods: {
handleClick() {
this.isSet = !this.isSet;
this.$emit('clickLabel', this.label);
},
},
};
</script>
<template>
<gl-link
class="d-flex align-items-baseline text-break-word label-item"
:class="{ 'is-focused': highlight }"
@click="handleClick"
>
<gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" />
<span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span>
<span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span>
<span>{{ label.title }}</span>
</gl-link>
</template>

View File

@ -58,29 +58,13 @@ export default {
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Iterate over all the labels and update
// `set` prop value to represent their current state.
const labelIds = labels.map(label => label.id);
state.labels = state.labels.reduce((allLabels, label) => {
if (labelIds.includes(label.id)) {
allLabels.push({
...label,
touched: true,
set: !label.set,
});
} else {
// In case multiselect is not allowed
// we unselect any existing selected label
const unchangedLabel = state.allowMultiselect
? label
: {
...label,
touched: true,
set: false,
};
allLabels.push(unchangedLabel);
}
return allLabels;
}, []);
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
const labelId = labels.pop()?.id;
const candidateLabel = state.labels.find(label => labelId === label.id);
if (candidateLabel) {
candidateLabel.touched = true;
candidateLabel.set = !candidateLabel.set;
}
},
};

View File

@ -26,7 +26,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project)
push_frontend_feature_flag(:diff_compare_with_head, @project)
push_frontend_feature_flag(:accessibility_merge_request_widget, @project)
end

View File

@ -367,7 +367,7 @@ module ApplicationSettingsHelper
end
end
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')
# The methods in `EE::ApplicationSettingsHelper` should be available as both
# instance and class methods.

View File

@ -152,7 +152,7 @@ module AuthHelper
extend self
end
AuthHelper.prepend_if_ee('EE::AuthHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
AuthHelper.prepend_if_ee('EE::AuthHelper')
# The methods added in EE should be available as both class and instance
# methods, just like the methods provided by `AuthHelper` itself.

View File

@ -249,7 +249,7 @@ module MilestonesHelper
if milestone.legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
group_milestone_path(@group, milestone.iid, milestone: params)
group_milestone_path(milestone.group, milestone.iid, milestone: params)
end
end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
module ProjectsHelper
prepend_if_ee('::EE::ProjectsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
def project_incident_management_setting
@project_incident_management_setting ||= @project.incident_management_setting ||
@project.build_incident_management_setting

View File

@ -101,7 +101,7 @@ module ServicesHelper
extend self
end
ServicesHelper.prepend_if_ee('EE::ServicesHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
ServicesHelper.prepend_if_ee('EE::ServicesHelper')
# The methods in `EE::ServicesHelper` should be available as both instance and
# class methods.

View File

@ -42,7 +42,7 @@ module SystemNoteHelper
extend self
end
SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper')
# The methods in `EE::SystemNoteHelper` should be available as both instance and
# class methods.

View File

@ -150,5 +150,5 @@ end
Noteable.extend(Noteable::ClassMethods)
Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods')
Noteable.prepend_if_ee('EE::Noteable')

View File

@ -50,8 +50,8 @@ module ProtectedRefAccess
end
end
ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes') # rubocop: disable Cop/InjectEnterpriseEditionModule
ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess') # rubocop: disable Cop/InjectEnterpriseEditionModule
ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes')
ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess')
# When using `prepend` (or `include` for that matter), the `ClassMethods`
# constants are not merged. This means that `class_methods` in

View File

@ -875,7 +875,7 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def diffable_merge_ref?
Feature.enabled?(:diff_compare_with_head, target_project) && can_be_merged? && merge_ref_head.present?
can_be_merged? && merge_ref_head.present?
end
# Returns boolean indicating the merge_status should be rechecked in order to

View File

@ -82,6 +82,8 @@ class SentNotification < ApplicationRecord
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
else
new_position = nil
end
super(new_position)

View File

@ -12,6 +12,14 @@ module Ci
end
end
condition(:unprotected_ref) do
if @subject.tag?
!ProtectedTag.protected?(@subject.project, @subject.ref)
else
!ProtectedBranch.protected?(@subject.project, @subject.ref)
end
end
condition(:owner_of_job) do
@subject.triggered_by?(@user)
end
@ -34,7 +42,7 @@ module Ci
prevent :erase_build
end
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build
rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_build

View File

@ -8,8 +8,6 @@ class AnalyticsSummaryEntity < Grape::Entity
private
def value
return object.value if object.value.is_a? String
object.value&.nonzero? ? object.value.to_s : '-'
object.value.to_s
end
end

View File

@ -14,7 +14,7 @@ module Branches
if new_branch
success(new_branch)
else
error("Invalid reference name: #{branch_name}")
error("Invalid reference name: #{ref}")
end
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)

View File

@ -10,6 +10,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :name: auto_devops:auto_devops_disable
:feature_category: :auto_devops
:has_external_dependencies:
@ -934,13 +941,6 @@
:resource_boundary: :unknown
:weight: 2
:idempotent: true
- :name: authorized_project_update_user_refresh_with_low_urgency
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :name: authorized_projects
:feature_category: :authentication_and_authorization
:has_external_dependencies:

View File

@ -4,6 +4,7 @@ module AuthorizedProjectUpdate
class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker
feature_category :authentication_and_authorization
urgency :low
queue_namespace :authorized_project_update
idempotent!
end

View File

@ -0,0 +1,5 @@
---
title: Disallow developers to delete builds of protected branches
merge_request: 28881
author: Alexander Kutelev
type: changed

View File

@ -1,5 +0,0 @@
---
title: backfill environment_id on deployment_merge_requests
merge_request: 27219
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Allow multiple usage of EE extension/inclusion on last lines
merge_request: 31183
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Link to subgroup milestones correctly from group milestones page
merge_request: 31383
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Allow showing merge request diffs compared to current version of target branch
merge_request: 31325
author:
type: added

View File

@ -160,5 +160,5 @@ class Rack::Attack
end
end
::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack') # rubocop: disable Cop/InjectEnterpriseEditionModule
::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack')
::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request')

View File

@ -34,8 +34,6 @@
- 2
- - authorized_project_update
- 1
- - authorized_project_update_user_refresh_with_low_urgency
- 1
- - authorized_projects
- 2
- - auto_devops

View File

@ -4,46 +4,19 @@ class BackfillEnvironmentIdOnDeploymentMergeRequests < ActiveRecord::Migration[6
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 400
DELAY = 1.minute
disable_ddl_transaction!
def up
max_mr_id = DeploymentMergeRequest
.select(:merge_request_id)
.distinct
.order(merge_request_id: :desc)
.limit(1)
.pluck(:merge_request_id)
.first || 0
# no-op
last_mr_id = 0
step = 0
while last_mr_id < max_mr_id
stop =
DeploymentMergeRequest
.select(:merge_request_id)
.distinct
.where('merge_request_id > ?', last_mr_id)
.order(:merge_request_id)
.offset(BATCH_SIZE)
.limit(1)
.pluck(:merge_request_id)
.first
stop ||= max_mr_id
migrate_in(
step * DELAY,
'BackfillEnvironmentIdDeploymentMergeRequests',
[last_mr_id + 1, stop]
)
last_mr_id = stop
step += 1
end
# this migration is deleted because there is no foreign key for
# deployments.environment_id and this caused a failure upgrading
# deployments_merge_requests.environment_id
#
# Details on the following issues:
# * https://gitlab.com/gitlab-org/gitlab/-/issues/217191
# * https://gitlab.com/gitlab-org/gitlab/-/issues/26229
end
def down

View File

@ -7,6 +7,8 @@ The `description_html` - was added to response JSON in [GitLab 12.7](https://git
Get all labels for a given project.
By default, this request returns 20 results at a time because the API results [are paginated](README.md#pagination).
```plaintext
GET /projects/:id/labels
```

View File

@ -39,8 +39,9 @@ runtime dependencies needed to compile the project:
- `artifacts`: **Use for stage results that will be passed between stages.**
Artifacts are files generated by a job which are stored and uploaded, and can then
be fetched and used by jobs in later stages of the **same pipeline**. This data
will not be available in different pipelines, but is available to be downloaded
be fetched and used by jobs in later stages of the **same pipeline**. In other words,
[you can't create an artifact in job-A in stage-1, and then use this artifact in job-B in stage-1](https://gitlab.com/gitlab-org/gitlab/-/issues/25837).
This data will not be available in different pipelines, but is available to be downloaded
from the UI.
The name `artifacts` sounds like it's only useful outside of the job, like for downloading

View File

@ -403,7 +403,9 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
1. Only if the job was triggered by the user
1. Only if the job was:
- Triggered by the user
- [Since GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/35069), not run for a protected branch
### Job permissions

View File

@ -57,7 +57,7 @@ source and target branch can be shown mixed together making it hard to
understand which changes are being added and which already exist in the
target branch.
In GitLab 12.10, we added an **experimental** comparison mode, which
In GitLab 12.10, we added a comparison mode, which
shows a diff calculated by simulating how it would look like once merged - a more accurate
representation of the changes rather than using the base of the two
branches. The new mode is available from the comparison target drop down
@ -67,26 +67,6 @@ current default comparison.
![Merge request versions compare HEAD](img/versions_compare_head_v12_10.png)
### Enable or disable `HEAD` comparison mode **(CORE ONLY)**
`HEAD` comparison mode is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
can enable it for your instance. You're welcome to test it, but use it at your
own risk.
To enable it:
```ruby
Feature.enable(:diff_compare_with_head)
```
To disable it:
```ruby
Feature.disable(:diff_compare_with_head)
```
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -5,7 +5,14 @@ module Gitlab
# BackfillEnvironmentIdDeploymentMergeRequests deletes duplicates
# from deployment_merge_requests table and backfills environment_id
class BackfillEnvironmentIdDeploymentMergeRequests
def perform(start_mr_id, stop_mr_id)
def perform(_start_mr_id, _stop_mr_id)
# no-op
# Background migration removed due to
# https://gitlab.com/gitlab-org/gitlab/-/issues/217191
end
def backfill_range(start_mr_id, stop_mr_id)
start_mr_id = Integer(start_mr_id)
stop_mr_id = Integer(stop_mr_id)

View File

@ -28,8 +28,7 @@ module Gitlab
end
def deployments_summary
@deployments_summary ||=
Summary::Deploy.new(project: @project, from: @from, to: @to)
@deployments_summary ||= Summary::Deploy.new(project: @project, from: @from, to: @to)
end
def deploy_stats
@ -39,7 +38,7 @@ module Gitlab
def deployment_frequency_stats
serialize(
Summary::DeploymentFrequency.new(
deployments: deployments_summary.value,
deployments: deployments_summary.value.raw_value,
from: @from,
to: @to),
with_unit: true

View File

@ -9,7 +9,7 @@ module Gitlab
end
def value
@value ||= count_commits
@value ||= commits_count ? Value::PrettyNumeric.new(commits_count) : Value::None.new
end
private
@ -18,10 +18,10 @@ module Gitlab
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
# the easiest way forward is to replicate the relevant portions of the
# `log` function here.
def count_commits
def commits_count
return unless ref
gitaly_commit_client.commit_count(ref, after: @from, before: @to)
@commits_count ||= gitaly_commit_client.commit_count(ref, after: @from, before: @to)
end
def gitaly_commit_client

View File

@ -4,18 +4,20 @@ module Gitlab
module CycleAnalytics
module Summary
class Deploy < Base
include Gitlab::Utils::StrongMemoize
def title
n_('Deploy', 'Deploys', value)
end
def value
strong_memoize(:value) do
query = @project.deployments.success.where("created_at >= ?", @from)
query = query.where("created_at <= ?", @to) if @to
query.count
end
@value ||= Value::PrettyNumeric.new(deployments_count)
end
private
def deployments_count
query = @project.deployments.success.where("created_at >= ?", @from)
query = query.where("created_at <= ?", @to) if @to
query.count
end
end
end

View File

@ -17,8 +17,7 @@ module Gitlab
end
def value
@value ||=
frequency(@deployments, @from, @to || Time.now)
@value ||= frequency(@deployments, @from, @to || Time.now)
end
def unit

View File

@ -16,7 +16,16 @@ module Gitlab
end
def value
@value ||= IssuesFinder.new(@current_user, project_id: @project.id, created_after: @from, created_before: @to).execute.count
@value ||= Value::PrettyNumeric.new(issues_count)
end
private
def issues_count
IssuesFinder
.new(@current_user, project_id: @project.id, created_after: @from, created_before: @to)
.execute
.count
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module Summary
class Value
attr_reader :value
def raw_value
value
end
def to_s
raise NotImplementedError
end
class None < self
def to_s
'-'
end
end
class Numeric < self
def initialize(value)
@value = value
end
def to_s
value.zero? ? '0' : value.to_s
end
end
class PrettyNumeric < Numeric
def to_s
# 0 is shown as -
value.nonzero? ? super : None.new.to_s
end
end
end
end
end
end

View File

@ -4,10 +4,11 @@ module Gitlab
module CycleAnalytics
module SummaryHelper
def frequency(count, from, to)
return count if count.zero?
return Summary::Value::None.new if count.zero?
freq = (count / days(from, to)).round(1)
freq.zero? ? '0' : freq
Summary::Value::Numeric.new(freq)
end
def days(from, to)

View File

@ -11,6 +11,9 @@ module Gitlab
class << self
include ActionView::RecordIdentifier
# Using a case statement here is preferable for readability and maintainability.
# See discussion in https://gitlab.com/gitlab-org/gitlab/-/issues/217397
#
# rubocop:disable Metrics/CyclomaticComplexity
def build(object, **options)
# Objects are sometimes wrapped in a BatchLoader instance

View File

@ -3857,15 +3857,6 @@ msgstr ""
msgid "Checkout|(x%{numberOfUsers})"
msgstr ""
msgid "Checkout|1. Your profile"
msgstr ""
msgid "Checkout|2. Checkout"
msgstr ""
msgid "Checkout|3. Your GitLab group"
msgstr ""
msgid "Checkout|Billing address"
msgstr ""
@ -6513,6 +6504,9 @@ msgstr ""
msgid "Customize your pipeline configuration."
msgstr ""
msgid "Cycle Time"
msgstr ""
msgid "CycleAnalyticsEvent|Issue closed"
msgstr ""
@ -17256,6 +17250,18 @@ msgstr ""
msgid "Registration"
msgstr ""
msgid "Registration|Checkout"
msgstr ""
msgid "Registration|Your GitLab group"
msgstr ""
msgid "Registration|Your first project"
msgstr ""
msgid "Registration|Your profile"
msgstr ""
msgid "Related Deployed Jobs"
msgstr ""

View File

@ -17,6 +17,8 @@ module RuboCop
CHECK_LINE_METHODS =
Set.new(%i[include_if_ee extend_if_ee prepend_if_ee]).freeze
CHECK_LINE_METHODS_REGEXP = Regexp.union(CHECK_LINE_METHODS.map(&:to_s)).freeze
DISALLOW_METHODS = Set.new(%i[include extend prepend]).freeze
def ee_const?(node)
@ -48,7 +50,13 @@ module RuboCop
# the expression is the last line _of code_.
last_line -= 1 if buffer.source.end_with?("\n")
add_offense(node, message: INVALID_LINE) if line < last_line
last_line_content = buffer.source.split("\n")[-1]
if CHECK_LINE_METHODS_REGEXP.match?(last_line_content)
ignore_node(node)
elsif line < last_line
add_offense(node, message: INVALID_LINE)
end
end
def verify_argument_type(node)

View File

@ -18,7 +18,6 @@ describe('Compare diff version dropdowns', () => {
};
localState.targetBranchName = 'baseVersion';
localState.mergeRequestDiffs = diffsMockData;
gon.features = { diffCompareWithHead: true };
});
describe('selectedTargetIndex', () => {
@ -129,14 +128,6 @@ describe('Compare diff version dropdowns', () => {
});
assertVersions(targetVersions);
});
it('does not list head version if feature flag is not enabled', () => {
gon.features = { diffCompareWithHead: false };
setupTest();
const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters);
expect(targetVersions.find(version => version.isHead)).toBeUndefined();
});
});
it('diffCompareDropdownSourceVersions', () => {

View File

@ -16,7 +16,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
selecteddashboard="[object Object]"
toggle-class="dropdown-menu-toggle"
/>
</div>

View File

@ -15,6 +15,7 @@ const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starr
describe('DashboardsDropdown', () => {
let wrapper;
let mockDashboards;
let mockSelectedDashboard;
function createComponent(props, opts = {}) {
const storeOpts = {
@ -23,6 +24,7 @@ describe('DashboardsDropdown', () => {
},
computed: {
allDashboards: () => mockDashboards,
selectedDashboard: () => mockSelectedDashboard,
},
};
@ -46,6 +48,7 @@ describe('DashboardsDropdown', () => {
beforeEach(() => {
mockDashboards = dashboardGitResponse;
mockSelectedDashboard = null;
});
describe('when it receives dashboards data', () => {
@ -153,13 +156,12 @@ describe('DashboardsDropdown', () => {
let modalDirective;
beforeEach(() => {
[mockSelectedDashboard] = dashboardGitResponse;
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
wrapper = createComponent(
{
selectedDashboard: dashboardGitResponse[0],
},
{},
{
directives: {
GlModal: modalDirective,

View File

@ -325,6 +325,7 @@ export const dashboardGitResponse = [
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
starred: false,
user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
},
{
default: false,
@ -334,6 +335,7 @@ export const dashboardGitResponse = [
project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
path: '.gitlab/dashboards/dashboard.yml',
starred: true,
user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
},
...customDashboardsData,
];

View File

@ -18,6 +18,7 @@ import {
fetchEnvironmentsData,
fetchDashboardData,
fetchAnnotations,
toggleStarredValue,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
@ -350,6 +351,49 @@ describe('Monitoring store actions', () => {
});
});
describe('Toggles starred value of current dashboard', () => {
const { state } = store;
let unstarredDashboard;
let starredDashboard;
beforeEach(() => {
state.isUpdatingStarredValue = false;
[unstarredDashboard, starredDashboard] = dashboardGitResponse;
});
describe('toggleStarredValue', () => {
it('performs no changes if no dashboard is selected', () => {
return testAction(toggleStarredValue, null, state, [], []);
});
it('performs no changes if already changing starred value', () => {
state.selectedDashboard = unstarredDashboard;
state.isUpdatingStarredValue = true;
return testAction(toggleStarredValue, null, state, [], []);
});
it('stars dashboard if it is not starred', () => {
state.selectedDashboard = unstarredDashboard;
mock.onPost(unstarredDashboard.user_starred_path).reply(200);
return testAction(toggleStarredValue, null, state, [
{ type: types.REQUEST_DASHBOARD_STARRING },
{ type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS, payload: true },
]);
});
it('unstars dashboard if it is starred', () => {
state.selectedDashboard = starredDashboard;
mock.onPost(starredDashboard.user_starred_path).reply(200);
return testAction(toggleStarredValue, null, state, [
{ type: types.REQUEST_DASHBOARD_STARRING },
{ type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
]);
});
});
});
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {

View File

@ -3,7 +3,7 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import { environmentData, metricsResult } from '../mock_data';
import { environmentData, metricsResult, dashboardGitResponse } from '../mock_data';
import {
metricsDashboardPayload,
metricResultStatus,
@ -350,4 +350,48 @@ describe('Monitoring store Getters', () => {
expect(variablesArray).toEqual([]);
});
});
describe('selectedDashboard', () => {
const { selectedDashboard } = getters;
it('returns a dashboard', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[0].path,
};
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
});
it('returns a non-default dashboard', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path,
};
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]);
});
it('returns a default dashboard when no dashboard is selected', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: null,
};
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
});
it('returns a default dashboard when dashboard cannot be found', () => {
const state = {
allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path',
};
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
});
it('returns null when no dashboards are present', () => {
const state = {
allDashboards: [],
currentDashboard: dashboardGitResponse[0].path,
};
expect(selectedDashboard(state)).toEqual(null);
});
});
});

View File

@ -72,6 +72,49 @@ describe('Monitoring mutations', () => {
});
});
describe('Dashboard starring mutations', () => {
it('REQUEST_DASHBOARD_STARRING', () => {
stateCopy = { isUpdatingStarredValue: false };
mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
expect(stateCopy.isUpdatingStarredValue).toBe(true);
});
describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
let allDashboards;
beforeEach(() => {
allDashboards = [...dashboardGitResponse];
stateCopy = {
allDashboards,
currentDashboard: allDashboards[1].path,
isUpdatingStarredValue: true,
};
});
it('sets a dashboard as starred', () => {
mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, true);
expect(stateCopy.isUpdatingStarredValue).toBe(false);
expect(stateCopy.allDashboards[1].starred).toBe(true);
});
it('sets a dashboard as unstarred', () => {
mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, false);
expect(stateCopy.isUpdatingStarredValue).toBe(false);
expect(stateCopy.allDashboards[1].starred).toBe(false);
});
});
it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
stateCopy = { isUpdatingStarredValue: true };
mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
expect(stateCopy.isUpdatingStarredValue).toBe(false);
});
});
describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
it('stores the deployment data', () => {
stateCopy.deploymentData = [];

View File

@ -0,0 +1,79 @@
import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import EditArea from '~/static_site_editor/components/edit_area.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import { sourceContentTitle as title, sourceContent as content, returnUrl } from '../mock_data';
describe('~/static_site_editor/components/edit_area.vue', () => {
let wrapper;
const savingChanges = true;
const newContent = `new ${content}`;
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(EditArea, {
provide: {
glFeatures: { richContentEditor: true },
},
propsData: {
title,
content,
returnUrl,
savingChanges,
...propsData,
},
});
};
const findEditHeader = () => wrapper.find(EditHeader);
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
beforeEach(() => {
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders edit header', () => {
expect(findEditHeader().exists()).toBe(true);
expect(findEditHeader().props('title')).toBe(title);
});
it('renders rich content editor', () => {
expect(findRichContentEditor().exists()).toBe(true);
expect(findRichContentEditor().props('value')).toBe(content);
});
it('renders publish toolbar', () => {
expect(findPublishToolbar().exists()).toBe(true);
expect(findPublishToolbar().props('returnUrl')).toBe(returnUrl);
expect(findPublishToolbar().props('savingChanges')).toBe(savingChanges);
expect(findPublishToolbar().props('saveable')).toBe(false);
});
describe('when content changes', () => {
beforeEach(() => {
findRichContentEditor().vm.$emit('input', newContent);
return wrapper.vm.$nextTick();
});
it('sets publish toolbar as saveable when content changes', () => {
expect(findPublishToolbar().props('saveable')).toBe(true);
});
it('sets publish toolbar as not saveable when content changes are rollback', () => {
findRichContentEditor().vm.$emit('input', content);
return wrapper.vm.$nextTick().then(() => {
expect(findPublishToolbar().props('saveable')).toBe(false);
});
});
});
});

View File

@ -1,21 +1,20 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSkeletonLoader } from '@gitlab/ui';
import createState from '~/static_site_editor/store/state';
import Home from '~/static_site_editor/pages/home.vue';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
import EditArea from '~/static_site_editor/components/edit_area.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
import {
returnUrl,
sourceContent,
sourceContentTitle,
sourceContent as content,
sourceContentTitle as title,
savedContentMeta,
submitChangesError,
} from '../mock_data';
@ -27,13 +26,12 @@ localVue.use(Vuex);
describe('static_site_editor/pages/home', () => {
let wrapper;
let store;
let loadContentActionMock;
let $apollo;
let setContentActionMock;
let submitChangesActionMock;
let dismissSubmitChangesErrorActionMock;
const buildStore = ({ initialState, getters } = {}) => {
loadContentActionMock = jest.fn();
setContentActionMock = jest.fn();
submitChangesActionMock = jest.fn();
dismissSubmitChangesErrorActionMock = jest.fn();
@ -47,53 +45,55 @@ describe('static_site_editor/pages/home', () => {
...getters,
},
actions: {
loadContent: loadContentActionMock,
setContent: setContentActionMock,
submitChanges: submitChangesActionMock,
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
},
});
};
const buildContentLoadedStore = ({ initialState, getters } = {}) => {
buildStore({
initialState: {
isContentLoaded: true,
...initialState,
const buildApollo = (queries = {}) => {
$apollo = {
queries: {
sourceContent: {
loading: false,
},
...queries,
},
getters: {
...getters,
},
});
};
};
const buildWrapper = (data = { appData: { isSupportedContent: true } }) => {
const buildWrapper = (data = {}) => {
wrapper = shallowMount(Home, {
localVue,
store,
provide: {
glFeatures: { richContentEditor: true },
mocks: {
$apollo,
},
data() {
return data;
return {
appData: { isSupportedContent: true, returnUrl },
...data,
};
},
});
};
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findEditHeader = () => wrapper.find(EditHeader);
const findEditArea = () => wrapper.find(EditArea);
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findSkeletonLoader = () => wrapper.find(SkeletonLoader);
const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
beforeEach(() => {
buildApollo();
buildStore();
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
$apollo = null;
});
it('renders the saved changes message when changes are submitted successfully', () => {
@ -107,103 +107,69 @@ describe('static_site_editor/pages/home', () => {
});
});
describe('when content is not loaded', () => {
it('does not render rich content editor', () => {
expect(findRichContentEditor().exists()).toBe(false);
});
it('does not render the saved changes message when changes are not submitted', () => {
buildWrapper();
it('does not render edit header', () => {
expect(findEditHeader().exists()).toBe(false);
});
it('does not render toolbar', () => {
expect(findPublishToolbar().exists()).toBe(false);
});
it('does not render saved changes message', () => {
expect(findSavedChangesMessage().exists()).toBe(false);
});
expect(findSavedChangesMessage().exists()).toBe(false);
});
describe('when content is loaded', () => {
const content = sourceContent;
const title = sourceContentTitle;
beforeEach(() => {
buildContentLoadedStore({ initialState: { content, title } });
buildWrapper();
buildStore({ initialState: { isSavingChanges: true } });
buildWrapper({ sourceContent: { title, content } });
});
it('renders the rich content editor', () => {
expect(findRichContentEditor().exists()).toBe(true);
it('renders edit area', () => {
expect(findEditArea().exists()).toBe(true);
});
it('renders the edit header', () => {
expect(findEditHeader().exists()).toBe(true);
it('provides source content to the edit area', () => {
expect(findEditArea().props()).toMatchObject({
title,
content,
});
});
it('does not render skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(false);
it('provides returnUrl to the edit area', () => {
expect(findEditArea().props('returnUrl')).toBe(returnUrl);
});
it('passes page content to the rich content editor', () => {
expect(findRichContentEditor().props('value')).toBe(content);
});
it('passes page title to edit header', () => {
expect(findEditHeader().props('title')).toBe(title);
});
it('renders toolbar', () => {
expect(findPublishToolbar().exists()).toBe(true);
it('provides isSavingChanges to the edit area', () => {
expect(findEditArea().props('savingChanges')).toBe(true);
});
});
it('sets toolbar as saveable when content changes', () => {
buildContentLoadedStore({
getters: {
contentChanged: () => true,
it('does not render edit area when content is not loaded', () => {
buildWrapper({ sourceContent: null });
expect(findEditArea().exists()).toBe(false);
});
it('renders skeleton loader when content is not loading', () => {
buildApollo({
sourceContent: {
loading: true,
},
});
buildWrapper();
expect(findPublishToolbar().props('saveable')).toBe(true);
});
it('displays skeleton loader when loading content', () => {
buildStore({ initialState: { isLoadingContent: true } });
buildWrapper();
expect(findSkeletonLoader().exists()).toBe(true);
});
it('does not display submit changes error when an error does not exist', () => {
buildContentLoadedStore();
buildWrapper();
expect(findSubmitChangesError().exists()).toBe(false);
});
it('sets toolbar as saving when saving changes', () => {
buildContentLoadedStore({
initialState: {
isSavingChanges: true,
it('does not render skeleton loader when content is not loading', () => {
buildApollo({
sourceContent: {
loading: false,
},
});
buildWrapper();
expect(findPublishToolbar().props('savingChanges')).toBe(true);
});
it('displays invalid content message when content is not supported', () => {
buildWrapper({ appData: { isSupportedContent: false } });
expect(findInvalidContentMessage().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
});
describe('when submitting changes fail', () => {
beforeEach(() => {
buildContentLoadedStore({
buildStore({
initialState: {
submitChangesError,
},
@ -228,24 +194,32 @@ describe('static_site_editor/pages/home', () => {
});
});
it('dispatches load content action', () => {
expect(loadContentActionMock).toHaveBeenCalled();
});
it('dispatches setContent action when rich content editor emits input event', () => {
buildContentLoadedStore();
it('does not display submit changes error when an error does not exist', () => {
buildWrapper();
findRichContentEditor().vm.$emit('input', sourceContent);
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined);
expect(findSubmitChangesError().exists()).toBe(false);
});
it('dispatches submitChanges action when toolbar emits submit event', () => {
buildContentLoadedStore();
buildWrapper();
findPublishToolbar().vm.$emit('submit');
it('displays invalid content message when content is not supported', () => {
buildWrapper({ appData: { isSupportedContent: false } });
expect(submitChangesActionMock).toHaveBeenCalled();
expect(findInvalidContentMessage().exists()).toBe(true);
});
describe('when edit area emits submit event', () => {
const newContent = `new ${content}`;
beforeEach(() => {
buildWrapper({ sourceContent: { title, content } });
findEditArea().vm.$emit('submit', { content: newContent });
});
it('dispatches setContent property', () => {
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), newContent, undefined);
});
it('dispatches submitChanges action', () => {
expect(submitChangesActionMock).toHaveBeenCalled();
});
});
});

View File

@ -1,9 +1,10 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
@ -78,16 +79,6 @@ describe('DropdownContentsLabelsView', () => {
});
describe('methods', () => {
describe('getDropdownLabelBoxStyle', () => {
it('returns an object containing `backgroundColor` based on provided `label` param', () => {
expect(wrapper.vm.getDropdownLabelBoxStyle(mockRegularLabel)).toEqual(
expect.objectContaining({
backgroundColor: mockRegularLabel.color,
}),
);
});
});
describe('isLabelSelected', () => {
it('returns true when provided `label` param is one of the selected labels', () => {
expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
@ -234,16 +225,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders label elements for all labels', () => {
const labelsEl = wrapper.findAll('.dropdown-content li');
const labelItemEl = labelsEl.at(0).find(GlLink);
expect(labelsEl.length).toBe(mockLabels.length);
expect(labelItemEl.exists()).toBe(true);
expect(labelItemEl.find(GlIcon).props('name')).toBe('mobile-issue-close');
expect(labelItemEl.find('.dropdown-label-box').attributes('style')).toBe(
'background-color: rgb(186, 218, 85);',
);
expect(labelItemEl.find(GlLink).text()).toContain(mockLabels[0].title);
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
@ -253,9 +235,9 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const labelsEl = wrapper.findAll('.dropdown-content li');
const labelItemEl = labelsEl.at(0).find(GlLink);
const labelItemEl = labelsEl.at(0).find(LabelItem);
expect(labelItemEl.attributes('class')).toContain('is-focused');
expect(labelItemEl.props('highlight')).toBe(true);
});
});
@ -267,7 +249,7 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const noMatchEl = wrapper.find('.dropdown-content li');
expect(noMatchEl.exists()).toBe(true);
expect(noMatchEl.isVisible()).toBe(true);
expect(noMatchEl.text()).toContain('No matching results');
});
});

View File

@ -0,0 +1,111 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
import { mockRegularLabel } from './mock_data';
const createComponent = ({ label = mockRegularLabel, highlight = true } = {}) =>
shallowMount(LabelItem, {
propsData: {
label,
highlight,
},
});
describe('LabelItem', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('labelBoxStyle', () => {
it('returns an object containing `backgroundColor` based on `label` prop', () => {
expect(wrapper.vm.labelBoxStyle).toEqual(
expect.objectContaining({
backgroundColor: mockRegularLabel.color,
}),
);
});
});
});
describe('methods', () => {
describe('handleClick', () => {
it('sets value of `isSet` data prop to opposite of its current value', () => {
wrapper.setData({
isSet: true,
});
wrapper.vm.handleClick();
expect(wrapper.vm.isSet).toBe(false);
wrapper.vm.handleClick();
expect(wrapper.vm.isSet).toBe(true);
});
it('emits event `clickLabel` on component with `label` prop as param', () => {
wrapper.vm.handleClick();
expect(wrapper.emitted('clickLabel')).toBeTruthy();
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockRegularLabel]);
});
});
});
describe('template', () => {
it('renders gl-link component', () => {
expect(wrapper.find(GlLink).exists()).toBe(true);
});
it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => {
wrapper.setProps({
highlight: true,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(GlLink).classes()).toContain('is-focused');
});
});
it('renders visible gl-icon component when `isSet` prop is true', () => {
wrapper.setData({
isSet: true,
});
return wrapper.vm.$nextTick(() => {
const iconEl = wrapper.find(GlIcon);
expect(iconEl.isVisible()).toBe(true);
expect(iconEl.props('name')).toBe('mobile-issue-close');
});
});
it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
wrapper.setData({
isSet: false,
});
return wrapper.vm.$nextTick(() => {
const placeholderEl = wrapper.find('[data-testid="no-icon"]');
expect(placeholderEl.isVisible()).toBe(true);
});
});
it('renders label color element', () => {
const colorEl = wrapper.find('[data-testid="label-color-box"]');
expect(colorEl.exists()).toBe(true);
expect(colorEl.attributes('style')).toBe('background-color: rgb(186, 218, 85);');
});
it('renders label title', () => {
expect(wrapper.text()).toContain(mockRegularLabel.title);
});
});
});

View File

@ -155,29 +155,12 @@ describe('LabelsSelect Mutations', () => {
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `true`', () => {
const updatedLabelIds = [2, 4];
const state = {
labels,
allowMultiselect: true,
};
mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
state.labels.forEach(label => {
if (updatedLabelIds.includes(label.id)) {
expect(label.touched).toBe(true);
expect(label.set).toBe(true);
}
});
});
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param when `state.allowMultiselect` is `false`', () => {
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
const updatedLabelIds = [2];
const state = {
labels,
allowMultiselect: false,
};
mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
state.labels.forEach(label => {
if (updatedLabelIds.includes(label.id)) {

View File

@ -85,4 +85,19 @@ describe MilestonesHelper do
end
end
end
describe "#group_milestone_route" do
let(:group) { build_stubbed(:group) }
let(:subgroup) { build_stubbed(:group, parent: group, name: "Test Subgrp") }
context "when in subgroup" do
let(:milestone) { build_stubbed(:group_milestone, group: subgroup) }
it 'generates correct url despite assigned @group' do
assign(:group, group)
milestone_path = "/groups/#{subgroup.full_path}/-/milestones/#{milestone.iid}"
expect(helper.group_milestone_route(milestone)).to eq(milestone_path)
end
end
end
end

View File

@ -32,7 +32,7 @@ describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeReques
expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
migration.perform(1, mr.id)
migration.backfill_range(1, mr.id)
expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
expect(deployment_merge_requests.count).to eq(2)

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::CycleAnalytics::Summary::Value do
describe Gitlab::CycleAnalytics::Summary::Value::None do
it 'returns `-`' do
expect(described_class.new.to_s).to eq('-')
end
end
describe Gitlab::CycleAnalytics::Summary::Value::Numeric do
it 'returns the string representation of the number' do
expect(described_class.new(3.2).to_s).to eq('3.2')
end
end
describe Gitlab::CycleAnalytics::Summary::Value::PrettyNumeric do
describe '#to_s' do
it 'returns `-` when the number is 0' do
expect(described_class.new(0).to_s).to eq('-')
end
it 'returns the string representation of the number' do
expect(described_class.new(100).to_s).to eq('100')
end
end
end
end

View File

@ -1,81 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb')
describe BackfillEnvironmentIdOnDeploymentMergeRequests do
let(:environments) { table(:environments) }
let(:merge_requests) { table(:merge_requests) }
let(:deployments) { table(:deployments) }
let(:deployment_merge_requests) { table(:deployment_merge_requests) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:migration_worker) { double('BackgroundMigrationWorker') }
before do
stub_const('BackgroundMigrationWorker', migration_worker)
end
it 'schedules nothing when there are no entries' do
expect(migration_worker).not_to receive(:perform_in)
migrate!
end
it 'batches the workload' do
stub_const("#{described_class.name}::BATCH_SIZE", 10)
namespace = namespaces.create!(name: 'foo', path: 'foo')
project = projects.create!(namespace_id: namespace.id)
environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
# Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it
# we must generate more than described_class::BATCH_SIZE merge requests, deployments,
# and deployment_merge_requests entries
entries = 13
expect(entries).to be > described_class::BATCH_SIZE
# merge requests and deployments bulk generation
mrs_params = []
deployments_params = []
entries.times do |i|
mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id }
deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 }
end
all_mrs = merge_requests.insert_all(mrs_params)
all_deployments = deployments.insert_all(deployments_params)
# deployment_merge_requests bulk generation
dmr_params = []
entries.times do |index|
mr_id = all_mrs.rows[index].first
deployment_id = all_deployments.rows[index].first
dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id }
end
deployment_merge_requests.insert_all(dmr_params)
first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id]
second_batch_limit = dmr_params.last[:merge_request_id]
expect(migration_worker).to receive(:perform_in)
.with(
0,
'BackfillEnvironmentIdDeploymentMergeRequests',
[1, first_batch_limit]
)
expect(migration_worker).to receive(:perform_in)
.with(
described_class::DELAY,
'BackfillEnvironmentIdDeploymentMergeRequests',
[first_batch_limit + 1, second_batch_limit]
)
migrate!
end
end

View File

@ -3836,40 +3836,28 @@ describe MergeRequest do
end
describe '#diffable_merge_ref?' do
context 'diff_compare_with_head enabled' do
context 'merge request can be merged' do
context 'merge_to_ref is not calculated' do
it 'returns true' do
expect(subject.diffable_merge_ref?).to eq(false)
end
end
context 'merge_to_ref is calculated' do
before do
MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
end
it 'returns true' do
expect(subject.diffable_merge_ref?).to eq(true)
end
context 'merge request can be merged' do
context 'merge_to_ref is not calculated' do
it 'returns true' do
expect(subject.diffable_merge_ref?).to eq(false)
end
end
context 'merge request cannot be merged' do
it 'returns false' do
subject.mark_as_unchecked!
context 'merge_to_ref is calculated' do
before do
MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
end
expect(subject.diffable_merge_ref?).to eq(false)
it 'returns true' do
expect(subject.diffable_merge_ref?).to eq(true)
end
end
end
context 'diff_compare_with_head disabled' do
before do
stub_feature_flags(diff_compare_with_head: { enabled: false, thing: subject.target_project })
end
context 'merge request cannot be merged' do
it 'returns false' do
subject.mark_as_unchecked!
expect(subject.diffable_merge_ref?).to eq(false)
end
end

View File

@ -326,4 +326,26 @@ describe SentNotification do
end
end
end
describe "#position=" do
subject { build(:sent_notification, noteable: create(:issue)) }
it "doesn't accept non-hash JSON passed as a string" do
subject.position = "true"
expect(subject.attributes_before_type_cast["position"]).to be(nil)
end
it "does accept a position hash as a string" do
subject.position = '{ "base_sha": "test" }'
expect(subject.position.base_sha).to eq("test")
end
it "does accept a hash" do
subject.position = { "base_sha" => "test" }
expect(subject.position.base_sha).to eq("test")
end
end
end

View File

@ -176,15 +176,21 @@ describe Ci::BuildPolicy do
end
context 'when developers can push to the branch' do
before do
create(:protected_branch, :developers_can_push,
name: build.ref, project: project)
end
context 'when the build was created by the developer' do
let(:owner) { user }
it { expect(policy).to be_allowed :erase_build }
context 'when the build was created for a protected ref' do
before do
create(:protected_branch, :developers_can_push,
name: build.ref, project: project)
end
it { expect(policy).to be_disallowed :erase_build }
end
context 'when the build was created for an unprotected ref' do
it { expect(policy).to be_allowed :erase_build }
end
end
context 'when the build was created by the other' do

View File

@ -630,7 +630,7 @@ describe API::Branches do
post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Invalid reference name: new_design3')
expect(json_response['message']).to eq('Invalid reference name: foo')
end
end

View File

@ -38,7 +38,7 @@ describe 'Creation of a new branch' do
let(:ref) { 'unknown' }
it_behaves_like 'a mutation that returns errors in the response',
errors: ['Invalid reference name: new_branch']
errors: ['Invalid reference name: unknown']
end
end
end

View File

@ -159,6 +159,17 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do
SOURCE
end
it 'does not flag the double use of `X_if_ee` on the last line' do
expect_no_offenses(<<~SOURCE)
class Foo
end
Foo.extend_if_ee('EE::Foo')
Foo.include_if_ee('EE::Foo')
Foo.prepend_if_ee('EE::Foo')
SOURCE
end
it 'autocorrects offenses by just disabling the Cop' do
source = <<~SOURCE
class Foo

View File

@ -3,39 +3,45 @@
require 'spec_helper'
describe Branches::CreateService do
let(:user) { create(:user) }
subject(:service) { described_class.new(project, user) }
let_it_be(:project) { create(:project_empty_repo) }
let_it_be(:user) { create(:user) }
describe '#execute' do
context 'when repository is empty' do
let(:project) { create(:project_empty_repo) }
it 'creates master branch' do
service.execute('my-feature', 'master')
expect(project.repository.branch_exists?('master')).to be_truthy
end
it 'creates my-feature branch' do
service.execute('my-feature', 'master')
it 'creates another-feature branch' do
service.execute('another-feature', 'master')
expect(project.repository.branch_exists?('my-feature')).to be_truthy
expect(project.repository.branch_exists?('another-feature')).to be_truthy
end
end
context 'when creating a branch fails' do
let(:project) { create(:project_empty_repo) }
context 'when branch already exists' do
it 'returns an error' do
result = service.execute('master', 'master')
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Branch already exists')
end
end
context 'when incorrect reference is provided' do
before do
allow(project.repository).to receive(:add_branch).and_return(false)
end
it 'returns an error with the branch name' do
result = service.execute('my-feature', 'master')
it 'returns an error with a reference name' do
result = service.execute('new-feature', 'unknown')
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Invalid reference name: my-feature")
expect(result[:message]).to eq('Invalid reference name: unknown')
end
end
end

0
vendor/gitignore/C++.gitignore vendored Executable file → Normal file
View File

0
vendor/gitignore/Java.gitignore vendored Executable file → Normal file
View File