Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-20 15:10:24 +00:00
parent b70394d26f
commit c70a70ea42
69 changed files with 663 additions and 736 deletions

View File

@ -2113,6 +2113,7 @@ Gitlab/NamespacedClass:
- 'ee/app/models/weight_note.rb'
- 'ee/app/policies/approval_merge_request_rule_policy.rb'
- 'ee/app/policies/approval_project_rule_policy.rb'
- 'ee/app/policies/approval_state_policy.rb'
- 'ee/app/policies/dast_scanner_profile_policy.rb'
- 'ee/app/policies/dast_site_profile_policy.rb'
- 'ee/app/policies/dast_site_validation_policy.rb'

View File

@ -1 +1,19 @@
export { BulletList as default } from '@tiptap/extension-bullet-list';
import { BulletList } from '@tiptap/extension-bullet-list';
import { getMarkdownSource } from '../services/markdown_sourcemap';
export default BulletList.extend({
addAttributes() {
return {
...this.parent?.(),
bullet: {
default: '*',
parseHTML(element) {
const bullet = getMarkdownSource(element)?.charAt(0);
return { bullet: '*+-'.includes(bullet) ? bullet : '*' };
},
},
};
},
});

View File

@ -118,8 +118,6 @@ const defaultSerializerConfig = {
},
};
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
/**
* A markdown serializer converts arbitrary Markdown content
* into a ProseMirror document and viceversa. To convert Markdown
@ -144,15 +142,15 @@ export default ({ render = () => null, serializerConfig = {} } = {}) => ({
deserialize: async ({ schema, content }) => {
const html = await render(content);
if (!html) {
return null;
}
if (!html) return null;
const parser = new DOMParser();
const {
body: { firstElementChild },
} = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
const { body } = parser.parseFromString(html, 'text/html');
// append original source as a comment that nodes can access
body.append(document.createComment(content));
const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
return state.toJSON();
},

View File

@ -0,0 +1,40 @@
const getFullSource = (element) => {
const commentNode = element.ownerDocument.body.lastChild;
if (commentNode.nodeName === '#comment') {
return commentNode.textContent.split('\n');
}
return [];
};
const getRangeFromSourcePos = (sourcePos) => {
const [start, end] = sourcePos.split('-');
const [startRow, startCol] = start.split(':');
const [endRow, endCol] = end.split(':');
return {
start: { row: Number(startRow) - 1, col: Number(startCol) - 1 },
end: { row: Number(endRow) - 1, col: Number(endCol) - 1 },
};
};
export const getMarkdownSource = (element) => {
if (!element.dataset.sourcepos) return undefined;
const source = getFullSource(element);
const range = getRangeFromSourcePos(element.dataset.sourcepos);
let elSource = '';
for (let i = range.start.row; i <= range.end.row; i += 1) {
if (i === range.start.row) {
elSource += source[i]?.substring(range.start.col);
} else if (i === range.end.row) {
elSource += `\n${source[i]?.substring(0, range.start.col)}`;
} else {
elSource += `\n${source[i]}` || '';
}
}
return elSource.trim();
};

View File

@ -98,12 +98,7 @@ export default {
},
},
methods: {
...mapActions([
'fetchCycleAnalyticsData',
'fetchStageData',
'setSelectedStage',
'setDateRange',
]),
...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']),
handleDateSelect(daysInPast) {
this.setDateRange(daysInPast);
},

View File

@ -1,4 +1,11 @@
fragment VersionListItem on DesignVersion {
id
sha
createdAt
author {
__typename
id
name
avatarUrl
}
}

View File

@ -1,13 +1,15 @@
#import "../fragments/design.fragment.graphql"
#import "../fragments/version.fragment.graphql"
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
designs {
...DesignItem
versions {
__typename
nodes {
id
sha
__typename
...VersionListItem
}
}
}

View File

@ -85,6 +85,13 @@ export const designUploadOptimisticResponse = (files) => {
__typename: 'DesignVersion',
id: -uniqueId(),
sha: -uniqueId(),
createdAt: '',
author: {
__typename: 'UserCore',
id: -uniqueId(),
name: '',
avatarUrl: '',
},
},
},
}));

View File

@ -43,7 +43,10 @@ const KNOWN_TYPES = [
},
];
export function isTextFile({ name, raw, content, mimeType = '' }) {
export function isTextFile({ name, raw, binary, content, mimeType = '' }) {
// some file objects already have a `binary` property set on them. If true, return false
if (binary) return false;
const knownType = KNOWN_TYPES.find((type) => type.isMatch(mimeType, name));
if (knownType) return knownType.isText;

View File

@ -32,7 +32,7 @@ export default {
},
computed: {
...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
...mapGetters([
'isLoading',
'isImportingAnyRepo',
@ -43,7 +43,7 @@ export default {
]),
pagePaginationStateKey() {
return `${this.filter}-${this.repositories.length}`;
return `${this.filter}-${this.repositories.length}-${this.pageInfo.page}`;
},
availableNamespaces() {

View File

@ -53,7 +53,6 @@ const importAll = ({ state, dispatch }) => {
const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
const nextPage = state.pageInfo.page + 1;
commit(types.SET_PAGE, nextPage);
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@ -67,11 +66,10 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
}),
)
.then(({ data }) => {
commit(types.SET_PAGE, nextPage);
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
})
.catch((e) => {
commit(types.SET_PAGE, nextPage - 1);
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else if (tooManyRequests(e)) {

View File

@ -9,7 +9,7 @@ const makeNewImportedProject = (importedProject) => ({
sanitizedName: importedProject.name,
providerLink: importedProject.providerLink,
},
importedProject,
importedProject: { ...importedProject },
});
const makeNewIncompatibleProject = (project) => ({
@ -63,15 +63,16 @@ export default {
factory: makeNewIncompatibleProject,
});
state.repositories = [
...newImportedProjects,
...state.repositories,
...repositories.providerRepos.map((project) => ({
const existingProjects = [...newImportedProjects, ...state.repositories];
const existingProjectNames = new Set(existingProjects.map((p) => p.importSource.fullName));
const newProjects = repositories.providerRepos
.filter((project) => !existingProjectNames.has(project.fullName))
.map((project) => ({
importSource: project,
importedProject: null,
})),
...newIncompatibleProjects,
];
}));
state.repositories = [...existingProjects, ...newProjects, ...newIncompatibleProjects];
if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
state.pageInfo.page -= 1;

View File

@ -3,6 +3,10 @@ import { GlBanner } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { setCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { EVENT_LABEL, DISMISS_EVENT, CLICK_EVENT } from '../constants';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
export default {
name: 'TerraformNotification',
@ -16,6 +20,7 @@ export default {
components: {
GlBanner,
},
mixins: [trackingMixin],
inject: ['terraformImagePath', 'bannerDismissedKey'],
data() {
return {
@ -31,6 +36,10 @@ export default {
handleClose() {
setCookie(this.bannerDismissedKey, true);
this.isVisible = false;
this.track(DISMISS_EVENT);
},
buttonClick() {
this.track(CLICK_EVENT);
},
},
};
@ -43,6 +52,7 @@ export default {
:button-link="docsUrl"
:svg-path="terraformImagePath"
variant="promotion"
@primary="buttonClick"
@close="handleClose"
>
<p>{{ $options.i18n.description }}</p>

View File

@ -0,0 +1,3 @@
export const EVENT_LABEL = 'terraform_banner';
export const DISMISS_EVENT = 'dismiss_banner';
export const CLICK_EVENT = 'click_button';

View File

@ -3,293 +3,4 @@
.cycle-analytics {
margin: 24px auto 0;
position: relative;
.landing {
margin-top: 0;
.inner-content {
white-space: normal;
h4,
p {
margin: 7px 0 0;
max-width: 480px;
padding: 0 $gl-padding;
@include media-breakpoint-down(sm) {
margin: 0 auto;
}
}
}
.svg-container svg {
width: 136px;
height: 136px;
}
}
.col-headers {
ul {
margin: 0;
padding: 0;
}
li {
line-height: 50px;
}
}
.card {
.content-block {
padding: 24px 0;
border-bottom: 0;
position: relative;
@include media-breakpoint-down(xs) {
padding: 6px 0 24px;
}
}
.column {
text-align: center;
@include media-breakpoint-down(xs) {
padding: 15px 0;
}
.header {
font-size: 30px;
line-height: 38px;
font-weight: $gl-font-weight-normal;
margin: 0;
}
.text {
color: var(--gray-500, $gray-500);
margin: 0;
}
&:last-child {
@include media-breakpoint-down(xs) {
text-align: center;
}
}
}
}
.stage-panel-body {
display: flex;
flex-wrap: wrap;
}
.stage-nav,
.stage-entries {
display: flex;
vertical-align: top;
font-size: $gl-font-size;
}
.stage-nav {
width: 40%;
margin-bottom: 0;
ul {
padding: 0;
margin: 0;
width: 100%;
}
li {
list-style-type: none;
}
.stage-nav-item {
line-height: 65px;
&.active {
background: var(--blue-50, $blue-50);
border-color: var(--blue-300, $blue-300);
box-shadow: inset 4px 0 0 0 var(--blue-500, $blue-500);
}
&:hover:not(.active) {
background-color: var(--gray-10, $gray-10);
box-shadow: inset 2px 0 0 0 var(--border-color, $border-color);
cursor: pointer;
}
.stage-nav-item-cell.stage-name {
width: 44.5%;
}
.stage-nav-item-cell.stage-median {
min-width: 43%;
}
.stage-empty,
.not-available {
color: var(--gray-500, $gray-500);
}
}
}
.stage-panel-container {
width: 100%;
overflow: auto;
}
.stage-panel {
min-width: 968px;
.card-header {
padding: 0;
background-color: transparent;
}
.events-description {
line-height: 65px;
}
.events-info {
color: var(--gray-500, $gray-500);
}
}
.stage-events {
min-height: 467px;
}
.stage-event-list {
margin: 0;
padding: 0;
}
.stage-event-item {
@include clearfix;
list-style-type: none;
padding-bottom: $gl-padding;
margin-bottom: $gl-padding;
border-bottom: 1px solid var(--gray-50, $gray-50);
&:last-child {
border-bottom: 0;
margin-bottom: 0;
}
.item-details,
.item-time {
float: left;
}
.item-details {
width: 75%;
}
.item-title {
margin: 0 0 2px;
&.issue-title,
&.commit-title,
&.merge-request-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
display: block;
a {
color: var(--gl-text-color, $gl-text-color);
}
}
}
.item-time {
width: 25%;
text-align: right;
}
.total-time {
font-size: $cycle-analytics-big-font;
color: var(--gl-text-color, $gl-text-color);
span {
color: var(--gl-text-color, $gl-text-color);
font-size: $gl-font-size;
}
}
.issue-date,
.build-date {
color: var(--gl-text-color, $gl-text-color);
}
.mr-link,
.issue-link,
.commit-author-link,
.issue-author-link {
color: var(--gl-text-color, $gl-text-color);
}
// Custom CSS for components
.item-conmmit-component {
.commit-icon {
svg {
display: inline-block;
width: 20px;
height: 20px;
vertical-align: bottom;
}
}
}
.merge-request-branch {
a {
max-width: 180px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
vertical-align: bottom;
}
}
}
// Custom Styles for stage items
.item-build-component {
.item-title {
.icon-build-status {
float: left;
margin-right: 5px;
position: relative;
top: 2px;
}
.item-build-name {
color: var(--gl-text-color, $gl-text-color);
}
.pipeline-id {
color: var(--gl-text-color, $gl-text-color);
padding: 0 3px 0 0;
}
.ref-name {
color: var(--gray-900, $gray-900);
display: inline-block;
max-width: 180px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.3;
vertical-align: top;
}
.commit-sha {
color: var(--blue-600, $blue-600);
line-height: 1.3;
vertical-align: top;
font-weight: $gl-font-weight-normal;
}
}
}
}

View File

@ -62,14 +62,10 @@ class Import::BitbucketController < Import::BaseController
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
already_added_projects_names = already_added_projects.map(&:import_source)
bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) || !repo.valid? }
bitbucket_repos.filter { |repo| repo.valid? }
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos

View File

@ -62,16 +62,10 @@ class Import::BitbucketServerController < Import::BaseController
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
# Use the import URL to filter beyond what BaseService#find_already_added_projects
already_added_projects = filter_added_projects('bitbucket_server', bitbucket_repos.map(&:browse_url))
already_added_projects_names = already_added_projects.map(&:import_source)
bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.browse_url) || !repo.valid? }
bitbucket_repos.filter { |repo| repo.valid? }
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
@ -90,12 +84,6 @@ class Import::BitbucketServerController < Import::BaseController
private
# rubocop: disable CodeReuse/ActiveRecord
def filter_added_projects(import_type, import_sources)
current_user.created_projects.where(import_type: import_type, import_source: import_sources).with_import_state
end
# rubocop: enable CodeReuse/ActiveRecord
def client
@client ||= BitbucketServer::Client.new(credentials)
end

View File

@ -74,16 +74,10 @@ class Import::FogbugzController < Import::BaseController
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
repos = client.repos
already_added_projects_names = already_added_projects.map(&:import_source)
repos.reject { |repo| already_added_projects_names.include? repo.name }
client.repos
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos

View File

@ -64,9 +64,7 @@ class Import::GithubController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
already_added_projects_names = already_added_projects.pluck(:import_source)
client_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) }
client_repos.to_a
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -39,16 +39,10 @@ class Import::GitlabController < Import::BaseController
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
already_added_projects_names = already_added_projects.map(&:import_source)
repos.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos

View File

@ -41,7 +41,7 @@ class Import::ManifestController < Import::BaseController
end
def create
repository = repositories.find do |project|
repository = importable_repos.find do |project|
project[:id] == params[:repo_id].to_i
end
@ -56,14 +56,10 @@ class Import::ManifestController < Import::BaseController
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
already_added_projects_names = already_added_projects.pluck(:import_url)
repositories.reject { |repo| already_added_projects_names.include?(repo[:url]) }
@importable_repos ||= manifest_import_metadata.repositories
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
@ -88,7 +84,7 @@ class Import::ManifestController < Import::BaseController
private
def ensure_import_vars
unless group && repositories.present?
unless group && importable_repos.present?
redirect_to(new_import_manifest_path)
end
end
@ -103,10 +99,6 @@ class Import::ManifestController < Import::BaseController
@manifest_import_status ||= Gitlab::ManifestImport::Metadata.new(current_user, fallback: session)
end
def repositories
@repositories ||= manifest_import_metadata.repositories
end
def find_jobs
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
end

View File

@ -1,14 +1,6 @@
# frozen_string_literal: true
module GroupsHelper
def group_sidebar_links
@group_sidebar_links ||= get_group_sidebar_links
end
def group_sidebar_link?(link)
group_sidebar_links.include?(link)
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
@ -33,20 +25,6 @@ module GroupsHelper
Ability.allowed?(current_user, :admin_group_member, group)
end
def group_issues_count(state:)
IssuesFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
.execute
.count
end
def group_merge_requests_count(state:)
MergeRequestsFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
.execute
.count
end
def group_dependency_proxy_image_prefix(group)
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
@ -181,36 +159,6 @@ module GroupsHelper
group.member_count > 1 || group.members_with_parents.count > 1
end
def get_group_sidebar_links
links = [:overview, :group_members]
resources = [:activity, :issues, :boards, :labels, :milestones,
:merge_requests]
links += resources.select do |resource|
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
# TODO Proper policies, such as `read_group_runners, should be implemented per
# See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
if can?(current_user, :admin_group, @group) && Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
links << :runners
end
if can?(current_user, :read_cluster, @group)
links << :kubernetes
end
if can?(current_user, :admin_group, @group)
links << :settings
end
if can?(current_user, :read_wiki, @group)
links << :wiki
end
links
end
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
icon = group_icon(group, class: "avatar-tile", width: 15, height: 15) if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?

View File

@ -45,7 +45,7 @@ class OnboardingProgress < ApplicationRecord
def onboard(namespace)
return unless root_namespace?(namespace)
safe_find_or_create_by(namespace: namespace)
create(namespace: namespace)
end
def onboarding?(namespace)

View File

@ -4,5 +4,5 @@
- unless public_visibility_restricted?
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://about.gitlab.com/"
= link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
= footer_message

View File

@ -1,3 +1 @@
-# We're migration the group sidebar to a logical model based structure. If you need to update
-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_group_menus.html.haml.
= render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user))

View File

@ -1 +0,0 @@
= render 'shared/sidebar_toggle_button'

View File

@ -0,0 +1,8 @@
---
name: add_namespace_and_project_to_snowplow_tracking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68277
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338670
milestone: '14.3'
type: development
group: group::product intelligence
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: roadmap_daterange_filter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55639
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323917
milestone: '14.3'
type: development
group: group::product planning
default_enabled: false

View File

@ -1,8 +1,31 @@
# frozen_string_literal: true
FastGettext.add_text_domain 'gitlab',
path: File.join(Rails.root, 'locale'),
type: :po,
ignore_fuzzy: true
translation_repositories = [
FastGettext::TranslationRepository.build(
'gitlab',
path: File.join(Rails.root, 'locale'),
type: :po,
ignore_fuzzy: true
)
]
Gitlab.jh do
translation_repositories.unshift(
FastGettext::TranslationRepository.build(
'gitlab',
path: File.join(Rails.root, 'jh', 'locale'),
type: :po,
ignore_fuzzy: true
)
)
end
FastGettext.add_text_domain(
'gitlab',
type: :chain,
chain: translation_repositories,
ignore_fuzzy: true
)
FastGettext.default_text_domain = 'gitlab'
FastGettext.default_locale = :en

View File

@ -577,9 +577,9 @@ panel_groups:
See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding-a-project-to-the-dashboard) for the maximum number of displayed projects.
## Environment data on Deploy Boards
## Environment data on deploy boards
[Deploy Boards](../user/project/deploy_boards.md) load information from Kubernetes about
[Deploy boards](../user/project/deploy_boards.md) load information from Kubernetes about
Pods and Deployments. However, data over 10 MB for a certain environment read from
Kubernetes won't be shown.

View File

@ -7568,9 +7568,19 @@ Describes a rule for who can approve merge requests.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="approvalruleapprovalsrequired"></a>`approvalsRequired` | [`Int`](#int) | Number of required approvals. |
| <a id="approvalruleapproved"></a>`approved` | [`Boolean`](#boolean) | Indicates if the rule is satisfied. |
| <a id="approvalruleapprovedby"></a>`approvedBy` | [`UserCoreConnection`](#usercoreconnection) | List of users defined in the rule that approved the merge request. (see [Connections](#connections)) |
| <a id="approvalrulecontainshiddengroups"></a>`containsHiddenGroups` | [`Boolean`](#boolean) | Indicates if the rule contains approvers from a hidden group. |
| <a id="approvalruleeligibleapprovers"></a>`eligibleApprovers` | [`UserCoreConnection`](#usercoreconnection) | List of all users eligible to approve the merge request (defined explicitly and from associated groups). (see [Connections](#connections)) |
| <a id="approvalrulegroups"></a>`groups` | [`GroupConnection`](#groupconnection) | List of groups added as approvers for the rule. (see [Connections](#connections)) |
| <a id="approvalruleid"></a>`id` | [`GlobalID!`](#globalid) | ID of the rule. |
| <a id="approvalrulename"></a>`name` | [`String`](#string) | Name of the rule. |
| <a id="approvalruleoverridden"></a>`overridden` | [`Boolean`](#boolean) | Indicates if the rule was overridden for the merge request. |
| <a id="approvalrulesection"></a>`section` | [`String`](#string) | Named section of the Code Owners file that the rule applies to. |
| <a id="approvalrulesourcerule"></a>`sourceRule` | [`ApprovalRule`](#approvalrule) | Source rule used to create the rule. |
| <a id="approvalruletype"></a>`type` | [`ApprovalRuleType`](#approvalruletype) | Type of the rule. |
| <a id="approvalruleusers"></a>`users` | [`UserCoreConnection`](#usercoreconnection) | List of users added as approvers for the rule. (see [Connections](#connections)) |
### `AwardEmoji`
@ -10600,6 +10610,7 @@ Maven metadata.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestallowcollaboration"></a>`allowCollaboration` | [`Boolean`](#boolean) | Indicates if members of the target project can push to the fork. |
| <a id="mergerequestapprovalstate"></a>`approvalState` | [`MergeRequestApprovalState!`](#mergerequestapprovalstate) | Information relating to rules that must be satisfied to merge this merge request. |
| <a id="mergerequestapprovalsleft"></a>`approvalsLeft` | [`Int`](#int) | Number of approvals left. |
| <a id="mergerequestapprovalsrequired"></a>`approvalsRequired` | [`Int`](#int) | Number of approvals required. |
| <a id="mergerequestapproved"></a>`approved` | [`Boolean!`](#boolean) | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
@ -10746,6 +10757,17 @@ Returns [`String!`](#string).
| ---- | ---- | ----------- |
| <a id="mergerequestreferencefull"></a>`full` | [`Boolean`](#boolean) | Boolean option specifying whether the reference should be returned in full. |
### `MergeRequestApprovalState`
Information relating to rules that must be satisfied to merge this merge request.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestapprovalstateapprovalrulesoverwritten"></a>`approvalRulesOverwritten` | [`Boolean`](#boolean) | Indicates if the merge request approval rules are overwritten for the merge request. |
| <a id="mergerequestapprovalstaterules"></a>`rules` | [`[ApprovalRule!]`](#approvalrule) | List of approval rules associated with the merge request. |
### `MergeRequestAssignee`
A user assigned to a merge request.

View File

@ -732,7 +732,7 @@ the `review/feature-1` spec takes precedence over `review/*` and `*` specs.
- [Use GitLab CI to deploy to multiple environments (blog post)](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/)
- [Review Apps](../review_apps/index.md): Use dynamic environments to deploy your code for every branch.
- [Deploy Boards](../../user/project/deploy_boards.md): View the status of your applications running on Kubernetes.
- [Deploy boards](../../user/project/deploy_boards.md): View the status of your applications running on Kubernetes.
- [Protected environments](protected_environments.md): Determine who can deploy code to your environments.
- [Environments Dashboard](../environments/environments_dashboard.md): View a summary of each
environment's operational health. **(PREMIUM)**

View File

@ -107,7 +107,7 @@ Its feature set is listed on the table below according to DevOps stages.
| [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
| [Canary Deployments](../user/project/canary_deployments.md) | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
| [Deploy Boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
| [Deploy boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
| [Feature Flags](../operations/feature_flags.md) **(PREMIUM)** | Deploy your features behind Feature Flags. |
| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |

View File

@ -90,6 +90,10 @@ Do not use. Instead, use **select** with buttons, links, menu items, and lists.
Do not use when talking about the product or its features. The documentation describes the product as it is today. ([Vale](../testing.md#vale) rule: [`CurrentStatus.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/CurrentStatus.yml))
## deploy board
Lowercase.
## Developer
When writing about the Developer role:

View File

@ -244,7 +244,7 @@ you to common environment tasks:
- **Stop environment** (**{stop}**) - For more information, see
[Stopping an environment](../../ci/environments/index.md#stop-an-environment)
GitLab displays the [Deploy Board](../../user/project/deploy_boards.md) below the
GitLab displays the [deploy board](../../user/project/deploy_boards.md) below the
environment's information, with squares representing pods in your
Kubernetes cluster, color-coded to show their status. Hovering over a square on
the deploy board displays the state of the deployment, and selecting the square

View File

@ -36,7 +36,7 @@ In order to:
[deploy to a Kubernetes cluster](../project/clusters/index.md#deploying-to-a-kubernetes-cluster)
successfully.
- Show pod usage correctly, you must
[enable Deploy Boards](../project/deploy_boards.md#enabling-deploy-boards).
[enable deploy boards](../project/deploy_boards.md#enabling-deploy-boards).
After you have successful deployments to your group-level or instance-level cluster:

View File

@ -66,7 +66,7 @@ With GitLab Enterprise Edition, you can also:
- [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server.
- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](../ci/pipelines/multi_project_pipelines.md).
- [Lock files](project/file_lock.md) to prevent conflicts.
- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](project/deploy_boards.md).
- View the current health and status of each CI environment running on Kubernetes with [deploy boards](project/deploy_boards.md).
- Leverage continuous delivery method with [Canary Deployments](project/canary_deployments.md).
- Scan your code for vulnerabilities and [display them in merge requests](application_security/sast/index.md).

View File

@ -26,7 +26,7 @@ percentage of users are affected and the change can either be fixed or quickly
reverted.
Leveraging [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments), visualize your canary
deployments right inside the [Deploy Board](deploy_boards.md), without the need to leave GitLab.
deployments right inside the [deploy board](deploy_boards.md), without the need to leave GitLab.
## Use cases
@ -47,9 +47,9 @@ this document.
## Enabling Canary Deployments
Canary deployments require that you properly configure Deploy Boards:
Canary deployments require that you properly configure deploy boards:
1. Follow the steps to [enable Deploy Boards](deploy_boards.md#enabling-deploy-boards).
1. Follow the steps to [enable deploy boards](deploy_boards.md#enabling-deploy-boards).
1. To track canary deployments you need to label your Kubernetes deployments and
pods with `track: canary`. To get started quickly, you can use the [Auto Deploy](../../topics/autodevops/stages.md#auto-deploy)
template for canary deployments that GitLab provides.
@ -61,13 +61,13 @@ This allows GitLab to discover whether a deployment is stable or canary (tempora
Once all of the above are set up and the pipeline has run at least once,
navigate to the environments page under **Pipelines > Environments**.
As the pipeline executes, Deploy Boards clearly mark canary pods, enabling
As the pipeline executes, deploy boards clearly mark canary pods, enabling
quick and easy insight into the status of each environment and deployment.
Canary deployments are marked with a yellow dot in the Deploy Board so that you
Canary deployments are marked with a yellow dot in the deploy board so that you
can easily notice them.
![Canary deployments on Deploy Board](img/deploy_boards_canary_deployments.png)
![Canary deployments on deploy board](img/deploy_boards_canary_deployments.png)
### Advanced traffic control with Canary Ingress
@ -104,17 +104,17 @@ Here's an example setup flow from scratch:
#### How to check the current traffic weight on a Canary Ingress
1. Visit the [Deploy Board](../../user/project/deploy_boards.md).
1. Visit the [deploy board](../../user/project/deploy_boards.md).
1. View the current weights on the right.
![Rollout Status Canary Ingress](img/canary_weight.png)
#### How to change the traffic weight on a Canary Ingress
You can change the traffic weight within your environment's Deploy Board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
You can change the traffic weight within your environment's deploy board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
or by sending requests to the [GraphQL API](../../api/graphql/getting_started.md#command-line).
To use your [Deploy Board](../../user/project/deploy_boards.md):
To use your [deploy board](../../user/project/deploy_boards.md):
1. Navigate to **Deployments > Environments** for your project.
1. Set the new weight with the dropdown on the right side.

View File

@ -30,7 +30,7 @@ features such as:
- Use [role-based or attribute-based access controls](cluster_access.md).
- Run serverless workloads on [Kubernetes with Knative](serverless/index.md).
- Connect GitLab to in-cluster applications using [cluster integrations](../../clusters/integrations.md).
- Use [Deploy Boards](../deploy_boards.md) to see the health and status of each CI [environment](../../../ci/environments/index.md) running on your Kubernetes cluster.
- Use [deploy boards](../deploy_boards.md) to see the health and status of each CI [environment](../../../ci/environments/index.md) running on your Kubernetes cluster.
- Use [Canary deployments](../canary_deployments.md) to update only a portion of your fleet with the latest version of your application.
- View your [Kubernetes podlogs](kubernetes_pod_logs.md) directly in GitLab.
- Connect to your cluster through GitLab [web terminals](deploy_to_cluster.md#web-terminals-for-kubernetes-clusters).

View File

@ -46,15 +46,15 @@ a [metrics dashboard](../../../operations/metrics/index.md) and select **View lo
[permissions](../../permissions.md#project-members-permissions) in the project.
1. To navigate to the **Log Explorer** from the sidebar menu, go to **Monitor > Logs**
([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22011) in GitLab 12.5.).
1. To navigate to the **Log Explorer** from a specific pod on a [Deploy Board](../deploy_boards.md):
1. To navigate to the **Log Explorer** from a specific pod on a [deploy board](../deploy_boards.md):
1. Go to **Deployments > Environments** and find the environment
which contains the desired pod, like `production`.
1. On the **Environments** page, you should see the status of the environment's
pods with [Deploy Boards](../deploy_boards.md).
pods with [deploy boards](../deploy_boards.md).
1. When mousing over the list of pods, GitLab displays a tooltip with the exact pod name
and status.
![Deploy Boards pod list](img/pod_logs_deploy_board.png)
![deploy boards pod list](img/pod_logs_deploy_board.png)
1. Click on the desired pod to display the **Log Explorer**.
### Logs view

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto, reference
---
# Deploy Boards **(FREE)**
# Deploy boards **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1589) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212320) to GitLab Free in 13.8.
@ -16,7 +16,7 @@ type: howto, reference
> This is [fixed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60525) in
> GitLab 13.12.
GitLab Deploy Boards offer a consolidated view of the current health and
GitLab deploy boards offer a consolidated view of the current health and
status of each CI [environment](../../ci/environments/index.md) running on [Kubernetes](https://kubernetes.io), displaying the status
of the pods in the deployment. Developers and other teammates can view the
progress and status of a rollout, pod by pod, in the workflow they already use
@ -28,23 +28,23 @@ environments by using [Auto DevOps](../../topics/autodevops/index.md).
## Overview
With Deploy Boards you can gain more insight into deploys with benefits such as:
With deploy boards you can gain more insight into deploys with benefits such as:
- Following a deploy from the start, not just when it's done
- Watching the rollout of a build across multiple servers
- Finer state detail (Succeeded, Running, Failed, Pending, Unknown)
- See [Canary Deployments](canary_deployments.md)
Here's an example of a Deploy Board of the production environment.
Here's an example of a deploy board of the production environment.
![Deploy Boards landing page](img/deploy_boards_landing_page.png)
![deploy boards landing page](img/deploy_boards_landing_page.png)
The squares represent pods in your Kubernetes cluster that are associated with
the given environment. Hovering above each square you can see the state of a
deploy rolling out. The percentage is the percent of the pods that are updated
to the latest release.
Since Deploy Boards are tightly coupled with Kubernetes, there is some required
Since deploy boards are tightly coupled with Kubernetes, there is some required
knowledge. In particular, you should be familiar with:
- [Kubernetes pods](https://kubernetes.io/docs/concepts/workloads/pods/)
@ -54,7 +54,7 @@ knowledge. In particular, you should be familiar with:
## Use cases
Since the Deploy Board is a visual representation of the Kubernetes pods for a
Since the deploy board is a visual representation of the Kubernetes pods for a
specific environment, there are a lot of use cases. To name a few:
- You want to promote what's running in staging, to production. You go to the
@ -73,9 +73,9 @@ specific environment, there are a lot of use cases. To name a few:
list, find the [Review App](../../ci/review_apps/index.md) you're interested in, and click the
manual action to deploy it to staging.
## Enabling Deploy Boards
## Enabling deploy boards
To display the Deploy Boards for a specific [environment](../../ci/environments/index.md) you should:
To display the deploy boards for a specific [environment](../../ci/environments/index.md) you should:
1. Have [defined an environment](../../ci/environments/index.md) with a deploy stage.
@ -83,7 +83,7 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
NOTE:
If you're using OpenShift, ensure that you're using the `Deployment` resource
instead of `DeploymentConfiguration`. Otherwise, the Deploy Boards don't render
instead of `DeploymentConfiguration`. Otherwise, the deploy boards don't render
correctly. For more information, read the
[OpenShift docs](https://docs.openshift.com/container-platform/3.7/dev_guide/deployments/kubernetes_deployments.html#kubernetes-deployments-vs-deployment-configurations)
and [GitLab issue #4584](https://gitlab.com/gitlab-org/gitlab/-/issues/4584).
@ -114,17 +114,17 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
If you use GCP to manage clusters, you can see the deployment details in GCP itself by navigating to **Workloads > deployment name > Details**:
![Deploy Boards Kubernetes Label](img/deploy_boards_kubernetes_label.png)
![deploy boards Kubernetes Label](img/deploy_boards_kubernetes_label.png)
Once all of the above are set up and the pipeline has run at least once,
navigate to the environments page under **Deployments > Environments**.
Deploy Boards are visible by default. You can explicitly click
Deploy boards are visible by default. You can explicitly click
the triangle next to their respective environment name in order to hide them.
### Example manifest file
The following example is an extract of a Kubernetes manifest deployment file, using the two annotations `app.gitlab.com/env` and `app.gitlab.com/app` to enable the **Deploy Boards**:
The following example is an extract of a Kubernetes manifest deployment file, using the two annotations `app.gitlab.com/env` and `app.gitlab.com/app` to enable the **deploy boards**:
```yaml
apiVersion: apps/v1

View File

@ -19,14 +19,20 @@ issue itself and the first commit related to that issue.
If the issue and the code you're committing are both in the same project,
add `#xxx` to the commit message, where `xxx` is the issue number.
If they are not in the same project, you can add the full URL to the issue
(`https://gitlab.com/<username>/<projectname>/issues/<xxx>`).
```shell
git commit -m "this is my commit message. Ref #xxx"
```
or
If they are in different projects, but in the same group,
add `projectname#xxx` to the commit message.
```shell
git commit -m "this is my commit message. Ref projectname#xxx"
```
If they are not in the same group, you can add the full URL to the issue
(`https://gitlab.com/<username>/<projectname>/issues/<xxx>`).
```shell
git commit -m "this is my commit message. Related to https://gitlab.com/<username>/<projectname>/issues/<xxx>"

View File

@ -5,9 +5,6 @@ module Gitlab
module Pipeline
module Chain
class Build < Chain::Base
include Gitlab::Allowable
include Chain::Helpers
def perform!
@pipeline.assign_attributes(
source: @command.source,
@ -23,35 +20,12 @@ module Gitlab
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
external_pull_request: @command.external_pull_request,
locked: @command.project.default_pipeline_lock,
variables_attributes: variables_attributes
)
locked: @command.project.default_pipeline_lock)
end
def break?
@pipeline.errors.any?
end
private
def variables_attributes
variables = Array(@command.variables_attributes)
# We allow parent pipelines to pass variables to child pipelines since
# these variables are coming from internal configurations. We will check
# permissions to :set_pipeline_variables when those are injected upstream,
# to the parent pipeline.
# In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
# the variables are provided from the outside and those should be guarded.
return variables if @command.creates_child_pipeline?
if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
error("Insufficient permissions to set pipeline variables")
variables = []
end
variables
end
end
end
end

View File

@ -6,7 +6,25 @@ module Gitlab
module Chain
class Build
class Associations < Chain::Base
include Gitlab::Allowable
include Chain::Helpers
def perform!
assign_pipeline_variables
assign_source_pipeline
end
def break?
@pipeline.errors.any?
end
private
def assign_pipeline_variables
@pipeline.variables_attributes = variables_attributes
end
def assign_source_pipeline
return unless @command.bridge
@pipeline.build_source_pipeline(
@ -17,8 +35,45 @@ module Gitlab
)
end
def break?
false
def variables_attributes
variables = Array(@command.variables_attributes)
variables = apply_permissions(variables)
validate_uniqueness(variables)
end
def apply_permissions(variables)
# We allow parent pipelines to pass variables to child pipelines since
# these variables are coming from internal configurations. We will check
# permissions to :set_pipeline_variables when those are injected upstream,
# to the parent pipeline.
# In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
# the variables are provided from the outside and those should be guarded.
return variables if @command.creates_child_pipeline?
if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
error("Insufficient permissions to set pipeline variables")
variables = []
end
variables
end
def validate_uniqueness(variables)
duplicated_keys = variables
.map { |var| var[:key] }
.tally
.filter_map { |key, count| key if count > 1 }
if duplicated_keys.empty?
variables
else
error(duplicate_variables_message(duplicated_keys), config_error: true)
[]
end
end
def duplicate_variables_message(keys)
"Duplicate variable #{'name'.pluralize(keys.size)}: #{keys.join(', ')}"
end
end
end

View File

@ -8,7 +8,8 @@ module Gitlab
def initialize(namespace: nil, project: nil, user: nil, **extra)
@namespace = namespace
@plan = @namespace&.actual_plan_name
@plan = namespace&.actual_plan_name
@project = project
@extra = extra
end
@ -34,14 +35,29 @@ module Gitlab
private
attr_accessor :namespace, :project, :extra, :plan
def to_h
{
environment: environment,
source: source,
plan: @plan,
extra: @extra
plan: plan,
extra: extra
}.merge(project_and_namespace)
end
def project_and_namespace
return {} unless ::Feature.enabled?(:add_namespace_and_project_to_snowplow_tracking, default_enabled: :yaml)
{
namespace_id: namespace&.id,
project_id: project_id
}
end
def project_id
project.is_a?(Integer) ? project : project&.id
end
end
end
end

View File

@ -16,11 +16,6 @@ module Sidebars
add_menu(Sidebars::Groups::Menus::SettingsMenu.new(context))
end
override :render_raw_menus_partial
def render_raw_menus_partial
'layouts/nav/sidebar/group_menus'
end
override :aria_label
def aria_label
context.group.subgroup? ? _('Subgroup navigation') : _('Group navigation')

View File

@ -15905,6 +15905,12 @@ msgstr ""
msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
msgstr ""
msgid "GroupRoadmap|This quarter"
msgstr ""
msgid "GroupRoadmap|This year"
msgstr ""
msgid "GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them."
msgstr ""
@ -15917,6 +15923,9 @@ msgstr ""
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
msgstr ""
msgid "GroupRoadmap|Within 3 years"
msgstr ""
msgid "GroupSAML|%{strongOpen}Warning%{strongClose} - Enabling %{linkStart}SSO enforcement%{linkEnd} can reduce security risks."
msgstr ""

View File

@ -75,16 +75,6 @@ RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo1[:id])
expect(json_response.dig("provider_repos", 1, "id")).to eq(repo2[:id])
end
it "does not show already added project" do
project = create(:project, import_type: 'manifest', namespace: user.namespace, import_status: :finished, import_url: repo1[:url])
get :status, format: :json
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos").length).to eq(1)
expect(json_response.dig("provider_repos", 0, "id")).not_to eq(repo1[:id])
end
end
context 'when the data is stored via Gitlab::ManifestImport::Metadata' do

View File

@ -76,9 +76,7 @@ describe('content_editor/extensions/attachment', () => {
const base64EncodedFile = 'data:image/png;base64,Zm9v';
beforeEach(() => {
renderMarkdown.mockResolvedValue(
loadMarkdownApiResult('project_wiki_attachment_image').body,
);
renderMarkdown.mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image'));
});
describe('when uploading succeeds', () => {
@ -153,7 +151,7 @@ describe('content_editor/extensions/attachment', () => {
});
describe('when the file has a zip (or any other attachment) mime type', () => {
const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link').body;
const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link');
beforeEach(() => {
renderMarkdown.mockResolvedValue(markdownApiResult);

View File

@ -11,10 +11,8 @@ describe('content_editor/extensions/code_block_highlight', () => {
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
beforeEach(() => {
const { html } = loadMarkdownApiResult('code_block');
tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
codeBlockHtmlFixture = html;
codeBlockHtmlFixture = loadMarkdownApiResult('code_block');
parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
tiptapEditor.commands.setContent(codeBlockHtmlFixture);

View File

@ -6,7 +6,8 @@ import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
const fixturePathPrefix = `api/markdown/${testName}.json`;
return getJSONFixture(fixturePathPrefix);
const fixture = getJSONFixture(fixturePathPrefix);
return fixture.body || fixture.html;
};
export const loadMarkdownApiExamples = () => {
@ -16,3 +17,9 @@ export const loadMarkdownApiExamples = () => {
return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
};
export const loadMarkdownApiExample = (testName) => {
return loadMarkdownApiExamples().find(([name, context]) => {
return (context ? `${context}_${name}` : name) === testName;
})[2];
};

View File

@ -9,8 +9,9 @@ describe('markdown processing', () => {
'correctly handles %s (context: %s)',
async (name, context, markdown) => {
const testName = context ? `${context}_${name}` : name;
const { html, body } = loadMarkdownApiResult(testName);
const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
const contentEditor = createContentEditor({
renderMarkdown: () => loadMarkdownApiResult(testName),
});
await contentEditor.setSerializedContent(markdown);
expect(contentEditor.getSerializedContent()).toBe(markdown);

View File

@ -0,0 +1,71 @@
import { Extension } from '@tiptap/core';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
import Paragraph from '~/content_editor/extensions/paragraph';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
import { loadMarkdownApiResult, loadMarkdownApiExample } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils';
const SourcemapExtension = Extension.create({
// lets add `source` attribute to every element using `getMarkdownSource`
addGlobalAttributes() {
return [
{
types: [Paragraph.name, BulletList.name, ListItem.name],
attributes: {
source: {
parseHTML: (element) => {
const source = getMarkdownSource(element);
if (source) return { source };
return {};
},
},
},
},
];
},
});
const tiptapEditor = createTestEditor({
extensions: [BulletList, ListItem, SourcemapExtension],
});
const {
builders: { doc, bulletList, listItem, paragraph },
} = createDocBuilder({
tiptapEditor,
names: {
bulletList: { nodeType: BulletList.name },
listItem: { nodeType: ListItem.name },
},
});
describe('content_editor/services/markdown_sourcemap', () => {
it('gets markdown source for a rendered HTML element', async () => {
const deserialized = await markdownSerializer({
render: () => loadMarkdownApiResult('bullet_list_style_3'),
serializerConfig: {},
}).deserialize({
schema: tiptapEditor.schema,
content: loadMarkdownApiExample('bullet_list_style_3'),
});
const expected = doc(
bulletList(
{ bullet: '+', source: '+ list item 1\n+ list item 2' },
listItem({ source: '+ list item 1' }, paragraph('list item 1')),
listItem(
{ source: '+ list item 2' },
paragraph('list item 2'),
bulletList(
{ bullet: '-', source: '- embedded list item 3' },
listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')),
),
),
),
);
expect(deserialized).toEqual(expected.toJSON());
});
});

View File

@ -338,6 +338,13 @@ describe('Design management index page', () => {
__typename: 'DesignVersion',
id: expect.anything(),
sha: expect.anything(),
createdAt: '',
author: {
__typename: 'UserCore',
id: expect.anything(),
name: '',
avatarUrl: '',
},
},
},
},

View File

@ -101,7 +101,13 @@ describe('optimistic responses', () => {
discussions: { __typename: 'DesignDiscussion', nodes: [] },
versions: {
__typename: 'DesignVersionConnection',
nodes: { __typename: 'DesignVersion', id: -1, sha: -1 },
nodes: {
__typename: 'DesignVersion',
id: expect.anything(),
sha: expect.anything(),
createdAt: '',
author: { __typename: 'UserCore', avatarUrl: '', name: '', id: expect.anything() },
},
},
},
],

View File

@ -66,11 +66,21 @@
- name: thematic_break
markdown: |-
---
- name: bullet_list
- name: bullet_list_style_1
markdown: |-
* list item 1
* list item 2
* embedded list item 3
- name: bullet_list_style_2
markdown: |-
- list item 1
- list item 2
* embedded list item 3
- name: bullet_list_style_3
markdown: |-
+ list item 1
+ list item 2
- embedded list item 3
- name: ordered_list
markdown: |-
1. list item 1

View File

@ -86,6 +86,11 @@ describe('WebIDE utils', () => {
expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true);
expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true);
});
it('returns true if there is a `binary` property already set on the file object', () => {
expect(isTextFile({ name: 'abc.txt', content: '' })).toBe(true);
expect(isTextFile({ name: 'abc.txt', content: '', binary: true })).toBe(false);
});
});
describe('trimPathComponents', () => {

View File

@ -85,7 +85,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(200, payload);
return testAction(
@ -93,8 +93,8 @@ describe('import_projects store actions', () => {
null,
localState,
[
{ type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 1 },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
@ -104,19 +104,14 @@ describe('import_projects store actions', () => {
);
});
it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_ERROR and SET_PAGE again mutations on an unsuccessful request', () => {
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
return testAction(
fetchRepos,
null,
localState,
[
{ type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 0 },
{ type: RECEIVE_REPOS_ERROR },
],
[{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
});
@ -135,7 +130,7 @@ describe('import_projects store actions', () => {
expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
});
it('correctly updates current page on an unsuccessful request', () => {
it('correctly keeps current page on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
const CURRENT_PAGE = 5;
@ -143,10 +138,7 @@ describe('import_projects store actions', () => {
fetchRepos,
null,
{ ...localState, pageInfo: { page: CURRENT_PAGE } },
expect.arrayContaining([
{ type: SET_PAGE, payload: CURRENT_PAGE + 1 },
{ type: SET_PAGE, payload: CURRENT_PAGE },
]),
expect.arrayContaining([]),
[],
);
});
@ -159,12 +151,7 @@ describe('import_projects store actions', () => {
fetchRepos,
null,
{ ...localState, filter: 'filter' },
[
{ type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 0 },
{ type: RECEIVE_REPOS_ERROR },
],
[{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
@ -183,8 +170,8 @@ describe('import_projects store actions', () => {
null,
{ ...localState, filter: 'filter' },
[
{ type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 1 },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),

View File

@ -1,7 +1,13 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
import TerraformNotification from '~/projects/terraform_notification/components/terraform_notification.vue';
import {
EVENT_LABEL,
DISMISS_EVENT,
CLICK_EVENT,
} from '~/projects/terraform_notification/constants';
jest.mock('~/lib/utils/common_utils');
@ -10,6 +16,7 @@ const bannerDismissedKey = 'terraform_notification_dismissed';
describe('TerraformNotificationBanner', () => {
let wrapper;
let trackingSpy;
const provideData = {
terraformImagePath,
@ -22,11 +29,13 @@ describe('TerraformNotificationBanner', () => {
provide: provideData,
stubs: { GlBanner },
});
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
wrapper.destroy();
parseBoolean.mockReturnValue(false);
unmockTracking();
});
describe('when the dismiss cookie is not set', () => {
@ -44,8 +53,26 @@ describe('TerraformNotificationBanner', () => {
expect(setCookie).toHaveBeenCalledWith(bannerDismissedKey, true);
});
it('should send the dismiss event', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, DISMISS_EVENT, {
label: EVENT_LABEL,
});
});
it('should remove the banner', () => {
expect(findBanner().exists()).toBe(false);
});
});
describe('when docs link is clicked', () => {
beforeEach(async () => {
await findBanner().vm.$emit('primary');
});
it('should send button click event', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, CLICK_EVENT, {
label: EVENT_LABEL,
});
});
});
});

View File

@ -267,61 +267,6 @@ RSpec.describe GroupsHelper do
end
end
describe '#group_sidebar_links' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
before do
group.add_owner(user)
allow(helper).to receive(:current_user) { user }
allow(helper).to receive(:can?) { |*args| Ability.allowed?(*args) }
helper.instance_variable_set(:@group, group)
end
it 'returns all the expected links' do
links = [
:overview, :activity, :issues, :labels, :milestones, :merge_requests,
:runners, :group_members, :settings
]
expect(helper.group_sidebar_links).to include(*links)
end
it 'excludes runners when the user cannot admin the group' do
expect(helper).to receive(:current_user) { user }
# TODO Proper policies, such as `read_group_runners, should be implemented per
# See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
expect(helper).to receive(:can?).twice.with(user, :admin_group, group) { false }
expect(helper.group_sidebar_links).not_to include(:runners)
end
it 'excludes runners when the feature "runner_list_group_view_vue_ui" is disabled' do
stub_feature_flags(runner_list_group_view_vue_ui: false)
expect(helper.group_sidebar_links).not_to include(:runners)
end
it 'excludes settings when the user can admin the group' do
expect(helper).to receive(:current_user) { user }
expect(helper).to receive(:can?).twice.with(user, :admin_group, group) { false }
expect(helper.group_sidebar_links).not_to include(:settings)
end
it 'excludes cross project features when the user cannot read cross project' do
cross_project_features = [:activity, :issues, :labels, :milestones,
:merge_requests]
allow(Ability).to receive(:allowed?).and_call_original
cross_project_features.each do |feature|
expect(Ability).to receive(:allowed?).with(user, "read_group_#{feature}".to_sym, group) { false }
end
expect(helper.group_sidebar_links).not_to include(*cross_project_features)
end
end
describe '#parent_group_options' do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, name: 'group') }

View File

@ -99,7 +99,7 @@ RSpec.describe Nav::NewDropdownHelper do
it 'has project menu item' do
expect(subject[:menu_sections]).to eq(
expected_menu_section(
title: 'GitLab',
title: _('GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_project',
title: 'New project/repository',
@ -117,7 +117,7 @@ RSpec.describe Nav::NewDropdownHelper do
it 'has group menu item' do
expect(subject[:menu_sections]).to eq(
expected_menu_section(
title: 'GitLab',
title: _('GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_group',
title: 'New group',
@ -135,7 +135,7 @@ RSpec.describe Nav::NewDropdownHelper do
it 'has new snippet menu item' do
expect(subject[:menu_sections]).to eq(
expected_menu_section(
title: 'GitLab',
title: _('GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_snippet',
title: 'New snippet',

View File

@ -3,10 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
let(:project) { create(:project, :repository) }
let(:user) { create(:user, developer_projects: [project]) }
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let(:pipeline) { Ci::Pipeline.new }
let(:step) { described_class.new(pipeline, command) }
let(:bridge) { nil }
let(:variables_attributes) do
[{ key: 'first', secret_value: 'world' },
{ key: 'second', secret_value: 'second_world' }]
end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
@ -20,7 +26,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
merge_request: nil,
project: project,
current_user: user,
bridge: bridge)
bridge: bridge,
variables_attributes: variables_attributes)
end
let(:step) { described_class.new(pipeline, command) }
shared_examples 'breaks the chain' do
it 'returns true' do
step.perform!
expect(step.break?).to be true
end
end
shared_examples 'does not break the chain' do
it 'returns false' do
step.perform!
expect(step.break?).to be false
end
end
context 'when a bridge is passed in to the pipeline creation' do
@ -37,26 +62,83 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
)
end
it 'never breaks the chain' do
step.perform!
expect(step.break?).to eq(false)
end
it_behaves_like 'does not break the chain'
end
context 'when a bridge is not passed in to the pipeline creation' do
let(:bridge) { nil }
it 'leaves the source pipeline empty' do
step.perform!
expect(pipeline.source_pipeline).to be_nil
end
it 'never breaks the chain' do
it_behaves_like 'does not break the chain'
end
it 'sets pipeline variables' do
step.perform!
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
end
context 'when project setting restrict_user_defined_variables is enabled' do
before do
project.update!(restrict_user_defined_variables: true)
end
context 'when user is developer' do
it_behaves_like 'breaks the chain'
it 'returns an error on variables_attributes', :aggregate_failures do
step.perform!
expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
expect(pipeline.variables).to be_empty
end
context 'when variables_attributes is not specified' do
let(:variables_attributes) { nil }
it_behaves_like 'does not break the chain'
it 'assigns empty variables' do
step.perform!
expect(pipeline.variables).to be_empty
end
end
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
it_behaves_like 'does not break the chain'
it 'assigns variables_attributes' do
step.perform!
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
end
end
end
context 'with duplicate pipeline variables' do
let(:variables_attributes) do
[{ key: 'first', secret_value: 'world' },
{ key: 'first', secret_value: 'second_world' }]
end
it_behaves_like 'breaks the chain'
it 'returns an error for variables_attributes' do
step.perform!
expect(step.break?).to eq(false)
expect(pipeline.errors.full_messages).to eq(['Duplicate variable name: first'])
expect(pipeline.variables).to be_empty
end
end
end

View File

@ -8,11 +8,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
let(:pipeline) { Ci::Pipeline.new }
let(:variables_attributes) do
[{ key: 'first', secret_value: 'world' },
{ key: 'second', secret_value: 'second_world' }]
end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :push,
@ -24,100 +19,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
schedule: nil,
merge_request: nil,
project: project,
current_user: user,
variables_attributes: variables_attributes)
current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
shared_examples 'builds pipeline' do
it 'builds a pipeline with the expected attributes' do
step.perform!
expect(pipeline.sha).not_to be_empty
expect(pipeline.sha).to eq project.commit.id
expect(pipeline.ref).to eq 'master'
expect(pipeline.tag).to be false
expect(pipeline.user).to eq user
expect(pipeline.project).to eq project
end
end
shared_examples 'breaks the chain' do
it 'returns true' do
step.perform!
expect(step.break?).to be true
end
end
shared_examples 'does not break the chain' do
it 'returns false' do
step.perform!
expect(step.break?).to be false
end
end
before do
stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
end
it_behaves_like 'does not break the chain'
it_behaves_like 'builds pipeline'
it 'sets pipeline variables' do
it 'does not break the chain' do
step.perform!
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
expect(step.break?).to be false
end
context 'when project setting restrict_user_defined_variables is enabled' do
before do
project.update!(restrict_user_defined_variables: true)
end
it 'builds a pipeline with the expected attributes' do
step.perform!
context 'when user is developer' do
it_behaves_like 'breaks the chain'
it_behaves_like 'builds pipeline'
it 'returns an error on variables_attributes', :aggregate_failures do
step.perform!
expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
expect(pipeline.variables).to be_empty
end
context 'when variables_attributes is not specified' do
let(:variables_attributes) { nil }
it_behaves_like 'does not break the chain'
it_behaves_like 'builds pipeline'
it 'assigns empty variables' do
step.perform!
expect(pipeline.variables).to be_empty
end
end
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
it_behaves_like 'does not break the chain'
it_behaves_like 'builds pipeline'
it 'assigns variables_attributes' do
step.perform!
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
end
end
expect(pipeline.sha).not_to be_empty
expect(pipeline.sha).to eq project.commit.id
expect(pipeline.ref).to eq 'master'
expect(pipeline.tag).to be false
expect(pipeline.user).to eq user
expect(pipeline.project).to eq project
end
it 'returns a valid pipeline' do

View File

@ -87,8 +87,26 @@ RSpec.describe Gitlab::Tracking::StandardContext do
end
end
it 'does not contain any ids' do
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
it 'does not contain user id' do
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id)
end
it 'contains namespace and project ids' do
expect(snowplow_context.to_json[:data].keys).to include(:project_id, :namespace_id)
end
it 'accepts just project id as integer' do
expect { described_class.new(project: 1).to_context }.not_to raise_error
end
context 'without add_namespace_and_project_to_snowplow_tracking feature' do
before do
stub_feature_flags(add_namespace_and_project_to_snowplow_tracking: false)
end
it 'does not contain any ids' do
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
end
end
end
end

View File

@ -47,7 +47,7 @@ RSpec.describe Gitlab::Tracking do
it "delegates to #{klass} destination" do
other_context = double(:context)
project = double(:project)
project = build_stubbed(:project)
user = double(:user)
expect(Gitlab::Tracking::StandardContext)

View File

@ -1248,16 +1248,47 @@ RSpec.describe Ci::CreatePipelineService do
end
context 'when pipeline variables are specified' do
let(:variables_attributes) do
[{ key: 'first', secret_value: 'world' },
{ key: 'second', secret_value: 'second_world' }]
end
subject(:pipeline) { execute_service(variables_attributes: variables_attributes).payload }
it 'creates a pipeline with specified variables' do
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
context 'with valid pipeline variables' do
let(:variables_attributes) do
[{ key: 'first', secret_value: 'world' },
{ key: 'second', secret_value: 'second_world' }]
end
it 'creates a pipeline with specified variables' do
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
end
end
context 'with duplicate pipeline variables' do
let(:variables_attributes) do
[{ key: 'hello', secret_value: 'world' },
{ key: 'hello', secret_value: 'second_world' }]
end
it 'fails to create the pipeline' do
expect(pipeline).to be_failed
expect(pipeline.variables).to be_empty
expect(pipeline.errors[:base]).to eq(['Duplicate variable name: hello'])
end
end
context 'with more than one duplicate pipeline variable' do
let(:variables_attributes) do
[{ key: 'hello', secret_value: 'world' },
{ key: 'hello', secret_value: 'second_world' },
{ key: 'single', secret_value: 'variable' },
{ key: 'other', secret_value: 'value' },
{ key: 'other', secret_value: 'other value' }]
end
it 'fails to create the pipeline' do
expect(pipeline).to be_failed
expect(pipeline.variables).to be_empty
expect(pipeline.errors[:base]).to eq(['Duplicate variable names: hello, other'])
end
end
end

View File

@ -103,6 +103,17 @@ RSpec.describe Ci::PipelineTriggerService do
end
end
context 'when params have duplicate variables' do
let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
let(:variables) { { 'TRIGGER_PAYLOAD' => 'duplicate value' } }
it 'creates a failed pipeline without variables' do
expect { result }.to change { Ci::Pipeline.count }
expect(result).to be_error
expect(result.message[:base]).to eq(['Duplicate variable name: TRIGGER_PAYLOAD'])
end
end
it_behaves_like 'detecting an unprocessable pipeline trigger'
end
@ -201,6 +212,17 @@ RSpec.describe Ci::PipelineTriggerService do
end
end
context 'when params have duplicate variables' do
let(:params) { { token: job.token, ref: 'master', variables: variables } }
let(:variables) { { 'TRIGGER_PAYLOAD' => 'duplicate value' } }
it 'creates a failed pipeline without variables' do
expect { result }.to change { Ci::Pipeline.count }
expect(result).to be_error
expect(result.message[:base]).to eq(['Duplicate variable name: TRIGGER_PAYLOAD'])
end
end
it_behaves_like 'detecting an unprocessable pipeline trigger'
end

View File

@ -82,16 +82,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id)
end
it "does not show already added project" do
project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim')
stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum)
get :status, format: :json
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos")).to eq([])
end
it "touches the etag cache store" do
stub_client(repos: [], orgs: [], each_page: [])

View File

@ -19,14 +19,4 @@ RSpec.shared_examples 'import controller status' do
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
end
it "does not show already added project" do
project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
stub_client(client_repos_field => [repo])
get :status, format: :json
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos")).to eq([])
end
end