diff options
73 files changed, 1476 insertions, 420 deletions
diff --git a/.haml-lint.yml b/.haml-lint.yml index 33a960f7efe..b6b0c63f286 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -8,6 +8,7 @@ exclude: - 'spec/**/*' require: - './haml_lint/linter/no_plain_nodes.rb' + - './haml_lint/linter/documentation_links.rb' linters: AltText: @@ -26,6 +27,12 @@ linters: enabled: false max_consecutive: 2 + DocumentationLinks: + enabled: false + include: + - 'app/views/**/*.haml' + - 'ee/app/views/**/*.haml' + EmptyObjectReference: enabled: true diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue index 4cc5a5cd6a2..b7fa3242fbf 100644 --- a/app/assets/javascripts/code_navigation/components/popover.vue +++ b/app/assets/javascripts/code_navigation/components/popover.vue @@ -1,10 +1,14 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlTabs, GlTab, GlLink, GlBadge } from '@gitlab/ui'; import DocLine from './doc_line.vue'; export default { components: { GlButton, + GlTabs, + GlTab, + GlLink, + GlBadge, DocLine, }, props: { @@ -54,6 +58,9 @@ export default { isDefinitionCurrentBlob() { return this.data.definition_path.indexOf(this.blobPath) === 0; }, + references() { + return this.data.references || []; + }, }, watch: { position: { @@ -82,37 +89,61 @@ export default { class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" > <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div> - <div class="overflow-auto code-navigation-popover-container"> - <div - v-for="(hover, index) in data.hover" - :key="index" - :class="{ 'border-bottom': index !== data.hover.length - 1 }" - > - <pre - v-if="hover.language" - ref="code-output" - :class="$options.colorScheme" - class="border-0 bg-transparent m-0 code highlight text-wrap" - ><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens"/></pre> - <p v-else ref="doc-output" class="p-3 m-0 gl-font-base"> - {{ hover.value }} + <gl-tabs nav-class="gl-hidden" content-class="gl-py-0"> + <gl-tab :title="__('Definition')"> + <div class="overflow-auto code-navigation-popover-container"> + <div + v-for="(hover, index) in data.hover" + :key="index" + :class="{ 'border-bottom': index !== data.hover.length - 1 }" + > + <pre + v-if="hover.language" + ref="code-output" + :class="$options.colorScheme" + class="border-0 bg-transparent m-0 code highlight text-wrap" + ><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens"/></pre> + <p v-else ref="doc-output" class="p-3 m-0"> + {{ hover.value }} + </p> + </div> + </div> + <div v-if="definitionPath || isCurrentDefinition" class="popover-body border-top"> + <span v-if="isCurrentDefinition" class="gl-font-weight-bold gl-font-base"> + {{ s__('CodeIntelligence|This is the definition') }} + </span> + <gl-button + v-else + :href="definitionPath" + :target="isDefinitionCurrentBlob ? null : '_blank'" + class="w-100" + variant="default" + data-testid="go-to-definition-btn" + > + {{ __('Go to definition') }} + </gl-button> + </div> + </gl-tab> + <gl-tab data-testid="references-tab" class="py-2"> + <template #title> + {{ __('References') }} + <gl-badge size="sm" class="gl-tab-counter-badge">{{ references.length }}</gl-badge> + </template> + <template v-if="references.length"> + <div v-for="(reference, index) in references" :key="index" class="gl-dropdown-item"> + <gl-link + :href="`${definitionPathPrefix}/${reference.path}`" + class="dropdown-item" + data-testid="reference-link" + > + {{ reference.path }} + </gl-link> + </div> + </template> + <p v-else class="gl-my-4 gl-px-4"> + {{ s__('CodeNavigation|No references found') }} </p> - </div> - </div> - <div v-if="definitionPath || isCurrentDefinition" class="popover-body border-top"> - <span v-if="isCurrentDefinition" class="gl-font-weight-bold gl-font-base"> - {{ s__('CodeIntelligence|This is the definition') }} - </span> - <gl-button - v-else - :href="definitionPath" - :target="isDefinitionCurrentBlob ? null : '_blank'" - class="w-100" - variant="default" - data-testid="go-to-definition-btn" - > - {{ __('Go to definition') }} - </gl-button> - </div> + </gl-tab> + </gl-tabs> </div> </template> diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 8d603e3ea44..12e2ce88b3c 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -53,7 +53,7 @@ export default { return this.type === 'jira'; }, showJiraIssuesFields() { - return this.isJira && this.glFeatures.jiraIntegration; + return this.isJira && this.glFeatures.jiraIssuesIntegration; }, }, }; diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index e1a40323f5d..f4787f5c1d4 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -293,10 +293,10 @@ export default { this.filters = filters; }, refetchIssuables() { - const ignored = ['utf8', 'state']; + const ignored = ['utf8']; const params = omit(this.filters, ignored); - historyPushState(setUrlParams(params, window.location.href, true)); + historyPushState(setUrlParams(params, window.location.href, true, true)); this.fetchIssuables(); }, handleFilter(filters) { diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js index 6b0c56c8dbd..40252c10d5f 100644 --- a/app/assets/javascripts/issuables_list/index.js +++ b/app/assets/javascripts/issuables_list/index.js @@ -36,7 +36,7 @@ function mountIssuableListRootApp() { } function mountIssuablesListApp() { - if (!gon.features?.vueIssuablesList && !gon.features?.jiraIntegration) { + if (!gon.features?.vueIssuablesList && !gon.features?.jiraIssuesIntegration) { return; } diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 0472b8cf51f..c6c34b831ee 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -344,9 +344,15 @@ export function objectToQuery(obj) { * @param {Object} params The query params to be set/updated * @param {String} url The url to be operated on * @param {Boolean} clearParams Indicates whether existing query params should be removed or not + * @param {Boolean} railsArraySyntax When enabled, changes the array syntax from `keys=` to `keys[]=` according to Rails conventions * @returns {String} A copy of the original with the updated query params */ -export const setUrlParams = (params, url = window.location.href, clearParams = false) => { +export const setUrlParams = ( + params, + url = window.location.href, + clearParams = false, + railsArraySyntax = false, +) => { const urlObj = new URL(url); const queryString = urlObj.search; const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString); @@ -355,11 +361,12 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f if (params[key] === null || params[key] === undefined) { searchParams.delete(key); } else if (Array.isArray(params[key])) { + const keyName = railsArraySyntax ? `${key}[]` : key; params[key].forEach((val, idx) => { if (idx === 0) { - searchParams.set(key, val); + searchParams.set(keyName, val); } else { - searchParams.append(key, val); + searchParams.append(keyName, val); } }); } else { diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 9fb07917f9b..63762e414df 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -7,11 +7,13 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectPermissionsSettings from '../shared/permissions'; +import initProjectRemoveModal from '~/projects/project_remove_modal'; document.addEventListener('DOMContentLoaded', () => { initFilePickers(); initConfirmDangerModal(); initSettingsPanels(); + initProjectRemoveModal(); mountBadgeSettings(PROJECT_BADGE); initProjectLoadingSpinner(); diff --git a/app/assets/javascripts/projects/components/remove_modal.vue b/app/assets/javascripts/projects/components/remove_modal.vue new file mode 100644 index 00000000000..37f58efcb30 --- /dev/null +++ b/app/assets/javascripts/projects/components/remove_modal.vue @@ -0,0 +1,108 @@ +<script> +import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { rstrip } from '~/lib/utils/common_utils'; +import csrf from '~/lib/utils/csrf'; + +export default { + components: { + GlModal, + GlSprintf, + GlFormInput, + GlButton, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + confirmPhrase: { + type: String, + required: true, + }, + warningMessage: { + type: String, + required: true, + }, + formPath: { + type: String, + required: true, + }, + }, + data() { + return { + userInput: null, + }; + }, + computed: { + buttonDisabled() { + return rstrip(this.userInput) !== this.confirmPhrase; + }, + csrfToken() { + return csrf.token; + }, + }, + methods: { + submitForm() { + this.$refs.form.submit(); + }, + }, + strings: { + removeProject: __('Remove project'), + title: __('Confirmation required'), + confirm: __('Confirm'), + dataLoss: __( + 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.', + ), + confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'), + }, + modalId: 'remove-project-modal', +}; +</script> + +<template> + <form ref="form" :action="formPath" method="post"> + <input type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{ + $options.strings.removeProject + }}</gl-button> + <gl-modal + ref="removeModal" + :modal-id="$options.modalId" + size="sm" + ok-variant="danger" + footer-class="bg-gray-light gl-p-5" + > + <template #modal-title>{{ $options.strings.title }}</template> + <template #modal-footer> + <div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0"> + <gl-button + :disabled="buttonDisabled" + category="primary" + variant="danger" + @click="submitForm" + > + {{ $options.strings.confirm }} + </gl-button> + </div> + </template> + <div> + <p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p> + <p class="gl-mb-0">{{ $options.strings.dataLoss }}</p> + <p> + <gl-sprintf :message="$options.strings.confirmText"> + <template #phrase_code> + <code>{{ confirmPhrase }}</code> + </template> + </gl-sprintf> + </p> + <gl-form-input + id="confirm_name_input" + v-model="userInput" + name="confirm_name_input" + type="text" + /> + </div> + </gl-modal> + </form> +</template> diff --git a/app/assets/javascripts/projects/project_remove_modal.js b/app/assets/javascripts/projects/project_remove_modal.js new file mode 100644 index 00000000000..dbdad1bf6f1 --- /dev/null +++ b/app/assets/javascripts/projects/project_remove_modal.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import RemoveProjectModal from './components/remove_modal.vue'; + +export default (selector = '#js-confirm-project-remove') => { + const el = document.querySelector(selector); + + if (!el) return; + + const { formPath, confirmPhrase, warningMessage } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(RemoveProjectModal, { + props: { + confirmPhrase, + warningMessage, + formPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js index 7bb6c275832..34cb74efabe 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js @@ -1,4 +1,4 @@ -import renderHtml from './renderers/render_html'; +import renderBlockHtml from './renderers/render_html_block'; import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownText from './renderers/render_kramdown_text'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; @@ -6,7 +6,7 @@ import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; -const htmlRenderers = [renderHtml]; +const htmlBlockRenderers = [renderBlockHtml]; const listRenderers = [renderKramdownList]; const paragraphRenderers = [renderIdentifierParagraph]; const textRenderers = [renderKramdownText, renderEmbeddedRubyText]; @@ -32,9 +32,9 @@ const buildCustomHTMLRenderer = ( ) => { const defaults = { htmlBlock(node, context) { - const allHtmlRenderers = [...customRenderers.list, ...htmlRenderers]; + const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers]; - return executeRenderer(allHtmlRenderers, node, context); + return executeRenderer(allHtmlBlockRenderers, node, context); }, htmlInline(node, context) { const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers]; @@ -47,7 +47,7 @@ const buildCustomHTMLRenderer = ( return executeRenderer(allListRenderers, node, context); }, paragraph(node, context) { - const allParagraphRenderers = [...customRenderers.list, ...paragraphRenderers]; + const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers]; return executeRenderer(allParagraphRenderers, node, context); }, diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js index c81478a8405..6937d2acb47 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js @@ -4,25 +4,36 @@ const buildToken = (type, tagName, props) => { const TAG_TYPES = { block: 'div', - inline: 'span', + inline: 'a', }; -export const buildUneditableOpenTokens = (token, type = TAG_TYPES.block) => { - return [ - buildToken('openTag', type, { - attributes: { contenteditable: false }, - classNames: [ - 'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', - ], - }), - token, - ]; +// Open helpers (singular and multiple) + +const buildUneditableOpenToken = (tagType = TAG_TYPES.block) => + buildToken('openTag', tagType, { + attributes: { contenteditable: false }, + classNames: [ + 'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', + ], + }); + +export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => { + return [buildUneditableOpenToken(tagType), token]; +}; + +// Close helpers (singular and multiple) + +export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) => + buildToken('closeTag', tagType); + +export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => { + return [token, buildUneditableCloseToken(tagType)]; }; -export const buildUneditableCloseToken = (type = TAG_TYPES.block) => buildToken('closeTag', type); +// Complete helpers (open plus close) -export const buildUneditableCloseTokens = (token, type = TAG_TYPES.block) => { - return [token, buildUneditableCloseToken(type)]; +export const buildUneditableTokens = token => { + return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; }; export const buildUneditableInlineTokens = token => { @@ -32,6 +43,19 @@ export const buildUneditableInlineTokens = token => { ]; }; -export const buildUneditableTokens = token => { - return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; +export const buildUneditableHtmlAsTextTokens = node => { + /* + Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain + nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want + to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html` + type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass ` + to prevent their persistence within the `text` content as the user did not intend these as edits. + + https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72 + */ + const regex = / data-tomark-pass /gm; + const content = node.literal.replace(regex, ''); + const htmlAsTextToken = buildToken('text', null, { content }); + + return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()]; }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js deleted file mode 100644 index a3b467851dc..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js +++ /dev/null @@ -1,9 +0,0 @@ -import { buildUneditableTokens } from './build_uneditable_token'; - -const canRender = ({ type }) => { - return type === 'htmlBlock'; -}; - -const render = (_, { origin }) => buildUneditableTokens(origin()); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js new file mode 100644 index 00000000000..b179ca61dba --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js @@ -0,0 +1,9 @@ +import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token'; + +const canRender = ({ type }) => { + return type === 'htmlBlock'; +}; + +const render = node => buildUneditableHtmlAsTextTokens(node); + +export default { canRender, render }; diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 6a133295cf1..d8e11ddd423 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -15,7 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController push_frontend_feature_flag(:junit_pipeline_view, project) push_frontend_feature_flag(:build_report_summary, project) push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true) - push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: false) + push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true) push_frontend_feature_flag(:pipelines_security_report_summary, project) end before_action :ensure_pipeline, only: [:show] diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 440d20e1db2..ece4147d399 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -13,7 +13,7 @@ class Projects::ServicesController < Projects::ApplicationController before_action :redirect_deprecated_prometheus_service, only: [:update] before_action only: :edit do push_frontend_feature_flag(:integration_form_refactor, default_enabled: true) - push_frontend_feature_flag(:jira_integration, @project) + push_frontend_feature_flag(:jira_issues_integration, @project, { default_enabled: true }) end respond_to :html diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb new file mode 100644 index 00000000000..02e306bc500 --- /dev/null +++ b/app/models/concerns/approvable_base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module ApprovableBase + extend ActiveSupport::Concern + + included do + has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :approved_by_users, through: :approvals, source: :user + end + + def has_approved?(user) + return false unless user + + approved_by_users.include?(user) + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index fbc9ade34e1..3a6b9cd557a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -20,6 +20,7 @@ class MergeRequest < ApplicationRecord include IgnorableColumns include MilestoneEventable include StateEventable + include ApprovableBase extend ::Gitlab::Utils::Override @@ -92,9 +93,6 @@ class MergeRequest < ApplicationRecord has_many :draft_notes has_many :reviews, inverse_of: :merge_request - has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - has_many :approved_by_users, through: :approvals, source: :user - KNOWN_MERGE_PARAMS = [ :auto_merge_strategy, :should_remove_source_branch, diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index af98a6ee36a..bccf0340749 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -8,6 +8,8 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated include ChecksCollaboration include Gitlab::Utils::StrongMemoize + APPROVALS_WIDGET_BASE_TYPE = 'base' + presents :merge_request def ci_status @@ -224,6 +226,22 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end end + def api_approvals_path + expose_path(api_v4_projects_merge_requests_approvals_path(id: project.id, merge_request_iid: merge_request.iid)) + end + + def api_approve_path + expose_path(api_v4_projects_merge_requests_approve_path(id: project.id, merge_request_iid: merge_request.iid)) + end + + def api_unapprove_path + expose_path(api_v4_projects_merge_requests_unapprove_path(id: project.id, merge_request_iid: merge_request.iid)) + end + + def approvals_widget_type + APPROVALS_WIDGET_BASE_TYPE + end + private def cached_can_be_reverted? diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb index 23266bfed7a..c51c08ab646 100644 --- a/app/serializers/merge_request_poll_cached_widget_entity.rb +++ b/app/serializers/merge_request_poll_cached_widget_entity.rb @@ -86,6 +86,18 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity presenter(merge_request).squash_on_merge? end + expose :api_approvals_path do |merge_request| + presenter(merge_request).api_approvals_path + end + + expose :api_approve_path do |merge_request| + presenter(merge_request).api_approve_path + end + + expose :api_unapprove_path do |merge_request| + presenter(merge_request).api_unapprove_path + end + private delegate :current_user, to: :request diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb index b0d311681c0..a365ebc29c9 100644 --- a/app/serializers/merge_request_poll_widget_entity.rb +++ b/app/serializers/merge_request_poll_widget_entity.rb @@ -157,6 +157,10 @@ class MergeRequestPollWidgetEntity < Grape::Entity presenter(merge_request).squash_on_merge? end + expose :approvals_widget_type do |merge_request| + presenter(merge_request).approvals_widget_type + end + private delegate :current_user, to: :request diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb index 87717edbfc3..9a6e103e5dd 100644 --- a/app/services/ci/create_job_artifacts_service.rb +++ b/app/services/ci/create_job_artifacts_service.rb @@ -22,7 +22,11 @@ module Ci return result unless result[:status] == :success headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size(artifact_type)) - headers[:ProcessLsif] = true if lsif?(artifact_type) + + if lsif?(artifact_type) + headers[:ProcessLsif] = true + headers[:ProcessLsifReferences] = Feature.enabled?(:code_navigation_references, project, default_enabled: false) + end success(headers: headers) end diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb index aeaaebdbff6..5bc44bdad00 100644 --- a/app/services/merge_requests/remove_approval_service.rb +++ b/app/services/merge_requests/remove_approval_service.rb @@ -4,7 +4,7 @@ module MergeRequests class RemoveApprovalService < MergeRequests::BaseService # rubocop: disable CodeReuse/ActiveRecord def execute(merge_request) - return unless approved_by_user?(merge_request) + return unless merge_request.has_approved?(current_user) # paranoid protection against running wrong deletes return unless merge_request.id && current_user.id @@ -24,10 +24,6 @@ module MergeRequests private - def approved_by_user?(merge_request) - merge_request.approved_by_users.include?(current_user) - end - def reset_approvals_cache(merge_request) merge_request.approvals.reset end diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 0631c024eb8..fea3ff4c3ba 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -10,7 +10,7 @@ = f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input' = f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do = _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.") - = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy') + = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy') .form-text.text-muted = _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.") = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries') diff --git a/app/views/projects/_remove.html.haml b/app/views/projects/_remove.html.haml index 6c84fbfeeb3..528d802261c 100644 --- a/app/views/projects/_remove.html.haml +++ b/app/views/projects/_remove.html.haml @@ -4,7 +4,6 @@ %h4.danger-title= _('Remove project') %p %strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.') - = form_tag(project_path(project), method: :delete) do - %p - %strong= _('Removed projects cannot be restored!') - = button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) } + %p + %strong= _('Removed projects cannot be restored!') + #js-confirm-project-remove{ data: { form_path: project_path(project), confirm_phrase: project.path, warning_message: remove_project_message(project) } } diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml index fc8683e1d19..ecbf6d9005d 100644 --- a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml +++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml @@ -8,4 +8,4 @@ - viewer.errors.messages.each do |error| %li= error.join(': ') -= link_to _('Learn more'), help_page_path('user/project/integrations/prometheus.md', anchor: 'defining-custom-dashboards-per-project') += link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md', anchor: 'defining-custom-dashboards-per-project') diff --git a/app/views/projects/services/prometheus/_custom_metrics.html.haml b/app/views/projects/services/prometheus/_custom_metrics.html.haml index 210d0f37d65..3642460467b 100644 --- a/app/views/projects/services/prometheus/_custom_metrics.html.haml +++ b/app/views/projects/services/prometheus/_custom_metrics.html.haml @@ -3,7 +3,7 @@ .col-lg-3 %p = s_('PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope "*" OR a manually configured Prometheus to be available.') - = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer" + = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer" .col-lg-9 .card.custom-monitored-metrics.js-panel-custom-monitored-metrics{ data: { qa_selector: 'custom_metrics_container', active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{@service.active}" } } diff --git a/app/views/projects/services/prometheus/_external_alerts.html.haml b/app/views/projects/services/prometheus/_external_alerts.html.haml index 7fc67a8cb4f..ffc7c37fff0 100644 --- a/app/views/projects/services/prometheus/_external_alerts.html.haml +++ b/app/views/projects/services/prometheus/_external_alerts.html.haml @@ -3,6 +3,6 @@ - notify_url = notify_project_prometheus_alerts_url(@project, format: :json) - authorization_key = @project.alerting_setting.try(:token) -- learn_more_url = help_page_path('user/project/integrations/prometheus', anchor: 'external-prometheus-instances') +- learn_more_url = help_page_path('operations/metrics/index.md', anchor: 'external-prometheus-instances') #js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: true } } diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml index 8bf150e6415..9f5160f3dd5 100644 --- a/app/views/projects/services/prometheus/_metrics.html.haml +++ b/app/views/projects/services/prometheus/_metrics.html.haml @@ -33,5 +33,5 @@ .flash-notice .flash-text = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe - = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'query-variables') + = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/dashboards/variables.md', anchor: 'query-variables') %ul.list-unstyled.metrics-list.js-missing-var-metrics-list diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index c44b64d88c0..b5452fcca55 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -71,6 +71,6 @@ = expanded ? _('Collapse') : _('Expand') %p = _("Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need.") - = link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy', target: '_blank', rel: 'noopener noreferrer') + = link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer') .settings-content = render 'projects/registry/settings/index' diff --git a/changelogs/unreleased/36330-v2-custom-renderer-html.yml b/changelogs/unreleased/36330-v2-custom-renderer-html.yml new file mode 100644 index 00000000000..4d0d2498143 --- /dev/null +++ b/changelogs/unreleased/36330-v2-custom-renderer-html.yml @@ -0,0 +1,5 @@ +--- +title: Add a custom HTML renderer to the Static Site Editor for HTML block syntax +merge_request: 36330 +author: +type: added diff --git a/changelogs/unreleased/id-expose-approvals-endpoints.yml b/changelogs/unreleased/id-expose-approvals-endpoints.yml new file mode 100644 index 00000000000..4689e5779e0 --- /dev/null +++ b/changelogs/unreleased/id-expose-approvals-endpoints.yml @@ -0,0 +1,5 @@ +--- +title: Expose approvals fields for FOSS FE +merge_request: 36564 +author: +type: changed diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile index 504db88d1d3..1d7f863c201 100644 --- a/danger/metadata/Dangerfile +++ b/danger/metadata/Dangerfile @@ -4,7 +4,6 @@ THROUGHPUT_LABELS = [ 'Community contribution', 'security', 'bug', - 'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360. 'feature', 'feature::addition', 'feature::maintenance', diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile index d5951e6d6c1..72e0c8e92f4 100644 --- a/danger/specs/Dangerfile +++ b/danger/specs/Dangerfile @@ -1,7 +1,6 @@ # frozen_string_literal: true NO_SPECS_LABELS = [ - 'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360. 'tooling', 'tooling::pipelines', 'tooling::workflow', diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index b03a2dae971..595447eeebc 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: howto --- -# Geo Troubleshooting **(PREMIUM ONLY)** +# Troubleshooting Geo **(PREMIUM ONLY)** Setting up Geo requires careful attention to details and sometimes it's easy to miss a step. diff --git a/doc/api/projects.md b/doc/api/projects.md index bdcce063249..ea8f154b144 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1831,12 +1831,19 @@ Example response: ## Remove project -This endpoint either: +This endpoint: - Removes a project including all associated resources (issues, merge requests etc). -- From [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for deletion. Actual - deletion happens after number of days specified in - [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). +- From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, +group admins can [configure](../user/group/index.md#enabling-delayed-project-removal-premium) projects within a group +to be deleted after a delayed period. +When enabled, actual deletion happens after the number of days +specified in the [default deletion period](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). + +CAUTION: **Warning:** +The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 +was changed to [Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) +in GitLab 13.2, as discussed in [Enabling delayed project removal](../user/group/index.md#enabling-delayed-project-removal-premium). ```plaintext DELETE /projects/:id diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md index fff0fda0ab4..b7dd74a0230 100644 --- a/doc/ci/directed_acyclic_graph/index.md +++ b/doc/ci/directed_acyclic_graph/index.md @@ -82,10 +82,10 @@ are certain use cases that you may need to work around. For more information: ## DAG Visualization > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215517) in GitLab 13.1 as a [Beta feature](https://about.gitlab.com/handbook/product/#beta). -> - It's deployed behind a feature flag, disabled by default. +> - It was deployed behind a feature flag, disabled by default. +> - It became [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36802) in 13.2. > - It's enabled on GitLab.com. -> - It's not recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-dag-visualization-core-only) +> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-dag-visualization-core-only). The DAG visualization makes it easier to visualize the relationships between dependent jobs in a DAG. This graph will display all the jobs in a pipeline that need or are needed by other jobs. Jobs with no relationships are not displayed in this view. @@ -97,15 +97,15 @@ Clicking a node will highlight all the job paths it depends on. ### Enable or disable DAG Visualization **(CORE ONLY)** -DAG Visualization is under development and requires more testing, but is being made available as a beta features so users can check its limitations and uses. +DAG Visualization is under development, but is being made available as a beta feature so users can check its limitations and uses. -It is deployed behind a feature flag that is **disabled by default**. +It is deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) -can opt to enable it for your instance: +can opt to disable it for your instance: ```ruby # Instance-wide -Feature.enable(:dag_pipeline_tab) +Feature.disable(:dag_pipeline_tab) # or by project -Feature.enable(:dag_pipeline_tab, Project.find(<project id>)) +Feature.disable(:dag_pipeline_tab, Project.find(<project id>)) ``` diff --git a/doc/operations/metrics/dashboards/yaml.md b/doc/operations/metrics/dashboards/yaml.md index 45820a3cf49..355acb1aa34 100644 --- a/doc/operations/metrics/dashboards/yaml.md +++ b/doc/operations/metrics/dashboards/yaml.md @@ -69,7 +69,7 @@ Panels in a panel group are laid out in rows consisting of two panels per row. A | Property | Type | Required | Description | | ----------- | ------ | ----------------------------- | -------------------------------------------------------------------- | | `name` | string | no, but highly encouraged | Y-Axis label for the panel. Replaces `y_label` if set. | -| `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](../../../user/project/integrations/prometheus_units.md). | +| `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](yaml_number_format.md). | | `precision` | number | no, defaults to `2` | Number of decimal places to display in the number. | | ## **Metrics (`metrics`) properties** diff --git a/doc/operations/metrics/dashboards/yaml_number_format.md b/doc/operations/metrics/dashboards/yaml_number_format.md new file mode 100644 index 00000000000..ae0cd9fff64 --- /dev/null +++ b/doc/operations/metrics/dashboards/yaml_number_format.md @@ -0,0 +1,177 @@ +--- +stage: Monitor +group: APM +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + +# Unit formats reference + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201999) in GitLab 12.9. + +Format the data in your dashboard panels. + +You can select units to format your charts by adding `format` to your +[axis configuration](yaml.md). + +## Internationalization and localization + +Currently, your [internationalization and localization options](https://en.wikipedia.org/wiki/Internationalization_and_localization) for number formatting are dependent on the system you are using i.e. your OS or browser. + +## Engineering Notation + +For generic or default data, numbers are formatted according to the current locale in [engineering notation](https://en.wikipedia.org/wiki/Engineering_notation). + +While an [engineering notation exists for the web](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat), GitLab uses a version based off the [scientific notation](https://en.wikipedia.org/wiki/Scientific_notation). GitLab formatting acts in accordance with SI prefixes. For example, using GitLab notation, `1500.00` becomes `1.5k` instead of `1.5E3`. Keep this distinction in mind when using the engineering notation for your metrics. + +Formats: `engineering` + +SI prefixes: + +| Name | Symbol | Value | +| ---------- | ------- | -------------------------- | +| `yotta` | Y | 1000000000000000000000000 | +| `zetta` | Z | 1000000000000000000000 | +| `exa` | E | 1000000000000000000 | +| `peta` | P | 1000000000000000 | +| `tera` | T | 1000000000000 | +| `giga` | G | 1000000000 | +| `mega` | M | 1000000 | +| `kilo` | k | 1000 | +| `milli` | m | 0.001 | +| `micro` | μ | 0.000001 | +| `nano` | n | 0.000000001 | +| `pico` | p | 0.000000000001 | +| `femto` | f | 0.000000000000001 | +| `atto` | a | 0.000000000000000001 | +| `zepto` | z | 0.000000000000000000001 | +| `yocto` | y | 0.000000000000000000000001 | + +**Examples:** + +| Data | Displayed | +| --------------------------------- | --------- | +| `0.000000000000000000000008` | 8y | +| `0.000000000000000000008` | 8z | +| `0.000000000000000008` | 8a | +| `0.000000000000008` | 8f | +| `0.000000000008` | 8p | +| `0.000000008` | 8n | +| `0.000008` | 8μ | +| `0.008` | 8m | +| `10` | 10 | +| `1080` | 1.08k | +| `18000` | 18k | +| `18888` | 18.9k | +| `188888` | 189k | +| `18888888` | 18.9M | +| `1888888888` | 1.89G | +| `1888888888888` | 1.89T | +| `1888888888888888` | 1.89P | +| `1888888888888888888` | 1.89E | +| `1888888888888888888888` | 1.89Z | +| `1888888888888888888888888` | 1.89Y | +| `1888888888888888888888888888` | 1.89e+27 | + +## Numbers + +For number data, numbers are formatted according to the current locale. + +Formats: `number` + +**Examples:** + +| Data | Displayed | +| ---------- | --------- | +| `10` | 1 | +| `1000` | 1,000 | +| `1000000` | 1,000,000 | + +## Percentage + +For percentage data, format numbers in the chart with a `%` symbol. + +Formats supported: `percent`, `percentHundred` + +**Examples:** + +| Format | Data | Displayed | +| ---------------- | ----- | --------- | +| `percent` | `0.5` | 50% | +| `percent` | `1` | 100% | +| `percent` | `2` | 200% | +| `percentHundred` | `50` | 50% | +| `percentHundred` | `100` | 100% | +| `percentHundred` | `200` | 200% | + +## Duration + +For time durations, format numbers in the chart with a time unit symbol. + +Formats supported: `milliseconds`, `seconds` + +**Examples:** + +| Format | Data | Displayed | +| -------------- | ------ | --------- | +| `milliseconds` | `10` | 10ms | +| `milliseconds` | `500` | 100ms | +| `milliseconds` | `1000` | 1000ms | +| `seconds` | `10` | 10s | +| `seconds` | `500` | 500s | +| `seconds` | `1000` | 1000s | + +## Digital (Metric) + +Converts a number of bytes using metric prefixes. It scales to +use the unit that's the best fit. + +Formats supported: + +- `decimalBytes` +- `kilobytes` +- `megabytes` +- `gigabytes` +- `terabytes` +- `petabytes` + +**Examples:** + +| Format | Data | Displayed | +| -------------- | --------- | --------- | +| `decimalBytes` | `1` | 1B | +| `decimalBytes` | `1000` | 1kB | +| `decimalBytes` | `1000000` | 1MB | +| `kilobytes` | `1` | 1kB | +| `kilobytes` | `1000` | 1MB | +| `kilobytes` | `1000000` | 1GB | +| `megabytes` | `1` | 1MB | +| `megabytes` | `1000` | 1GB | +| `megabytes` | `1000000` | 1TB | + +## Digital (IEC) + +Converts a number of bytes using binary prefixes. It scales to +use the unit that's the best fit. + +Formats supported: + +- `bytes` +- `kibibytes` +- `mebibytes` +- `gibibytes` +- `tebibytes` +- `pebibytes` + +**Examples:** + +| Format | Data | Displayed | +| ----------- | ------------- | --------- | +| `bytes` | `1` | 1B | +| `bytes` | `1024` | 1KiB | +| `bytes` | `1024 * 1024` | 1MiB | +| `kibibytes` | `1` | 1KiB | +| `kibibytes` | `1024` | 1MiB | +| `kibibytes` | `1024 * 1024` | 1GiB | +| `mebibytes` | `1` | 1MiB | +| `mebibytes` | `1024` | 1GiB | +| `mebibytes` | `1024 * 1024` | 1TiB | diff --git a/doc/operations/metrics/index.md b/doc/operations/metrics/index.md index 8c7c4bf92be..cb4266d3361 100644 --- a/doc/operations/metrics/index.md +++ b/doc/operations/metrics/index.md @@ -132,7 +132,7 @@ If the metric exceeds the threshold of the alert for over 5 minutes, an email wi > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202146) in GitLab 13.2. -You can use keyboard shortcuts to interact more quickly with your currently-focused chartpanel. To activate keyboard shortcuts, use keyboard tabs to highlight the**{ellipsis_v}** **More actions** dropdown menu, or hover over the dropdown menuwith your mouse, then press the key corresponding to your desired action: +You can use keyboard shortcuts to interact more quickly with your currently-focused chartpanel. To activate keyboard shortcuts, use keyboard tabs to highlight the**{ellipsis_v}** **More actions** dropdown menu, or hover over the dropdown menu with your mouse, then press the key corresponding to your desired action: - **Expand panel** - <kbd>e</kbd> - **View logs** - <kbd>l</kbd> (lowercase 'L') diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index d9ca4a0881a..92eeb6a04b7 100644 --- a/doc/user/admin_area/settings/visibility_and_access_controls.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -68,8 +68,16 @@ To ensure only admin users can delete projects: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6. -By default, a project or group marked for removal will be permanently removed after 7 days. -This period may be changed, and setting this period to 0 will enable immediate removal +By default, a project marked for deletion will be permanently removed with immediate effect. +By default, a group marked for deletion will be permanently removed after 7 days. + +CAUTION: **Warning:** +The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to +[Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2. + +Projects within a group can be deleted after a delayed period, by [configuring in Group Settings](../../group/index.md#enabling-delayed-project-removal-premium). + +The default period is 7 days, and can be changed. Setting this period to 0 will enable immediate removal of projects or groups. To change this period: diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png Binary files differindex 6df2765a2ad..7cab7b0a61f 100644 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png +++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 84b2e48d9d3..465b2204250 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -650,6 +650,23 @@ To enable this feature: 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable group mentions**. 1. Click **Save changes**. +#### Enabling delayed Project removal **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2. + +By default, projects within a group are deleted immediately. +Optionally, on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, +you can configure the projects within a group to be deleted after a delayed interval. + +During this interval period, the projects will be in a read-only state and can be restored, if required. +The interval period defaults to 7 days, and can be modified by an admin in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). + +To enable delayed deletion of projects: + +1. Navigate to the group's **Settings > General** page. +1. Expand the **Permissions, LFS, 2FA** section, and check **Enable delayed project removal**. +1. Click **Save changes**. + ### Advanced settings - **Projects**: View all projects within that group, add members to each project, diff --git a/doc/user/project/integrations/img/prometheus_manual_configuration_v13_2.png b/doc/user/project/integrations/img/prometheus_manual_configuration_v13_2.png Binary files differnew file mode 100644 index 00000000000..b6ec08f492d --- /dev/null +++ b/doc/user/project/integrations/img/prometheus_manual_configuration_v13_2.png diff --git a/doc/user/project/integrations/img/prometheus_service_configuration.png b/doc/user/project/integrations/img/prometheus_service_configuration.png Binary files differdeleted file mode 100644 index a38d1bce197..00000000000 --- a/doc/user/project/integrations/img/prometheus_service_configuration.png +++ /dev/null diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 96e301cc639..85b2683463f 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -99,14 +99,24 @@ Installing and configuring Prometheus to monitor applications is fairly straight The actual configuration of Prometheus integration within GitLab is very simple. All you will need is the domain name or IP address of the Prometheus server you'd like -to integrate with. - -1. Navigate to the [Integrations page](overview.md#accessing-integrations). +to integrate with. If the Prometheus resource is secured with Google's Identity-Aware Proxy (IAP), +additional information like Client ID and Service Account credentials can be passed which +GitLab can use to access the resource. More information about authentication from a +service account can be found at Google's documentation for +[Authenticating from a service account](https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account). + +1. Navigate to the [Integrations page](overview.md#accessing-integrations) at + **{settings}** **Settings > Integrations**. 1. Click the **Prometheus** service. -1. Provide the domain name or IP address of your server, for example `http://prometheus.example.com/` or `http://192.0.2.1/`. +1. For **API URL**, provide the domain name or IP address of your server, such as + `http://prometheus.example.com/` or `http://192.0.2.1/`. +1. (Optional) In **Google IAP Audience Client ID**, provide the Client ID of the + Prometheus OAuth Client secured with Google IAP. +1. (Optional) In **Google IAP Service Account JSON**, provide the contents of the + Service Account credentials file that is authorized to access the Prometheus resource. 1. Click **Save changes**. -![Configure Prometheus Service](img/prometheus_service_configuration.png) +![Configure Prometheus Service](img/prometheus_manual_configuration_v13_2.png) #### Thanos configuration in GitLab diff --git a/doc/user/project/integrations/prometheus_units.md b/doc/user/project/integrations/prometheus_units.md index 6f48ad3de82..ee4f3ed77d4 100644 --- a/doc/user/project/integrations/prometheus_units.md +++ b/doc/user/project/integrations/prometheus_units.md @@ -1,175 +1,5 @@ --- -stage: Monitor -group: APM -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +redirect_to: '../../../operations/metrics/dashboards/yaml_number_format.md' --- -# Unit formats reference - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201999) in GitLab 12.9. - -You can select units to format your charts by adding `format` to your -[axis configuration](../../../operations/metrics/dashboards/yaml.md). - -## Internationalization and localization - -Currently, your [internationalization and localization options](https://en.wikipedia.org/wiki/Internationalization_and_localization) for number formatting are dependent on the system you are using i.e. your OS or browser. - -## Engineering Notation - -For generic or default data, numbers are formatted according to the current locale in [engineering notation](https://en.wikipedia.org/wiki/Engineering_notation). - -While an [engineering notation exists for the web](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat), GitLab uses a version based off the [scientific notation](https://en.wikipedia.org/wiki/Scientific_notation). GitLab formatting acts in accordance with SI prefixes. For example, using GitLab notation, `1500.00` becomes `1.5k` instead of `1.5E3`. Keep this distinction in mind when using the engineering notation for your metrics. - -Formats: `engineering` - -SI prefixes: - -| Name | Symbol | Value | -| ---------- | ------- | -------------------------- | -| `yotta` | Y | 1000000000000000000000000 | -| `zetta` | Z | 1000000000000000000000 | -| `exa` | E | 1000000000000000000 | -| `peta` | P | 1000000000000000 | -| `tera` | T | 1000000000000 | -| `giga` | G | 1000000000 | -| `mega` | M | 1000000 | -| `kilo` | k | 1000 | -| `milli` | m | 0.001 | -| `micro` | μ | 0.000001 | -| `nano` | n | 0.000000001 | -| `pico` | p | 0.000000000001 | -| `femto` | f | 0.000000000000001 | -| `atto` | a | 0.000000000000000001 | -| `zepto` | z | 0.000000000000000000001 | -| `yocto` | y | 0.000000000000000000000001 | - -**Examples:** - -| Data | Displayed | -| --------------------------------- | --------- | -| `0.000000000000000000000008` | 8y | -| `0.000000000000000000008` | 8z | -| `0.000000000000000008` | 8a | -| `0.000000000000008` | 8f | -| `0.000000000008` | 8p | -| `0.000000008` | 8n | -| `0.000008` | 8μ | -| `0.008` | 8m | -| `10` | 10 | -| `1080` | 1.08k | -| `18000` | 18k | -| `18888` | 18.9k | -| `188888` | 189k | -| `18888888` | 18.9M | -| `1888888888` | 1.89G | -| `1888888888888` | 1.89T | -| `1888888888888888` | 1.89P | -| `1888888888888888888` | 1.89E | -| `1888888888888888888888` | 1.89Z | -| `1888888888888888888888888` | 1.89Y | -| `1888888888888888888888888888` | 1.89e+27 | - -## Numbers - -For number data, numbers are formatted according to the current locale. - -Formats: `number` - -**Examples:** - -| Data | Displayed | -| ---------- | --------- | -| `10` | 1 | -| `1000` | 1,000 | -| `1000000` | 1,000,000 | - -## Percentage - -For percentage data, format numbers in the chart with a `%` symbol. - -Formats supported: `percent`, `percentHundred` - -**Examples:** - -| Format | Data | Displayed | -| ---------------- | ----- | --------- | -| `percent` | `0.5` | 50% | -| `percent` | `1` | 100% | -| `percent` | `2` | 200% | -| `percentHundred` | `50` | 50% | -| `percentHundred` | `100` | 100% | -| `percentHundred` | `200` | 200% | - -## Duration - -For time durations, format numbers in the chart with a time unit symbol. - -Formats supported: `milliseconds`, `seconds` - -**Examples:** - -| Format | Data | Displayed | -| -------------- | ------ | --------- | -| `milliseconds` | `10` | 10ms | -| `milliseconds` | `500` | 100ms | -| `milliseconds` | `1000` | 1000ms | -| `seconds` | `10` | 10s | -| `seconds` | `500` | 500s | -| `seconds` | `1000` | 1000s | - -## Digital (Metric) - -Converts a number of bytes using metric prefixes. It scales to -use the unit that's the best fit. - -Formats supported: - -- `decimalBytes` -- `kilobytes` -- `megabytes` -- `gigabytes` -- `terabytes` -- `petabytes` - -**Examples:** - -| Format | Data | Displayed | -| -------------- | --------- | --------- | -| `decimalBytes` | `1` | 1B | -| `decimalBytes` | `1000` | 1kB | -| `decimalBytes` | `1000000` | 1MB | -| `kilobytes` | `1` | 1kB | -| `kilobytes` | `1000` | 1MB | -| `kilobytes` | `1000000` | 1GB | -| `megabytes` | `1` | 1MB | -| `megabytes` | `1000` | 1GB | -| `megabytes` | `1000000` | 1TB | - -## Digital (IEC) - -Converts a number of bytes using binary prefixes. It scales to -use the unit that's the best fit. - -Formats supported: - -- `bytes` -- `kibibytes` -- `mebibytes` -- `gibibytes` -- `tebibytes` -- `pebibytes` - -**Examples:** - -| Format | Data | Displayed | -| ----------- | ------------- | --------- | -| `bytes` | `1` | 1B | -| `bytes` | `1024` | 1KiB | -| `bytes` | `1024 * 1024` | 1MiB | -| `kibibytes` | `1` | 1KiB | -| `kibibytes` | `1024` | 1MiB | -| `kibibytes` | `1024 * 1024` | 1GiB | -| `mebibytes` | `1` | 1MiB | -| `mebibytes` | `1024` | 1GiB | -| `mebibytes` | `1024 * 1024` | 1TiB | +This document was moved to [another location](../../../operations/metrics/dashboards/yaml_number_format.md). diff --git a/doc/user/project/operations/alert_management.md b/doc/user/project/operations/alert_management.md index 5dc097c069d..171cf3cd5aa 100644 --- a/doc/user/project/operations/alert_management.md +++ b/doc/user/project/operations/alert_management.md @@ -22,6 +22,30 @@ Enabling any of these methods will allow the Alerts list to display. After confi alerts, visit **{cloud-gear}** **Operations > Alerts** in your project's sidebar to [view the list](#alert-management-list) of alerts. +### Opsgenie integration **(PREMIUM)** + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. + +A new way of monitoring Alerts via a GitLab integration is with +[Opsgenie](https://www.atlassian.com/software/opsgenie). + +NOTE: **Note:** +If you enable the Opsgenie integration, you cannot have other GitLab alert services, +such as [Generic Alerts](../integrations/generic_alerts.md) or +Prometheus alerts, active at the same time. + +To enable Opsgenie integration: + +1. Sign in as a user with Maintainer or Owner [permissions](../../permissions.md). +1. Navigate to **{cloud-gear}** **Operations > Alerts**. +1. In the **Integrations** select box, select Opsgenie. +1. Click the **Active** toggle. +1. In the **API URL**, enter the base URL for your Opsgenie integration, such + as `https://app.opsgenie.com/alert/list`. +1. Click **Save changes**. + +After enabling the integration, navigate to the Alerts list page at **{cloud-gear}** **Operations > Alerts**, and click **View alerts in Opsgenie**. + ### Enable a Generic Alerts endpoint GitLab provides the Generic Alerts endpoint so you can accept alerts from a third-party diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 55fdc5b7a7d..28dfb68234e 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -223,13 +223,18 @@ To remove a project: 1. In the Remove project section, click the **Remove project** button. 1. Confirm the action when asked to. -This action either: +This action: - Removes a project including all associated resources (issues, merge requests etc). -- Since [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/-/issues/32935), on - [GitLab Premium or GitLab.com Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a project for - deletion. The deletion will happen 7 days later by default, but this can be changed in the - [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). +- From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, +group admins can [configure](../../group/index.md#enabling-delayed-project-removal-premium) projects within a group +to be deleted after a delayed period. +When enabled, actual deletion happens after number of days +specified in [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only). + +CAUTION: **Warning:** +The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to +[Immediate deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) in GitLab 13.2. #### Restore a project **(PREMIUM)** diff --git a/haml_lint/linter/documentation_links.rb b/haml_lint/linter/documentation_links.rb new file mode 100644 index 00000000000..f8e0eec5cdc --- /dev/null +++ b/haml_lint/linter/documentation_links.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_relative '../../lib/gitlab/utils/markdown' + +module HamlLint + class Linter + # This class is responsible for detection of help_page_path helpers + # with incorrect links or anchors + class DocumentationLinks < Linter + include ::HamlLint::LinterRegistry + include ::Gitlab::Utils::Markdown + + DOCS_DIRECTORY = File.join(File.expand_path('../..', __dir__), 'doc') + + HELP_PATH_LINK_PATTERN = <<~PATTERN + `(send nil? :help_page_path $...) + PATTERN + + MARKDOWN_HEADER = %r{\A\#{1,6}\s+(?<header>.+)\Z}.freeze + + def visit_script(node) + check(node) + end + + def visit_silent_script(node) + check(node) + end + + def visit_tag(node) + check(node) + end + + private + + def check(node) + match = extract_link_and_anchor(node) + + return if match.empty? + + path_to_file = detect_path_to_file(match[:link]) + + unless File.file?(path_to_file) + record_lint(node, "help_page_path points to the unknown location: #{path_to_file}") + return + end + + unless correct_anchor?(path_to_file, match[:anchor]) + record_lint(node, "anchor (#{match[:anchor]}) is missing in: #{path_to_file}") + end + end + + def extract_link_and_anchor(node) + ast_tree = fetch_ast_tree(node) + + return {} unless ast_tree + + link_match, attributes_match = ::RuboCop::NodePattern.new(HELP_PATH_LINK_PATTERN).match(ast_tree) + + { link: fetch_link(link_match), anchor: fetch_anchor(attributes_match) }.compact + end + + def fetch_ast_tree(node) + # Sometimes links are provided via data attributes in html tag + return node.parsed_attributes.syntax_tree if node.type == :tag + + node.parsed_script.syntax_tree + end + + def detect_path_to_file(link) + path = File.join(DOCS_DIRECTORY, link) + path += '.md' unless path.end_with?('.md') + path + end + + def fetch_link(link_match) + return unless link_match && link_match.str_type? + + link_match.value + end + + def fetch_anchor(attributes_match) + return unless attributes_match + + attributes_match.each_pair do |pkey, pvalue| + break pvalue.value if pkey.value == :anchor + end + end + + def correct_anchor?(path_to_file, anchor) + return true unless anchor + + File.open(path_to_file).any? do |line| + result = line.match(MARKDOWN_HEADER) + + string_to_anchor(result[:header]) == anchor if result + end + end + end + end +end diff --git a/lib/api/entities/approvals.rb b/lib/api/entities/approvals.rb new file mode 100644 index 00000000000..74973772831 --- /dev/null +++ b/lib/api/entities/approvals.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class Approvals < Grape::Entity + expose :user, using: ::API::Entities::UserBasic + end + end +end diff --git a/lib/api/entities/merge_request_approvals.rb b/lib/api/entities/merge_request_approvals.rb index 92afefd47f2..d8a464cfeda 100644 --- a/lib/api/entities/merge_request_approvals.rb +++ b/lib/api/entities/merge_request_approvals.rb @@ -3,6 +3,22 @@ module API module Entities class MergeRequestApprovals < Grape::Entity + expose :user_has_approved do |merge_request, options| + merge_request.has_approved?(options[:current_user]) + end + + expose :user_can_approve do |merge_request, options| + !merge_request.has_approved?(options[:current_user]) && + options[:current_user].can?(:approve_merge_request, merge_request) + end + + expose :approved do |merge_request| + merge_request.approvals.present? + end + + expose :approved_by, using: ::API::Entities::Approvals do |merge_request| + merge_request.approvals + end end end end diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index a2c8e92e560..b362607aed2 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -17,7 +17,7 @@ module Banzai # :toc - String containing Table of Contents data as a `ul` element with # `li` child elements. class TableOfContentsFilter < HTML::Pipeline::Filter - PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze + include Gitlab::Utils::Markdown def call return doc if context[:no_header_anchors] @@ -29,14 +29,7 @@ module Banzai doc.css('h1, h2, h3, h4, h5, h6').each do |node| if header_content = node.children.first - id = node - .text - .strip - .downcase - .gsub(PUNCTUATION_REGEXP, '') # remove punctuation - .tr(' ', '-') # replace spaces with dash - .squeeze('-') # replace multiple dashes with one - .gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs + id = string_to_anchor(node.text) uniq = headers[id] > 0 ? "-#{headers[id]}" : '' headers[id] += 1 diff --git a/lib/gitlab/danger/changelog.rb b/lib/gitlab/danger/changelog.rb index 4589bc435e1..4427c331b8e 100644 --- a/lib/gitlab/danger/changelog.rb +++ b/lib/gitlab/danger/changelog.rb @@ -4,7 +4,6 @@ module Gitlab module Danger module Changelog NO_CHANGELOG_LABELS = [ - 'backstage', # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360. 'tooling', 'tooling::pipelines', 'tooling::workflow', @@ -14,7 +13,7 @@ module Gitlab NO_CHANGELOG_CATEGORIES = %i[docs none].freeze def needed? - categories_need_changelog? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? + categories_need_changelog? && without_no_changelog_label? end def found @@ -34,6 +33,10 @@ module Gitlab def categories_need_changelog? (helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any? end + + def without_no_changelog_label? + (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? + end end end end diff --git a/lib/gitlab/utils/markdown.rb b/lib/gitlab/utils/markdown.rb new file mode 100644 index 00000000000..82c4a0e3b23 --- /dev/null +++ b/lib/gitlab/utils/markdown.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module Markdown + PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze + + def string_to_anchor(string) + string + .strip + .downcase + .gsub(PUNCTUATION_REGEXP, '') # remove punctuation + .tr(' ', '-') # replace spaces with dash + .squeeze('-') # replace multiple dashes with one + .gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1f149873dbb..a89ea118938 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5879,6 +5879,9 @@ msgstr "" msgid "CodeIntelligence|This is the definition" msgstr "" +msgid "CodeNavigation|No references found" +msgstr "" + msgid "CodeOwner|Pattern" msgstr "" @@ -7380,15 +7383,30 @@ msgstr "" msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Silver plan." msgstr "" -msgid "DastProfiles|Manage profiles" +msgid "DastProfiles|Could not create the site profile. Please try again." +msgstr "" + +msgid "DastProfiles|Do you want to discard this site profile?" msgstr "" -msgid "DastProfiles|New Site Profile" +msgid "DastProfiles|Manage profiles" msgstr "" msgid "DastProfiles|New site profile" msgstr "" +msgid "DastProfiles|Please enter a valid URL format, ex: http://www.example.com/home" +msgstr "" + +msgid "DastProfiles|Profile name" +msgstr "" + +msgid "DastProfiles|Save profile" +msgstr "" + +msgid "DastProfiles|Target URL" +msgstr "" + msgid "Data is still calculating..." msgstr "" @@ -7509,6 +7527,9 @@ msgstr "" msgid "Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here." msgstr "" +msgid "Definition" +msgstr "" + msgid "Delayed Project Deletion (%{adjourned_deletion})" msgstr "" @@ -8164,6 +8185,9 @@ msgstr "" msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them." msgstr "" +msgid "Discard" +msgstr "" + msgid "Discard all changes" msgstr "" @@ -19179,6 +19203,9 @@ msgstr "" msgid "Reference:" msgstr "" +msgid "References" +msgstr "" + msgid "Refresh" msgstr "" diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 1e3cdaea3ea..31830c13018 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -291,6 +291,17 @@ function base_config_changed() { curl "${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/changes" | jq '.changes | any(.old_path == "scripts/review_apps/base-config.yaml")' } +function parse_gitaly_image_tag() { + local gitaly_version="${GITALY_VERSION}" + + # returns sha if gitaly_version uses a sha + if [[ ${#gitaly_version} -eq 40 ]]; then + echo "${gitaly_version}" + else + echo "v${gitaly_version}" + fi +} + function deploy() { local namespace="${KUBE_NAMESPACE}" local release="${CI_ENVIRONMENT_SLUG}" @@ -306,6 +317,7 @@ function deploy() { gitlab_webservice_image_repository="${IMAGE_REPOSITORY}/gitlab-webservice-ee" gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-task-runner-ee" gitlab_gitaly_image_repository="${IMAGE_REPOSITORY}/gitaly" + gitaly_image_tag=$(parse_gitaly_image_tag) gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell" gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-ee" @@ -327,7 +339,7 @@ HELM_CMD=$(cat << EOF --set gitlab.migrations.image.repository="${gitlab_migrations_image_repository}" \ --set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \ --set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \ - --set gitlab.gitaly.image.tag="v${GITALY_VERSION}" \ + --set gitlab.gitaly.image.tag="${gitaly_image_tag}" \ --set gitlab.gitlab-shell.image.repository="${gitlab_shell_image_repository}" \ --set gitlab.gitlab-shell.image.tag="v${GITLAB_SHELL_VERSION}" \ --set gitlab.sidekiq.annotations.commit="${CI_COMMIT_SHORT_SHA}" \ diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap index 65c658536a3..161c2bade05 100644 --- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap +++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap @@ -10,57 +10,81 @@ exports[`Code navigation popover component renders popover 1`] = ` style="left: 0px;" /> - <div - class="overflow-auto code-navigation-popover-container" + <gl-tabs-stub + contentclass="gl-py-0" + nav-class="gl-hidden" + theme="indigo" > - <div - class="" + <gl-tab-stub + title="Definition" > - <pre - class="border-0 bg-transparent m-0 code highlight text-wrap" + <div + class="overflow-auto code-navigation-popover-container" > - <span - class="line" - lang="javascript" + <div + class="" > - <span - class="k" + <pre + class="border-0 bg-transparent m-0 code highlight text-wrap" > - function - </span> - <span> - main() { - </span> - </span> - <span - class="line" - lang="javascript" + <span + class="line" + lang="javascript" + > + <span + class="k" + > + function + </span> + <span> + main() { + </span> + </span> + <span + class="line" + lang="javascript" + > + <span> + } + </span> + </span> + </pre> + </div> + </div> + + <div + class="popover-body border-top" + > + <gl-button-stub + category="tertiary" + class="w-100" + data-testid="go-to-definition-btn" + href="http://gitlab.com/test.js" + icon="" + size="medium" + target="_blank" + variant="default" > - <span> - } - </span> - </span> - </pre> - </div> - </div> - - <div - class="popover-body border-top" - > - <gl-button-stub - category="tertiary" - class="w-100" - data-testid="go-to-definition-btn" - href="http://gitlab.com/test.js" - icon="" - size="medium" - target="_blank" - variant="default" + + Go to definition + + </gl-button-stub> + </div> + </gl-tab-stub> + + <gl-tab-stub + class="py-2" + data-testid="references-tab" > + + <p + class="gl-my-4 gl-px-4" + > + + No references found - Go to definition - - </gl-button-stub> - </div> + </p> + </gl-tab-stub> + </gl-tabs-stub> </div> `; diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js index d5a72ab8af8..7b323cfab72 100644 --- a/spec/frontend/code_navigation/components/popover_spec.js +++ b/spec/frontend/code_navigation/components/popover_spec.js @@ -40,6 +40,17 @@ const MOCK_DOCS_DATA = Object.freeze({ definition_path: 'test.js#L20', }); +const MOCK_DATA_WITH_REFERENCES = Object.freeze({ + hover: [ + { + language: null, + value: 'console.log', + }, + ], + references: [{ path: 'index.js' }, { path: 'app.js' }], + definition_path: 'test.js#L20', +}); + let wrapper; function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }) { @@ -64,6 +75,16 @@ describe('Code navigation popover component', () => { expect(wrapper.element).toMatchSnapshot(); }); + it('srender references tab with empty text when no references exist', () => { + factory({ + position: { x: 0, y: 0, height: 0 }, + data: MOCK_CODE_DATA, + definitionPathPrefix: DEFINITION_PATH_PREFIX, + }); + + expect(wrapper.find('[data-testid="references-tab"]').text()).toContain('No references found'); + }); + it('renders link with hash to current file', () => { factory({ position: { x: 0, y: 0, height: 0 }, @@ -75,6 +96,17 @@ describe('Code navigation popover component', () => { expect(wrapper.find('[data-testid="go-to-definition-btn"]').attributes('href')).toBe('#L20'); }); + it('renders list of references', () => { + factory({ + position: { x: 0, y: 0, height: 0 }, + data: MOCK_DATA_WITH_REFERENCES, + definitionPathPrefix: DEFINITION_PATH_PREFIX, + }); + + expect(wrapper.find('[data-testid="references-tab"]').exists()).toBe(true); + expect(wrapper.findAll('[data-testid="reference-link"]').length).toBe(2); + }); + describe('code output', () => { it('renders code output', () => { factory({ diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index 7915544814a..3d27b9c3051 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -88,17 +88,17 @@ describe('IntegrationForm', () => { expect(findJiraTriggerFields().exists()).toBe(true); }); - describe('featureFlag jiraIntegration is false', () => { + describe('featureFlag jiraIssuesIntegration is false', () => { it('does not render JiraIssuesFields', () => { - createComponent({ type: 'jira' }, { jiraIntegration: false }); + createComponent({ type: 'jira' }, { jiraIssuesIntegration: false }); expect(findJiraIssuesFields().exists()).toBe(false); }); }); - describe('featureFlag jiraIntegration is true', () => { + describe('featureFlag jiraIssuesIntegration is true', () => { it('renders JiraIssuesFields', () => { - createComponent({ type: 'jira' }, { jiraIntegration: true }); + createComponent({ type: 'jira' }, { jiraIssuesIntegration: true }); expect(findJiraIssuesFields().exists()).toBe(true); }); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 85e680fe216..e769580b587 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -595,6 +595,14 @@ describe('URL utility', () => { ); }); + it('handles arrays properly when railsArraySyntax=true', () => { + const url = 'https://gitlab.com/test'; + + expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true)).toEqual( + 'https://gitlab.com/test?labels%5B%5D=foo&labels%5B%5D=bar', + ); + }); + it('removes all existing URL params and sets a new param when cleanParams=true', () => { const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project'; diff --git a/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap b/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap new file mode 100644 index 00000000000..4d5b6c56a34 --- /dev/null +++ b/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap @@ -0,0 +1,126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Project remove modal initialized matches the snapshot 1`] = ` +<form + action="some/path" + method="post" +> + <input + name="_method" + type="hidden" + value="delete" + /> + + <input + name="authenticity_token" + type="hidden" + /> + + <b-button-stub + class="[object Object]" + event="click" + role="button" + routertag="a" + size="md" + tabindex="0" + tag="button" + type="button" + variant="danger" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + Remove project + </span> + </b-button-stub> + + <b-modal-stub + canceltitle="Cancel" + cancelvariant="secondary" + footerclass="bg-gray-light gl-p-5" + headerclosecontent="×" + headercloselabel="Close" + id="remove-project-modal" + ignoreenforcefocusselector="" + lazy="true" + modalclass="gl-modal," + oktitle="OK" + okvariant="danger" + size="sm" + title="" + titletag="h4" + > + + <div> + <p + class="gl-text-red-500 gl-font-weight-bold" + > + This can lead to data loss. + </p> + + <p + class="gl-mb-0" + > + This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention. + </p> + + <p> + <gl-sprintf-stub + message="Please type %{phrase_code} to proceed or close this modal to cancel." + /> + </p> + + <gl-form-input-stub + id="confirm_name_input" + name="confirm_name_input" + type="text" + /> + </div> + + <template /> + + <template> + Confirmation required + </template> + + <template /> + + <template /> + + <template /> + + <template> + <div + class="gl-w-full gl-display-flex gl-just-content-start gl-m-0" + > + <b-button-stub + class="[object Object]" + disabled="true" + event="click" + routertag="a" + size="md" + tag="button" + type="button" + variant="danger" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Confirm + + </span> + </b-button-stub> + </div> + </template> + </b-modal-stub> +</form> +`; diff --git a/spec/frontend/projects/components/remove_modal_spec.js b/spec/frontend/projects/components/remove_modal_spec.js new file mode 100644 index 00000000000..339aee65b99 --- /dev/null +++ b/spec/frontend/projects/components/remove_modal_spec.js @@ -0,0 +1,62 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlModal } from '@gitlab/ui'; +import ProjectRemoveModal from '~/projects/components/remove_modal.vue'; + +describe('Project remove modal', () => { + let wrapper; + + const findFormElement = () => wrapper.find('form').element; + const findConfirmButton = () => wrapper.find(GlModal).find(GlButton); + + const defaultProps = { + formPath: 'some/path', + confirmPhrase: 'foo', + warningMessage: 'This can lead to data loss.', + }; + + const createComponent = (data = {}) => { + wrapper = shallowMount(ProjectRemoveModal, { + propsData: defaultProps, + data: () => data, + stubs: { + GlButton, + GlModal, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('initialized', () => { + beforeEach(() => { + createComponent(); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('user input matches the confirmPhrase', () => { + beforeEach(() => { + createComponent({ userInput: defaultProps.confirmPhrase }); + }); + + it('the confirm button is not dislabled', () => { + expect(findConfirmButton().attributes('disabled')).toBe(undefined); + }); + + describe('and when the confirmation button is clicked', () => { + beforeEach(() => { + findConfirmButton().vm.$emit('click'); + }); + + it('submits the form element', () => { + expect(findFormElement().submit).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js index 2253db7cbd0..0007aed5c4d 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js @@ -2,8 +2,9 @@ import { buildUneditableOpenTokens, buildUneditableCloseToken, buildUneditableCloseTokens, - buildUneditableInlineTokens, buildUneditableTokens, + buildUneditableInlineTokens, + buildUneditableHtmlAsTextTokens, } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; import { @@ -12,6 +13,7 @@ import { uneditableOpenTokens, uneditableCloseToken, uneditableCloseTokens, + uneditableBlockTokens, uneditableInlineTokens, uneditableTokens, } from './mock_data'; @@ -41,6 +43,15 @@ describe('Build Uneditable Token renderer helper', () => { }); }); + describe('buildUneditableTokens', () => { + it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { + const result = buildUneditableTokens(originToken); + + expect(result).toHaveLength(3); + expect(result).toStrictEqual(uneditableTokens); + }); + }); + describe('buildUneditableInlineTokens', () => { it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => { const result = buildUneditableInlineTokens(originInlineToken); @@ -50,12 +61,20 @@ describe('Build Uneditable Token renderer helper', () => { }); }); - describe('buildUneditableTokens', () => { - it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { - const result = buildUneditableTokens(originToken); + describe('buildUneditableHtmlAsTextTokens', () => { + it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => { + const htmlBlockNode = { + type: 'htmlBlock', + literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>', + }; + const result = buildUneditableHtmlAsTextTokens(htmlBlockNode); + const { type, content } = result[1]; + + expect(type).toBe('text'); + expect(content).not.toMatch(/ data-tomark-pass /); expect(result).toHaveLength(3); - expect(result).toStrictEqual(uneditableTokens); + expect(result).toStrictEqual(uneditableBlockTokens); }); }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js index 0c010a20d98..433f41774b4 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js @@ -12,7 +12,7 @@ export const normalTextNode = buildMockTextNode('This is just normal text.'); // Token spec helpers -const buildUneditableOpenToken = type => { +const buildMockUneditableOpenToken = type => { return { type: 'openTag', tagName: type, @@ -23,7 +23,7 @@ const buildUneditableOpenToken = type => { }; }; -const buildUneditableCloseToken = type => { +const buildMockUneditableCloseToken = type => { return { type: 'closeTag', tagName: type }; }; @@ -31,8 +31,8 @@ export const originToken = { type: 'text', content: '{:.no_toc .hidden-md .hidden-lg}', }; -export const uneditableCloseToken = buildUneditableCloseToken('div'); -export const uneditableOpenTokens = [buildUneditableOpenToken('div'), originToken]; +export const uneditableCloseToken = buildMockUneditableCloseToken('div'); +export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken]; export const uneditableCloseTokens = [originToken, uneditableCloseToken]; export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; @@ -41,7 +41,17 @@ export const originInlineToken = { content: '<i>Inline</i> content', }; export const uneditableInlineTokens = [ - buildUneditableOpenToken('span'), + buildMockUneditableOpenToken('a'), originInlineToken, - buildUneditableCloseToken('span'), + buildMockUneditableCloseToken('a'), +]; + +export const uneditableBlockTokens = [ + buildMockUneditableOpenToken('div'), + { + type: 'text', + tagName: null, + content: '<div><h1>Some header</h1><p>Some paragraph</p></div>', + }, + buildMockUneditableCloseToken('div'), ]; diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js new file mode 100644 index 00000000000..a6c712eeb31 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js @@ -0,0 +1,38 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block'; +import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; + +import { normalTextNode } from './mock_data'; + +const htmlBlockNode = { + firstChild: null, + literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>', + type: 'htmlBlock', +}; + +describe('Render HTML renderer', () => { + describe('canRender', () => { + it('should return true when the argument is an html block', () => { + expect(renderer.canRender(htmlBlockNode)).toBe(true); + }); + + it('should return false when the argument is not an html block', () => { + expect(renderer.canRender(normalTextNode)).toBe(false); + }); + }); + + describe('render', () => { + const htmlBlockNodeToMark = { + firstChild: null, + literal: '<div data-to-mark ></div>', + type: 'htmlBlock', + }; + + it.each` + node + ${htmlBlockNode} + ${htmlBlockNodeToMark} + `('should return uneditable tokens wrapping the $node as a token', ({ node }) => { + expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node)); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js deleted file mode 100644 index c863b86ebf6..00000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html'; -import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; - -import { normalTextNode } from './mock_data'; - -const htmlLiteral = '<div><h1>Heading</h1><p>Paragraph.</p></div>'; -const htmlBlockNode = { - firstChild: null, - literal: htmlLiteral, - type: 'htmlBlock', -}; - -describe('Render HTML renderer', () => { - describe('canRender', () => { - it('should return true when the argument is an html block', () => { - expect(renderer.canRender(htmlBlockNode)).toBe(true); - }); - - it('should return false when the argument is not an html block', () => { - expect(renderer.canRender(normalTextNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should return uneditable tokens wrapping the origin token', () => { - const origin = jest.fn(); - const context = { origin }; - - expect(renderer.render(htmlBlockNode, context)).toStrictEqual( - buildUneditableTokens(origin()), - ); - }); - }); -}); diff --git a/spec/haml_lint/linter/documentation_links_spec.rb b/spec/haml_lint/linter/documentation_links_spec.rb new file mode 100644 index 00000000000..68de8317b82 --- /dev/null +++ b/spec/haml_lint/linter/documentation_links_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'haml_lint' +require 'haml_lint/spec' +require Rails.root.join('haml_lint/linter/documentation_links') + +RSpec.describe HamlLint::Linter::DocumentationLinks do + include_context 'linter' + + context 'when link_to points to the existing file path' do + let(:haml) { "= link_to 'Description', help_page_path('README.md')" } + + it { is_expected.not_to report_lint } + end + + context 'when link_to points to the existing file with valid anchor' do + let(:haml) { "= link_to 'Description', help_page_path('README.md', anchor: 'overview'), target: '_blank'" } + + it { is_expected.not_to report_lint } + end + + context 'when link_to points to the existing file path without .md extension' do + let(:haml) { "= link_to 'Description', help_page_path('README')" } + + it { is_expected.not_to report_lint } + end + + context 'when anchor is not correct' do + let(:haml) { "= link_to 'Description', help_page_path('README.md', anchor: 'wrong')" } + + it { is_expected.to report_lint } + + context 'when help_page_path has multiple options' do + let(:haml) { "= link_to 'Description', help_page_path('README.md', key: :value, anchor: 'wrong')" } + + it { is_expected.to report_lint } + end + end + + context 'when file path is wrong' do + let(:haml) { "= link_to 'Description', help_page_path('wrong.md'), target: '_blank'" } + + it { is_expected.to report_lint } + end + + context 'when link with wrong file path is assigned to a variable' do + let(:haml) { "- my_link = link_to 'Description', help_page_path('wrong.md')" } + + it { is_expected.to report_lint } + end + + context 'when it is a broken code' do + let(:haml) { "= I am broken! ]]]]" } + + it { is_expected.not_to report_lint } + end + + context 'when anchor belongs to a different element' do + let(:haml) { "= link_to 'Description', help_page_path('README.md'), target: (anchor: 'blank')" } + + it { is_expected.not_to report_lint } + end + + context 'when a simple help_page_path' do + let(:haml) { "- url = help_page_path('wrong.md')" } + + it { is_expected.to report_lint } + end + + context 'when link is not a string' do + let(:haml) { "- url = help_page_path(help_url)" } + + it { is_expected.not_to report_lint } + end + + context 'when link is a part of the tag' do + let(:haml) { ".data-form{ data: { url: help_page_path('wrong.md') } }" } + + it { is_expected.to report_lint } + end +end diff --git a/spec/lib/api/entities/merge_request_approvals_spec.rb b/spec/lib/api/entities/merge_request_approvals_spec.rb new file mode 100644 index 00000000000..cbbb037100a --- /dev/null +++ b/spec/lib/api/entities/merge_request_approvals_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::MergeRequestApprovals do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + + subject { described_class.new(merge_request, current_user: user).as_json } + + before do + merge_request.project.add_developer(user) + end + + it 'serializes an approved merge request' do + create(:approval, merge_request: merge_request, user: user) + + is_expected.to eq({ + user_has_approved: true, + user_can_approve: false, + approved: true, + approved_by: [{ + user: API::Entities::UserBasic.new(user).as_json + }] + }) + end + + it 'serializes a merge request that is not approved' do + is_expected.to eq({ + user_has_approved: false, + user_can_approve: true, + approved: false, + approved_by: [] + }) + end +end diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb index 3e9990a45fe..f5954cd8c1e 100644 --- a/spec/lib/gitlab/danger/changelog_spec.rb +++ b/spec/lib/gitlab/danger/changelog_spec.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true require 'fast_spec_helper' -require 'rspec-parameterized' require_relative 'danger_spec_helper' require 'gitlab/danger/changelog' RSpec.describe Gitlab::Danger::Changelog do - using RSpec::Parameterized::TableSyntax include DangerSpecHelper let(:added_files) { nil } @@ -26,34 +24,36 @@ RSpec.describe Gitlab::Danger::Changelog do subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) } describe '#needed?' do - subject { changelog.needed? } + let(:category_with_changelog) { :backend } + let(:label_with_changelog) { 'frontend' } + let(:category_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first } + let(:label_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_LABELS.first } - where(:categories, :labels) do - { backend: nil } | %w[backend backstage] - { frontend: nil, docs: nil } | ['ci-build'] - { engineering_productivity: nil, none: nil } | ['meta'] - end + subject { changelog.needed? } - with_them do - let(:changes_by_category) { categories } - let(:mr_labels) { labels } + context 'when MR contains only categories requiring no changelog' do + let(:changes_by_category) { { category_without_changelog => nil } } + let(:mr_labels) { [] } - it "is falsy when categories and labels require no changelog" do + it 'is falsey' do is_expected.to be_falsy end end - where(:categories, :labels) do - { frontend: nil, docs: nil } | ['database::review pending', 'feature'] - { backend: nil } | ['backend', 'technical debt'] - { engineering_productivity: nil, none: nil } | ['frontend'] + context 'when MR contains a label that require no changelog' do + let(:changes_by_category) { { category_with_changelog => nil } } + let(:mr_labels) { [label_with_changelog, label_without_changelog] } + + it 'is falsey' do + is_expected.to be_falsy + end end - with_them do - let(:changes_by_category) { categories } - let(:mr_labels) { labels } + context 'when MR contains a category that require changelog and a category that require no changelog' do + let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } } + let(:mr_labels) { [] } - it "is truthy when categories and labels require a changelog" do + it 'is truthy' do is_expected.to be_truthy end end diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb new file mode 100644 index 00000000000..001ff5bc487 --- /dev/null +++ b/spec/lib/gitlab/utils/markdown_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Utils::Markdown do + let(:klass) do + Class.new do + include Gitlab::Utils::Markdown + end + end + + subject(:object) { klass.new } + + describe '#string_to_anchor' do + subject { object.string_to_anchor(string) } + + let(:string) { 'My Header' } + + it 'converts string to anchor' do + is_expected.to eq 'my-header' + end + + context 'when string has punctuation' do + let(:string) { 'My, Header!' } + + it 'removes punctuation' do + is_expected.to eq 'my-header' + end + end + + context 'when string starts and ends with spaces' do + let(:string) { ' My Header ' } + + it 'removes extra spaces' do + is_expected.to eq 'my-header' + end + end + + context 'when string has multiple spaces and dashes in the middle' do + let(:string) { 'My - - - Header' } + + it 'removes consecutive dashes' do + is_expected.to eq 'my-header' + end + end + + context 'when string contains only digits' do + let(:string) { '123' } + + it 'adds anchor prefix' do + is_expected.to eq 'anchor-123' + end + end + + context 'when string is empty' do + let(:string) { '' } + + it 'returns an empty string' do + is_expected.to eq '' + end + end + end +end diff --git a/spec/models/concerns/approvable_base_spec.rb b/spec/models/concerns/approvable_base_spec.rb new file mode 100644 index 00000000000..e4aded1b8d0 --- /dev/null +++ b/spec/models/concerns/approvable_base_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ApprovableBase do + describe '#has_approved?' do + let(:merge_request) { create(:merge_request) } + let(:user) { create(:user) } + + subject { merge_request.has_approved?(user) } + + context 'when a user has not approved' do + it 'returns false' do + is_expected.to be_falsy + end + end + + context 'when a user has approved' do + let!(:approval) { create(:approval, merge_request: merge_request, user: user) } + + it 'returns false' do + is_expected.to be_truthy + end + end + + context 'when a user is nil' do + let(:user) { nil } + + it 'returns false' do + is_expected.to be_falsy + end + end + end +end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index e7184d23767..f1e581efd44 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -613,4 +613,22 @@ RSpec.describe MergeRequestPresenter do end end end + + describe '#api_approvals_path' do + subject { described_class.new(resource, current_user: user).api_approvals_path } + + it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/approvals")) } + end + + describe '#api_approve_path' do + subject { described_class.new(resource, current_user: user).api_approve_path } + + it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/approve")) } + end + + describe '#api_unapprove_path' do + subject { described_class.new(resource, current_user: user).api_unapprove_path } + + it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/unapprove")) } + end end diff --git a/spec/requests/api/ci/runner_spec.rb b/spec/requests/api/ci/runner_spec.rb index 8106b7195c8..c8718309bf2 100644 --- a/spec/requests/api/ci/runner_spec.rb +++ b/spec/requests/api/ci/runner_spec.rb @@ -1823,13 +1823,36 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(json_response['ProcessLsif']).to be_truthy end + it 'adds ProcessLsifReferences header' do + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsifReferences']).to be_truthy + end + context 'code_navigation feature flag is disabled' do - it 'does not add ProcessLsif header' do + it 'responds with a forbidden error' do stub_feature_flags(code_navigation: false) + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['ProcessLsif']).to be_falsy + expect(json_response['ProcessLsifReferences']).to be_falsy + end + end + end + context 'code_navigation_references feature flag is disabled' do + it 'sets ProcessLsifReferences header to false' do + stub_feature_flags(code_navigation_references: false) authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - expect(response).to have_gitlab_http_status(:forbidden) + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsif']).to be_truthy + expect(json_response['ProcessLsifReferences']).to be_falsy + end end end end |