diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 78769217d7c..b68508db993 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -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' diff --git a/app/assets/javascripts/content_editor/extensions/bullet_list.js b/app/assets/javascripts/content_editor/extensions/bullet_list.js index 01ead571fe1..941a04daf98 100644 --- a/app/assets/javascripts/content_editor/extensions/bullet_list.js +++ b/app/assets/javascripts/content_editor/extensions/bullet_list.js @@ -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 : '*' }; + }, + }, + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 57e8de2914b..73d14f93e79 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -118,8 +118,6 @@ const defaultSerializerConfig = { }, }; -const wrapHtmlPayload = (payload) => `
${payload}
`; - /** * 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(); }, diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js new file mode 100644 index 00000000000..a1199589c9b --- /dev/null +++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js @@ -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(); +}; diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue index c9ecac6829b..839e3769362 100644 --- a/app/assets/javascripts/cycle_analytics/components/base.vue +++ b/app/assets/javascripts/cycle_analytics/components/base.vue @@ -98,12 +98,7 @@ export default { }, }, methods: { - ...mapActions([ - 'fetchCycleAnalyticsData', - 'fetchStageData', - 'setSelectedStage', - 'setDateRange', - ]), + ...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']), handleDateSelect(daysInPast) { this.setDateRange(daysInPast); }, diff --git a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql index 7eb40b12f51..b715633a9f2 100644 --- a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql +++ b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql @@ -1,4 +1,11 @@ fragment VersionListItem on DesignVersion { id sha + createdAt + author { + __typename + id + name + avatarUrl + } } diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql index 84aeb374351..111f5ac18a7 100644 --- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql +++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql @@ -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 } } } diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js index 05b220801f2..7470f3d259b 100644 --- a/app/assets/javascripts/design_management/utils/design_management_utils.js +++ b/app/assets/javascripts/design_management/utils/design_management_utils.js @@ -85,6 +85,13 @@ export const designUploadOptimisticResponse = (files) => { __typename: 'DesignVersion', id: -uniqueId(), sha: -uniqueId(), + createdAt: '', + author: { + __typename: 'UserCore', + id: -uniqueId(), + name: '', + avatarUrl: '', + }, }, }, })); diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 275fecc5a32..53d3b853f03 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -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; diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue index 14d08caef34..0cd3519bcec 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue @@ -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() { diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js index 5cbc6e85bf3..92be028b8a9 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/actions.js +++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js @@ -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)) { diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js index c5e1922597a..45f7a684161 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js +++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js @@ -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; diff --git a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue index 02e31d6fbb3..2290d6a078f 100644 --- a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue +++ b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue @@ -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" >

{{ $options.i18n.description }}

diff --git a/app/assets/javascripts/projects/terraform_notification/constants.js b/app/assets/javascripts/projects/terraform_notification/constants.js new file mode 100644 index 00000000000..029f40b2ab2 --- /dev/null +++ b/app/assets/javascripts/projects/terraform_notification/constants.js @@ -0,0 +1,3 @@ +export const EVENT_LABEL = 'terraform_banner'; +export const DISMISS_EVENT = 'dismiss_banner'; +export const CLICK_EVENT = 'click_button'; diff --git a/app/assets/stylesheets/page_bundles/cycle_analytics.scss b/app/assets/stylesheets/page_bundles/cycle_analytics.scss index 2248d95ae24..5d42ece32c9 100644 --- a/app/assets/stylesheets/page_bundles/cycle_analytics.scss +++ b/app/assets/stylesheets/page_bundles/cycle_analytics.scss @@ -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; - } - } - } } diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 57bd39bbe06..d32755dbd94 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -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 diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 1846b1e0cec..31e9694ca1d 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -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 diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 9f91f3a1e1c..377292d47d8 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -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 diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 22bcd14d664..d7aebd25432 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -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 diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index cc68eb02741..662b02010ba 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -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 diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb index 8497e15c07c..956d0c9a2ae 100644 --- a/app/controllers/import/manifest_controller.rb +++ b/app/controllers/import/manifest_controller.rb @@ -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 diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 0e4aeaae20d..0fdb8b07260 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -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? diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb index 9185547d7cd..c12309d1852 100644 --- a/app/models/onboarding_progress.rb +++ b/app/models/onboarding_progress.rb @@ -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) diff --git a/app/views/devise/shared/_footer.html.haml b/app/views/devise/shared/_footer.html.haml index ca1adb48543..5803107a8f7 100644 --- a/app/views/devise/shared/_footer.html.haml +++ b/app/views/devise/shared/_footer.html.haml @@ -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 diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 980730bc3be..c2b50bc0e52 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -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)) diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml deleted file mode 100644 index 4c0ed6a888d..00000000000 --- a/app/views/layouts/nav/sidebar/_group_menus.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'shared/sidebar_toggle_button' diff --git a/config/feature_flags/development/add_namespace_and_project_to_snowplow_tracking.yml b/config/feature_flags/development/add_namespace_and_project_to_snowplow_tracking.yml new file mode 100644 index 00000000000..ebffae2a446 --- /dev/null +++ b/config/feature_flags/development/add_namespace_and_project_to_snowplow_tracking.yml @@ -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 diff --git a/config/feature_flags/development/roadmap_daterange_filter.yml b/config/feature_flags/development/roadmap_daterange_filter.yml new file mode 100644 index 00000000000..276242427c2 --- /dev/null +++ b/config/feature_flags/development/roadmap_daterange_filter.yml @@ -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 diff --git a/config/initializers_before_autoloader/001_fast_gettext.rb b/config/initializers_before_autoloader/001_fast_gettext.rb index ede38450582..76a1dafd2d8 100644 --- a/config/initializers_before_autoloader/001_fast_gettext.rb +++ b/config/initializers_before_autoloader/001_fast_gettext.rb @@ -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 diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 1ea2ec4c904..6c18f416e89 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -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. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d00739420bb..6600901e187 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7568,9 +7568,19 @@ Describes a rule for who can approve merge requests. | Name | Type | Description | | ---- | ---- | ----------- | +| `approvalsRequired` | [`Int`](#int) | Number of required approvals. | +| `approved` | [`Boolean`](#boolean) | Indicates if the rule is satisfied. | +| `approvedBy` | [`UserCoreConnection`](#usercoreconnection) | List of users defined in the rule that approved the merge request. (see [Connections](#connections)) | +| `containsHiddenGroups` | [`Boolean`](#boolean) | Indicates if the rule contains approvers from a hidden group. | +| `eligibleApprovers` | [`UserCoreConnection`](#usercoreconnection) | List of all users eligible to approve the merge request (defined explicitly and from associated groups). (see [Connections](#connections)) | +| `groups` | [`GroupConnection`](#groupconnection) | List of groups added as approvers for the rule. (see [Connections](#connections)) | | `id` | [`GlobalID!`](#globalid) | ID of the rule. | | `name` | [`String`](#string) | Name of the rule. | +| `overridden` | [`Boolean`](#boolean) | Indicates if the rule was overridden for the merge request. | +| `section` | [`String`](#string) | Named section of the Code Owners file that the rule applies to. | +| `sourceRule` | [`ApprovalRule`](#approvalrule) | Source rule used to create the rule. | | `type` | [`ApprovalRuleType`](#approvalruletype) | Type of the rule. | +| `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 | | ---- | ---- | ----------- | | `allowCollaboration` | [`Boolean`](#boolean) | Indicates if members of the target project can push to the fork. | +| `approvalState` | [`MergeRequestApprovalState!`](#mergerequestapprovalstate) | Information relating to rules that must be satisfied to merge this merge request. | | `approvalsLeft` | [`Int`](#int) | Number of approvals left. | | `approvalsRequired` | [`Int`](#int) | Number of approvals required. | | `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). | ---- | ---- | ----------- | | `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 | +| ---- | ---- | ----------- | +| `approvalRulesOverwritten` | [`Boolean`](#boolean) | Indicates if the merge request approval rules are overwritten for the merge request. | +| `rules` | [`[ApprovalRule!]`](#approvalrule) | List of approval rules associated with the merge request. | + ### `MergeRequestAssignee` A user assigned to a merge request. diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index 1b4d8890c6e..4a77e6d84b4 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -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)** diff --git a/doc/ci/index.md b/doc/ci/index.md index 9175c20f580..30c36d0a061 100644 --- a/doc/ci/index.md +++ b/doc/ci/index.md @@ -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. | diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 240b78a741f..3ba9af1bf98 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -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: diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 2cf5a5befd7..5d3a04c7096 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -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 diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md index cb721115e76..cad55f0cf0b 100644 --- a/doc/user/clusters/environments.md +++ b/doc/user/clusters/environments.md @@ -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: diff --git a/doc/user/index.md b/doc/user/index.md index 104da206e5f..d6eaad469c1 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -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). diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md index cac283f6f18..b4723294438 100644 --- a/doc/user/project/canary_deployments.md +++ b/doc/user/project/canary_deployments.md @@ -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. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index a0efea267f0..c534c30c75e 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -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). diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md index 7a9c7eb423d..eb0e8d0e91c 100644 --- a/doc/user/project/clusters/kubernetes_pod_logs.md +++ b/doc/user/project/clusters/kubernetes_pod_logs.md @@ -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 diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md index 64a5515136b..05f026cca18 100644 --- a/doc/user/project/deploy_boards.md +++ b/doc/user/project/deploy_boards.md @@ -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 diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md index 2b07131df6e..ed6c07f2c6d 100644 --- a/doc/user/project/issues/crosslinking_issues.md +++ b/doc/user/project/issues/crosslinking_issues.md @@ -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///issues/`). ```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///issues/`). ```shell git commit -m "this is my commit message. Related to https://gitlab.com///issues/" diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index d3bc3a38f1f..6feb693221b 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -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 diff --git a/lib/gitlab/ci/pipeline/chain/build/associations.rb b/lib/gitlab/ci/pipeline/chain/build/associations.rb index eb49c56bcd7..b5d63691849 100644 --- a/lib/gitlab/ci/pipeline/chain/build/associations.rb +++ b/lib/gitlab/ci/pipeline/chain/build/associations.rb @@ -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 diff --git a/lib/gitlab/tracking/standard_context.rb b/lib/gitlab/tracking/standard_context.rb index 7902f96dfa6..fe5669be014 100644 --- a/lib/gitlab/tracking/standard_context.rb +++ b/lib/gitlab/tracking/standard_context.rb @@ -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 diff --git a/lib/sidebars/groups/panel.rb b/lib/sidebars/groups/panel.rb index 73b943c5655..6efe89d496a 100644 --- a/lib/sidebars/groups/panel.rb +++ b/lib/sidebars/groups/panel.rb @@ -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') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8e6cfa285f8..94ce205e36f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb index d5a498e80d9..0111ad9501f 100644 --- a/spec/controllers/import/manifest_controller_spec.rb +++ b/spec/controllers/import/manifest_controller_spec.rb @@ -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 diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index 1334b1ddaad..97a33b28cdd 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -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); diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js index 188e6580dc6..828fdb224fc 100644 --- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js +++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js @@ -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); diff --git a/spec/frontend/content_editor/markdown_processing_examples.js b/spec/frontend/content_editor/markdown_processing_examples.js index 12eed00f3c6..b3aabfeb145 100644 --- a/spec/frontend/content_editor/markdown_processing_examples.js +++ b/spec/frontend/content_editor/markdown_processing_examples.js @@ -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]; +}; diff --git a/spec/frontend/content_editor/markdown_processing_spec.js b/spec/frontend/content_editor/markdown_processing_spec.js index da3f6e64db8..71565768558 100644 --- a/spec/frontend/content_editor/markdown_processing_spec.js +++ b/spec/frontend/content_editor/markdown_processing_spec.js @@ -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); diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js new file mode 100644 index 00000000000..0ef822942ea --- /dev/null +++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js @@ -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()); + }); +}); diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index 95cb1ac943c..d35c5398b20 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -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: '', + }, }, }, }, diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js index 5b7f99e9d96..dc6056badb9 100644 --- a/spec/frontend/design_management/utils/design_management_utils_spec.js +++ b/spec/frontend/design_management/utils/design_management_utils_spec.js @@ -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() }, + }, }, }, ], diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml index 09a57e04631..6924423eecc 100644 --- a/spec/frontend/fixtures/api_markdown.yml +++ b/spec/frontend/fixtures/api_markdown.yml @@ -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 diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index 00733615f81..2f58cd8f22a 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -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', () => { diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js index f2bfc61381c..0ebe8525b5a 100644 --- a/spec/frontend/import_entities/import_projects/store/actions_spec.js +++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js @@ -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 }), diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js index 71c22998b08..630d0ffae54 100644 --- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js +++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js @@ -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, + }); + }); + }); }); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 42da1cb71f1..661b1816548 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -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') } diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb index 03b9c538225..79b49be92a5 100644 --- a/spec/helpers/nav/new_dropdown_helper_spec.rb +++ b/spec/helpers/nav/new_dropdown_helper_spec.rb @@ -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', diff --git a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb index 5fa414f5bd1..32c92724f62 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index 7771289abe6..dca2204f544 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -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 diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index a0fb6a270a5..ca7a6b6b1c3 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -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 diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index 994316f38ee..90109db6db2 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -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) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 2fdb0ed3c0d..d5d65598589 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -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 diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb index 2f93b1ecd3c..29d12b0dd0e 100644 --- a/spec/services/ci/pipeline_trigger_service_spec.rb +++ b/spec/services/ci/pipeline_trigger_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index a9c6da7bc2b..0ffa32dec9e 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -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: []) diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb index b9ae0e23e26..44baadaaade 100644 --- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -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