diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
commit | e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch) | |
tree | 2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /app/assets/javascripts/vue_shared | |
parent | ffda4e7bcac36987f936b4ba515995a6698698f0 (diff) |
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared')
74 files changed, 967 insertions, 1270 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue index 4ec301b946b..2d3815439a6 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue @@ -22,15 +22,15 @@ const DATA_REFETCH_DELAY = 250; export default { i18n: { FETCH_USERS_ERROR: s__( - 'AlertManagement|There was an error while updating the assignee(s) list. Please try again.', + 'AlertManagement|There was an error while updating the assignees list. Please try again.', ), UPDATE_ALERT_ASSIGNEES_ERROR: s__( - 'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.', + 'AlertManagement|There was an error while updating the assignees of the alert. Please try again.', ), UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR: s__( 'AlertManagement|This assignee cannot be assigned to this alert.', ), - ASSIGNEES_BLOCK: s__('AlertManagement|Alert assignee(s): %{assignees}'), + ASSIGNEES_BLOCK: s__('AlertManagement|Alert assignees: %{assignees}'), }, components: { GlIcon, diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index c3f3226c46e..1d6dbef799a 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -45,8 +45,12 @@ export default { :category="category" :toggle-text="toggleText" data-qa-selector="action_dropdown" + fluid-width + block + @shown="$emit('shown')" + @hidden="$emit('hidden')" > - <gl-disclosure-dropdown-group> + <gl-disclosure-dropdown-group class="edit-dropdown-group-width"> <gl-disclosure-dropdown-item v-for="action in actions" :key="action.key" @@ -65,5 +69,6 @@ export default { </template> </gl-disclosure-dropdown-item> </gl-disclosure-dropdown-group> + <slot></slot> </gl-disclosure-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 8f1f7ba0ad8..59f03b41144 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -184,6 +184,7 @@ export default { class="gl-mr-3 gl-my-2" :class="awardList.classes" :title="awardList.title" + :data-emoji-name="awardList.name" data-testid="award-button" @click="handleAward(awardList.name)" > @@ -209,7 +210,6 @@ export default { @hidden="setIsMenuOpen(false)" > <template #button-content> - <span class="gl-sr-only">{{ __('Add reaction') }}</span> <span class="reaction-control-icon reaction-control-icon-neutral"> <gl-icon name="slight-smile" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue index 3a3929fba9b..3e24a35ea39 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue @@ -12,8 +12,22 @@ export default { SafeHtml, }, mixins: [ViewerMixin], + data() { + return { + isLoading: true, + }; + }, mounted() { - handleBlobRichViewer(this.$refs.content, this.type); + window.requestIdleCallback(async () => { + /** + * Rendering Markdown usually takes long due to the amount of HTML being parsed. + * This ensures that content is loaded only when the browser goes into idle. + * More details here: https://gitlab.com/gitlab-org/gitlab/-/issues/331448 + * */ + this.isLoading = false; + await this.$nextTick(); + handleBlobRichViewer(this.$refs.content, this.type); + }); }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'], @@ -22,6 +36,7 @@ export default { </script> <template> <markdown-field-view + v-if="!isLoading" ref="content" v-safe-html:[$options.safeHtmlConfig]="richViewer || content" /> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 0d7547d88a1..6670b931416 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -36,6 +36,15 @@ export default { status: { type: Object, required: true, + validator(status) { + const { group, icon } = status; + return ( + typeof group === 'string' && + group.length && + typeof icon === 'string' && + icon.startsWith('status_') + ); + }, }, size: { type: Number, @@ -69,7 +78,7 @@ export default { computed: { wrapperStyleClasses() { const status = this.status.group; - return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`; + return `ci-status-icon ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`; }, icon() { return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon; @@ -84,7 +93,6 @@ export default { { interactive: isInteractive, active: isActive, borderless: isBorderless }, ]" :style="{ height: `${size}px`, width: `${size}px` }" - data-testid="ci-icon-wrapper" > <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue index 352d03befc3..d98858da95f 100644 --- a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue +++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue @@ -1,6 +1,6 @@ <script> +import { escape } from 'lodash'; import SafeHtml from '~/vue_shared/directives/safe_html'; - import languageLoader from '~/content_editor/services/highlight_js_language_loader'; import CodeBlock from './code_block.vue'; @@ -39,7 +39,7 @@ export default { return this.hljs.highlight(this.code, { language: this.language }).value; } - return this.code; + return escape(this.code); }, }, async mounted() { diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js index 90d55d0f93f..c6b9e61b85f 100644 --- a/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js +++ b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js @@ -6,7 +6,5 @@ export const CONFIRM_DANGER_MODAL_BUTTON = __('Confirm'); export const CONFIRM_DANGER_WARNING = __( 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.', ); -export const CONFIRM_DANGER_PHRASE_TEXT = __( - 'Please type %{phrase_code} to proceed or close this modal to cancel.', -); +export const CONFIRM_DANGER_PHRASE_TEXT = __('Please type %{phrase_code} to proceed.'); export const CONFIRM_DANGER_MODAL_CANCEL = __('Cancel'); diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue index 1a3220d8db9..970c24c6e87 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue @@ -75,10 +75,13 @@ export default { computed: { selected: { set(value) { - this.$emit('input', value); this.selectedValue = value; this.selectedText = value === null ? null : this.items.find((item) => item.value === value).text; + this.$emit('input', { + value: this.selectedValue, + text: this.selectedText, + }); }, get() { return this.selectedValue; @@ -161,7 +164,7 @@ export default { }, onReset() { this.selected = null; - this.$emit('input', null); + this.$emit('input', {}); }, onBottomReached() { this.fetchEntities(this.page + 1); diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue index ff137d764ee..71e3bf4ff63 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue @@ -121,6 +121,7 @@ export default { :default-toggle-text="$options.i18n.toggleText" :fetch-items="fetchGroups" :fetch-initial-selection-text="fetchGroupName" + v-on="$listeners" > <template #error> <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{ diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue index 7af3819f2a5..13a825a68f6 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue @@ -166,6 +166,7 @@ export default { :fetch-initial-selection-text="fetchProjectName" :block="block" clearable + v-on="$listeners" > <template v-if="hasHtmlLabel" #label> <span v-safe-html="label"></span> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 28baabbdb81..1adda905006 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -114,11 +114,7 @@ export default { </script> <template> - <header - class="page-content-header gl-md-display-flex gl-min-h-7" - data-qa-selector="pipeline_header" - data-testid="ci-header-content" - > + <header class="page-content-header gl-md-display-flex gl-min-h-7" data-testid="ci-header-content"> <section class="header-main-content gl-mr-3"> <ci-badge-link class="gl-mr-3" :status="status" /> diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js b/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js index ad89b78b521..b447822b1e0 100644 --- a/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js +++ b/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; export const initListboxInputs = () => { const els = [...document.querySelectorAll('.js-listbox-input')]; @@ -30,6 +31,8 @@ export const initListboxInputs = () => { name, defaultToggleText, selected: this.selected, + block: parseBoolean(el.dataset.block), + fluidWidth: parseBoolean(el.dataset.fluidWidth), items, }, attrs: { diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue index 0f8ff5291a4..a59a7494472 100644 --- a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue +++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue @@ -47,6 +47,21 @@ export default { required: false, default: false, }, + fluidWidth: { + type: GlCollapsibleListbox.props.fluidWidth.type, + required: false, + default: GlCollapsibleListbox.props.fluidWidth.default, + }, + placement: { + type: GlCollapsibleListbox.props.placement.type, + required: false, + default: GlCollapsibleListbox.props.placement.default, + }, + block: { + type: GlCollapsibleListbox.props.block.type, + required: false, + default: GlCollapsibleListbox.props.block.default, + }, }, data() { return { @@ -123,6 +138,9 @@ export default { :searchable="isSearchable" :no-results-text="$options.i18n.noResultsText" :disabled="disabled" + :fluid-width="fluidWidth" + :placement="placement" + :block="block" @search="search" @select="$emit($options.model.event, $event)" /> diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue index f51ec715678..a570abae9d3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue @@ -12,7 +12,8 @@ export default { }, defaultCommitMessage: { type: String, - required: true, + required: false, + default: null, }, batchSuggestionsCount: { type: Number, diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue index 186f5619b87..966a5556d24 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue @@ -1,7 +1,6 @@ <script> import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import { updateText } from '~/lib/utils/text_markdown'; import savedRepliesQuery from './saved_replies.query.graphql'; export default { @@ -54,20 +53,8 @@ export default { }, onSelect(id) { const savedReply = this.savedReplies.find((r) => r.id === id); - const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); - - if (savedReply && textArea) { - updateText({ - textArea, - tag: savedReply.content, - cursorOffset: 0, - wrap: false, - }); - - // Wait for text to be added into textarea - requestAnimationFrame(() => { - textArea.focus(); - }); + if (savedReply) { + this.$emit('select', savedReply.content); } }, }, @@ -81,13 +68,14 @@ export default { :items="filteredSavedReplies" :toggle-text="__('Insert comment template')" text-sr-only + no-caret toggle-class="js-comment-template-toggle" icon="comment-lines" category="tertiary" placement="right" searchable size="small" - class="comment-template-dropdown" + class="comment-template-dropdown gl-mr-3" positioning-strategy="fixed" :searching="$apollo.queries.savedReplies.loading" @shown="fetchCommentTemplates" @@ -104,7 +92,7 @@ export default { </template> <template #footer> <div - class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3" + class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-justify-content-center gl-p-2" > <gl-button :href="newCommentTemplatePath" @@ -130,4 +118,8 @@ export default { .comment-template-dropdown .gl-new-dropdown-item-check-icon { display: none; } + +.comment-template-dropdown input { + border-radius: 0; +} </style> diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue index 645975ca565..2426a917a53 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue @@ -1,10 +1,16 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlPopover, GlLink } from '@gitlab/ui'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import { __ } from '~/locale'; +import RICH_TEXT_EDITOR_ILLUSTRATION from '../../../../images/callouts/rich_text_editor_illustration.svg?url'; +import { counter } from './utils'; export default { components: { GlButton, + GlLink, + GlPopover, + UserCalloutDismisser, }, props: { value: { @@ -12,21 +18,102 @@ export default { required: true, }, }, + data() { + return { + counter: counter(), + }; + }, computed: { + showPromoPopover() { + return this.markdownEditorSelected && this.counter === 0; + }, markdownEditorSelected() { return this.value === 'markdown'; }, text() { - return this.markdownEditorSelected ? __('Switch to rich text') : __('Switch to Markdown'); + return this.markdownEditorSelected + ? __('Switch to rich text editing') + : __('Switch to plain text editing'); }, }, + methods: { + switchEditorType(insertTemplate = false) { + this.$emit('switch', insertTemplate); + }, + }, + richTextEditorButtonId: 'switch-to-rich-text-editor', + RICH_TEXT_EDITOR_ILLUSTRATION, }; </script> <template> - <gl-button - class="btn btn-default btn-sm gl-button btn-default-tertiary" - data-qa-selector="editing_mode_switcher" - @click="$emit('input')" - >{{ text }}</gl-button - > + <div class="content-editor-switcher gl-display-inline-flex gl-align-items-center"> + <user-callout-dismisser feature-name="rich_text_editor"> + <template #default="{ dismiss, shouldShowCallout }"> + <div> + <gl-popover + :target="$options.richTextEditorButtonId" + :show="Boolean(showPromoPopover && shouldShowCallout)" + show-close-button + :css-classes="['rich-text-promo-popover gl-p-2']" + triggers="manual" + data-testid="rich-text-promo-popover" + @close-button-clicked="dismiss" + > + <img + :src="$options.RICH_TEXT_EDITOR_ILLUSTRATION" + :alt="''" + class="rich-text-promo-popover-illustration" + width="280" + height="130" + /> + <h5 class="gl-mt-3 gl-mb-3">{{ __('Writing just got easier') }}</h5> + <p class="gl-m-0"> + {{ + __( + 'Use the new rich text editor to see your text and tables fully formatted as you type. No need to remember any formatting syntax, or switch between preview and editing modes!', + ) + }} + </p> + <gl-link + class="gl-button btn btn-confirm block gl-mb-2 gl-mt-4" + variant="confirm" + category="primary" + target="_blank" + block + @click=" + switchEditorType(showPromoPopover); + dismiss(); + " + > + {{ __('Try the rich text editor now') }} + </gl-link> + </gl-popover> + <gl-button + :id="$options.richTextEditorButtonId" + class="btn btn-default btn-sm gl-button btn-default-tertiary gl-font-sm! gl-text-secondary! gl-px-4!" + data-qa-selector="editing_mode_switcher" + @click=" + switchEditorType(); + dismiss(); + " + >{{ text }}</gl-button + > + </div> + </template> + </user-callout-dismisser> + </div> </template> +<style> +.rich-text-promo-popover { + box-shadow: 0 0 18px -1.9px rgba(119, 89, 194, 0.16), 0 0 12.9px -1.7px rgba(119, 89, 194, 0.16), + 0 0 9.2px -1.4px rgba(119, 89, 194, 0.16), 0 0 6.4px -1.1px rgba(119, 89, 194, 0.16), + 0 0 4.5px -0.8px rgba(119, 89, 194, 0.16), 0 0 3px -0.6px rgba(119, 89, 194, 0.16), + 0 0 1.8px -0.3px rgba(119, 89, 194, 0.16), 0 0 0.6px rgba(119, 89, 194, 0.16); + z-index: 999; +} + +.rich-text-promo-popover-illustration { + width: calc(100% + 32px); + margin: -32px -16px 0; +} +</style> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 602a83132e4..7c569763a75 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -68,10 +68,10 @@ export default { required: false, default: false, }, - quickActionsDocsPath: { - type: String, + supportsQuickActions: { + type: Boolean, required: false, - default: '', + default: false, }, canAttachFile: { type: Boolean, @@ -355,10 +355,7 @@ export default { <template> <div ref="gl-form" - :class="{ - 'gl-border-none! gl-shadow-none!': removeBorder, - }" - class="js-vue-markdown-field md-area position-relative gfm-form" + class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden" :data-uploads-path="uploadsPath" > <markdown-header @@ -371,13 +368,12 @@ export default { :uploads-path="uploadsPath" :markdown-preview-path="markdownPreviewPath" :drawio-enabled="drawioEnabled" + :supports-quick-actions="supportsQuickActions" data-testid="markdownHeader" :restricted-tool-bar-items="restrictedToolBarItems" - :show-content-editor-switcher="showContentEditorSwitcher" @showPreview="showPreview" @hidePreview="hidePreview" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" - @enableContentEditor="$emit('enableContentEditor')" /> <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> @@ -391,36 +387,31 @@ export default { </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" :show-comment-tool-bar="showCommentToolBar" + :show-content-editor-switcher="showContentEditorSwitcher" + @enableContentEditor="$emit('enableContentEditor')" /> </div> </div> - <template v-if="hasSuggestion"> - <div - v-show="previewMarkdown" - ref="markdown-preview" - class="js-vue-md-preview md-preview-holder gl-px-5" - > - <suggestions - v-if="hasSuggestion" - :note-html="markdownPreview" - :line-type="lineType" - :disabled="true" - :suggestions="suggestions" - :help-page-path="helpPagePath" - /> - </div> - </template> - <template v-else> - <div - v-show="previewMarkdown" - ref="markdown-preview" - v-safe-html:[$options.safeHtmlConfig]="markdownPreview" - class="js-vue-md-preview md md-preview-holder gl-px-5" - ></div> - </template> + <div + v-show="previewMarkdown" + ref="markdown-preview" + class="js-vue-md-preview md-preview-holder gl-px-5" + :class="{ md: !hasSuggestion }" + > + <suggestions + v-if="hasSuggestion" + :note-html="markdownPreview" + :line-type="lineType" + :disabled="true" + :suggestions="suggestions" + :help-page-path="helpPagePath" + /> + <template v-else> + <div v-safe-html:[$options.safeHtmlConfig]="markdownPreview"></div> + </template> + </div> <div v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading" v-safe-html:[$options.safeHtmlConfig]="referencedCommands" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index af0b34f1389..0907e064e01 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -13,13 +13,13 @@ import { import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { getModifierKey } from '~/constants'; import { getSelectedFragment } from '~/lib/utils/common_utils'; -import { s__, __ } from '~/locale'; +import { truncateSha } from '~/lib/utils/text_utility'; +import { s__, __, sprintf } from '~/locale'; import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import { updateText } from '~/lib/utils/text_markdown'; import ToolbarButton from './toolbar_button.vue'; import DrawioToolbarButton from './drawio_toolbar_button.vue'; import CommentTemplatesDropdown from './comment_templates_dropdown.vue'; -import EditorModeSwitcher from './editor_mode_switcher.vue'; export default { components: { @@ -29,7 +29,6 @@ export default { DrawioToolbarButton, CommentTemplatesDropdown, AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'), - EditorModeSwitcher, }, directives: { GlTooltip: GlTooltipDirective, @@ -40,6 +39,7 @@ export default { default: null, }, editorAiActions: { default: () => [] }, + mrGeneratedContent: { default: null }, }, props: { previewMarkdown: { @@ -91,17 +91,19 @@ export default { required: false, default: false, }, - showContentEditorSwitcher: { + supportsQuickActions: { type: Boolean, required: false, default: false, }, }, data() { + const modifierKey = getModifierKey(); return { tag: '> ', suggestPopoverVisible: false, - modifierKey: getModifierKey(), + modifierKey, + shiftKey: modifierKey === '⌘' ? '⇧' : 'Shift+', }; }, computed: { @@ -126,9 +128,6 @@ export default { const expandText = s__('MarkdownEditor|Click to expand'); return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n'); }, - showEditorModeSwitcher() { - return this.showContentEditorSwitcher && !this.previewMarkdown; - }, }, watch: { showSuggestPopover() { @@ -199,17 +198,25 @@ export default { insertIntoTextarea(text) { const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); if (textArea) { - const generatedByText = `${text}\n\n---\n\n_${__('This comment was generated by AI')}_`; updateText({ textArea, - tag: generatedByText, + tag: text, cursorOffset: 0, wrap: false, }); } }, - handleEditorModeChanged() { - this.$emit('enableContentEditor'); + replaceTextarea(text) { + const { description, descriptionForSha } = this.$options.i18n; + const headSha = document.getElementById('merge_request_diff_head_sha').value; + const addendum = headSha + ? sprintf(descriptionForSha, { revision: truncateSha(headSha) }) + : description; + + if (this.mrGeneratedContent) { + this.mrGeneratedContent.setGeneratedContent(`${text}\n\n---\n\n_${addendum}_`); + this.mrGeneratedContent.showWarning(); + } }, switchPreview() { if (this.previewMarkdown) { @@ -218,6 +225,12 @@ export default { this.showMarkdownPreview(); } }, + insertAIAction(text) { + this.insertIntoTextarea(`${text}\n\n---\n\n_${__('This comment was generated by AI')}_`); + }, + insertSavedReply(savedReply) { + this.insertIntoTextarea(savedReply); + }, }, shortcuts: { bold: keysFor(BOLD_TEXT), @@ -228,27 +241,36 @@ export default { outdent: keysFor(OUTDENT_LINE), }, i18n: { - preview: __('Preview'), + comment: __('This comment was generated by AI'), + description: s__('MergeRequest|This description was generated using AI'), + descriptionForSha: s__( + 'MergeRequest|This description was generated for revision %{revision} using AI', + ), hidePreview: __('Continue editing'), + preview: __('Preview'), }, }; </script> <template> - <div class="md-header gl-bg-gray-50 gl-px-2 gl-rounded-base gl-mx-2 gl-mt-2"> - <div - class="gl-display-flex gl-align-items-center gl-flex-wrap" - :class="{ - 'gl-justify-content-end': previewMarkdown, - 'gl-justify-content-space-between': !previewMarkdown, - }" - > + <div class="md-header gl-border-b gl-border-gray-100 gl-px-3"> + <div class="gl-display-flex gl-align-items-center gl-flex-wrap"> <div data-testid="md-header-toolbar" - class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap" - :class="{ 'gl-display-none!': previewMarkdown }" + class="md-header-toolbar gl-display-flex gl-py-3 gl-flex-wrap gl-row-gap-3" > - <template v-if="canSuggest"> + <gl-button + v-if="enablePreview" + data-testid="preview-toggle" + value="preview" + :label="$options.i18n.previewTabTitle" + class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2" + size="small" + category="tertiary" + @click="switchPreview" + >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button + > + <template v-if="!previewMarkdown && canSuggest"> <toolbar-button ref="suggestButton" :tag="mdSuggestion" @@ -289,11 +311,13 @@ export default { </gl-popover> </template> <ai-actions-dropdown - v-if="editorAiActions.length" + v-if="!previewMarkdown && editorAiActions.length" :actions="editorAiActions" - @input="insertIntoTextarea" + @input="insertAIAction" + @replace="replaceTextarea" /> <toolbar-button + v-show="!previewMarkdown" tag="**" :button-title=" /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ @@ -305,6 +329,7 @@ export default { icon="bold" /> <toolbar-button + v-show="!previewMarkdown" tag="_" :button-title=" /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ @@ -317,11 +342,13 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('strikethrough')" + v-show="!previewMarkdown" tag="~~" :button-title=" /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), { + modifierKey, + shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, }) " :shortcuts="$options.shortcuts.strikethrough" @@ -329,14 +356,22 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('quote')" + v-show="!previewMarkdown" :prepend="true" :tag="tag" :button-title="__('Insert a quote')" icon="quote" @click="handleQuote" /> - <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> <toolbar-button + v-show="!previewMarkdown" + tag="`" + tag-block="```" + :button-title="__('Insert code')" + icon="code" + /> + <toolbar-button + v-show="!previewMarkdown" tag="[{text}](url)" tag-select="url" :button-title=" @@ -350,6 +385,7 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('bullet-list')" + v-show="!previewMarkdown" :prepend="true" tag="- " :button-title="__('Add a bullet list')" @@ -357,6 +393,7 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('numbered-list')" + v-show="!previewMarkdown" :prepend="true" tag="1. " :button-title="__('Add a numbered list')" @@ -364,6 +401,7 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('task-list')" + v-show="!previewMarkdown" :prepend="true" tag="- [ ] " :button-title="__('Add a checklist')" @@ -371,6 +409,7 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('indent')" + v-show="!previewMarkdown" class="gl-display-none" :button-title=" /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ @@ -384,6 +423,7 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('outdent')" + v-show="!previewMarkdown" class="gl-display-none" :button-title=" /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ @@ -397,6 +437,7 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('collapsible-section')" + v-show="!previewMarkdown" :tag="mdCollapsibleSection" :prepend="true" tag-select="Click to expand" @@ -405,17 +446,18 @@ export default { /> <toolbar-button v-if="!restrictedToolBarItems.includes('table')" + v-show="!previewMarkdown" :tag="mdTable" :prepend="true" :button-title="__('Add a table')" icon="table" /> <gl-button - v-if="!restrictedToolBarItems.includes('attach-file')" + v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')" v-gl-tooltip :aria-label="__('Attach a file or image')" :title="__('Attach a file or image')" - class="gl-mr-2" + class="gl-mr-3" data-testid="button-attach-file" category="tertiary" icon="paperclip" @@ -423,46 +465,37 @@ export default { @click="handleAttachFile" /> <drawio-toolbar-button - v-if="drawioEnabled" + v-if="!previewMarkdown && drawioEnabled" :uploads-path="uploadsPath" :markdown-preview-path="markdownPreviewPath" /> + <!-- TODO Add icon and trigger functionality from here --> + <toolbar-button + v-if="supportsQuickActions" + v-show="!previewMarkdown" + :prepend="true" + tag="/" + :button-title="__('Add a quick action')" + icon="quick-actions" + /> <comment-templates-dropdown - v-if="newCommentTemplatePath && glFeatures.savedReplies" + v-if="!previewMarkdown && newCommentTemplatePath && glFeatures.savedReplies" :new-comment-template-path="newCommentTemplatePath" + @select="insertSavedReply" /> - </div> - <div class="switch-preview gl-py-2 gl-display-flex gl-align-items-center gl-ml-auto"> - <editor-mode-switcher - v-if="showEditorModeSwitcher" - size="small" - class="gl-mr-2" - value="markdown" - @input="handleEditorModeChanged" - /> - <gl-button - v-if="enablePreview" - data-testid="preview-toggle" - value="preview" - :label="$options.i18n.previewTabTitle" - class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!" - size="small" - category="tertiary" - @click="switchPreview" - >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button - > - <gl-button - v-if="!restrictedToolBarItems.includes('full-screen')" - v-gl-tooltip - :class="{ 'gl-display-none!': previewMarkdown }" - class="js-zen-enter gl-ml-2" - category="tertiary" - icon="maximize" - size="small" - :title="__('Go full screen')" - :prepend="true" - :aria-label="__('Go full screen')" - /> + <div v-if="!previewMarkdown" class="full-screen"> + <gl-button + v-if="!restrictedToolBarItems.includes('full-screen')" + v-gl-tooltip + class="js-zen-enter" + category="tertiary" + icon="maximize" + size="small" + :title="__('Go full screen')" + :prepend="true" + :aria-label="__('Go full screen')" + /> + </div> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index 9fd606d775d..8b8247a5b2c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -5,6 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { updateDraft, clearDraft, getDraft } from '~/lib/utils/autosave'; import { setUrlParams, joinPaths } from '~/lib/utils/url_utility'; import { + EDITING_MODE_KEY, EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR, CLEAR_AUTOSAVE_ENTRY_EVENT, @@ -80,11 +81,6 @@ export default { required: false, default: '', }, - quickActionsDocsPath: { - type: String, - required: false, - default: '', - }, drawioEnabled: { type: Boolean, required: false, @@ -100,6 +96,11 @@ export default { required: false, default: false, }, + codeSuggestionsConfig: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { return { @@ -171,7 +172,7 @@ export default { renderMarkdown(markdown) { const url = setUrlParams( { render_quick_actions: this.supportsQuickActions }, - joinPaths(window.location.origin, gon.relative_url_root, this.renderMarkdownPath), + joinPaths(gon.relative_url_root || window.location.origin, this.renderMarkdownPath), ); return axios.post(url, { text: markdown }).then(({ data }) => data.body); }, @@ -223,14 +224,15 @@ export default { } }, }, + EDITING_MODE_KEY, }; </script> <template> - <div class="md-area gl-px-0! gl-overflow-hidden"> + <div class="gl-px-0!"> <local-storage-sync :value="editingMode" as-string - storage-key="gl-markdown-editor-mode" + :storage-key="$options.EDITING_MODE_KEY" @input="onEditingModeRestored" /> <markdown-field @@ -240,12 +242,16 @@ export default { data-testid="markdown-field" :markdown-preview-path="renderMarkdownPath" :can-attach-file="!disableAttachments" + :can-suggest="codeSuggestionsConfig.canSuggest" + :line="codeSuggestionsConfig.line" + :lines="codeSuggestionsConfig.lines" + :show-suggest-popover="codeSuggestionsConfig.showPopover" :textarea-value="markdown" :uploads-path="uploadsPath" :enable-autocomplete="enableAutocomplete" :autocomplete-data-sources="autocompleteDataSources" :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" + :supports-quick-actions="supportsQuickActions" :show-content-editor-switcher="enableContentEditor" :drawio-enabled="drawioEnabled" :restricted-tool-bar-items="markdownFieldRestrictedToolBarItems" @@ -272,9 +278,10 @@ export default { <content-editor ref="contentEditor" :render-markdown="renderMarkdown" + :markdown-docs-path="markdownDocsPath" :uploads-path="uploadsPath" :markdown="markdown" - :quick-actions-docs-path="quickActionsDocsPath" + :supports-quick-actions="supportsQuickActions" :autofocus="contentEditorAutofocused" :placeholder="formFieldProps.placeholder" :drawio-enabled="drawioEnabled" @@ -282,6 +289,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :editable="!disabled" :disable-attachments="disableAttachments" + :code-suggestions-config="codeSuggestionsConfig" @initialized="setEditorAsAutofocused" @change="updateMarkdownFromContentEditor" @keydown="$emit('keydown', $event)" diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js index 8ff14220eab..0b0867ae84c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js +++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js @@ -1,6 +1,9 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createApolloClient from '~/lib/graphql'; import { queryToObject, objectToQuery } from '~/lib/utils/url_utility'; import { parseBoolean } from '~/lib/utils/common_utils'; + import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '../../constants'; import MarkdownEditor from './markdown_editor.vue'; import eventHub from './eventhub'; @@ -51,8 +54,13 @@ function mountAutosaveClearOnSubmit(autosaveKey) { } } -export function mountMarkdownEditor() { +export function mountMarkdownEditor(options = {}) { const el = document.querySelector('.js-markdown-editor'); + const componentConfiguration = { + provide: { + ...options.provide, + }, + }; if (!el) { return null; @@ -71,6 +79,7 @@ export function mountMarkdownEditor() { const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true); const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true); const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false); + const autofocus = parseBoolean(el.dataset.autofocus ?? true); const hiddenInput = el.querySelector('input[type="hidden"]'); const formFieldName = hiddenInput.getAttribute('name'); const formFieldId = hiddenInput.getAttribute('id'); @@ -86,6 +95,9 @@ export function mountMarkdownEditor() { const setFacade = (props) => Object.assign(facade, props); const autosaveKey = `autosave/${document.location.pathname}/${searchTerm}/description`; + componentConfiguration.apolloProvider = + options.apolloProvider || new VueApollo({ defaultClient: createApolloClient() }); + // eslint-disable-next-line no-new new Vue({ el, @@ -110,10 +122,11 @@ export function mountMarkdownEditor() { autocompleteDataSources: gl.GfmAutoComplete?.dataSources, supportsQuickActions, disableAttachments, - autofocus: true, + autofocus, }, }); }, + ...componentConfiguration, }); mountAutosaveClearOnSubmit(autosaveKey); diff --git a/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js new file mode 100644 index 00000000000..0ba6a44d153 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.stories.js @@ -0,0 +1,89 @@ +import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue'; + +export default { + title: 'vue_shared/non_gfm_markdown', + component: Markdown, + parameters: { + docs: { + description: { + component: ` +This component is designed to render the markdown, which is **not** the GitLab Flavored Markdown. + +It renders the code snippets the same way GitLab Flavored Markdown code snippets are rendered +respecting the user's preferred color scheme and featuring a copy-code button. + +This component can be used to render client-side markdown that doesn't have GitLab-specific markdown elements such as issue links. +`, + }, + }, + }, +}; + +const Template = (args, { argTypes }) => ({ + components: { Markdown }, + props: Object.keys(argTypes), + template: '<markdown v-bind="$props" />', +}); + +const textWithCodeblock = ` +#### Here is the text with the code block. + +\`\`\`javascript +function sayHi(name) { + console.log('Hi ' + name || 'Mark'); +} +\`\`\` + +It *can* have **formatting** as well +`; + +export const OneCodeBlock = Template.bind({}); +OneCodeBlock.args = { markdown: textWithCodeblock }; + +const textWithMultipleCodeBlocks = ` +#### Here is the text with the code block. + +\`\`\`javascript +function sayHi(name) { + console.log('Hi ' + name || 'Mark'); +} +\`\`\` + +Note that the copy buttons are appearing independently + +\`\`\`yaml +stages: + - build + - test + - deploy +\`\`\` +`; + +export const MultipleCodeBlocks = Template.bind({}); +MultipleCodeBlocks.args = { markdown: textWithMultipleCodeBlocks }; + +const textUndefinedLanguage = ` +#### Here is the code block with no language provided. + +\`\`\` +function sayHi(name) { + console.log('Hi ' + name || 'Mark'); +} +\`\`\` +`; + +export const UndefinedLanguage = Template.bind({}); +UndefinedLanguage.args = { markdown: textUndefinedLanguage }; + +const textCodeOneLiner = ` +#### Here is the text with the one-liner code block. + +Note that copy button rendering is ok. + +\`\`\`javascript +const foo = 'bar'; +\`\`\` +`; + +export const CodeOneLiner = Template.bind({}); +CodeOneLiner.args = { markdown: textCodeOneLiner }; diff --git a/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue new file mode 100644 index 00000000000..814e59681d0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/non_gfm_markdown.vue @@ -0,0 +1,120 @@ +<script> +/* +This component is designed to render the markdown, which is **not** the GitLab Flavored Markdown. + +It renders the code snippets the same way GitLab Flavored Markdown code snippets are rendered +respecting the user's preferred color scheme and featuring a copy-code button. + +This component can be used to render client-side markdown that doesn't have GitLab-specific markdown elements such as issue links. +*/ +import { marked } from 'marked'; +import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { sanitize } from '~/lib/dompurify'; +import { markdownConfig } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; + +export default { + components: { + CodeBlockHighlighted, + ModalCopyButton, + }, + directives: { + SafeHtml, + }, + props: { + markdown: { + type: String, + required: true, + }, + }, + data() { + return { + hoverMap: {}, + }; + }, + computed: { + markdownBlocks() { + // we use lexer https://marked.js.org/using_pro#lexer + // to get an array of tokens that marked npm module uses. + // We will use these tokens to override rendering of some of them + // with our vue components + const tokens = marked.lexer(this.markdown); + + // since we only want to differentiate between code and non-code blocks + // we want non-code blocks merged together so that the markdown parser could render + // them according to the markdown rules. + // This way we introduce minimum extra wrapper mark-up + const flattenedTokens = []; + + for (const token of tokens) { + const lastFlattenedToken = flattenedTokens[flattenedTokens.length - 1]; + if (token.type === 'code') { + flattenedTokens.push(token); + } else if (lastFlattenedToken?.type === 'markdown') { + lastFlattenedToken.raw += token.raw; + } else { + flattenedTokens.push({ type: 'markdown', raw: token.raw }); + } + } + + return flattenedTokens; + }, + }, + methods: { + getSafeHtml(markdown) { + return sanitize(marked.parse(markdown), markdownConfig); + }, + setHoverOn(key) { + this.hoverMap = { ...this.hoverMap, [key]: true }; + }, + setHoverOff(key) { + this.hoverMap = { ...this.hoverMap, [key]: false }; + }, + isLastElement(index) { + return index === this.markdownBlocks.length - 1; + }, + }, + safeHtmlConfig: { + ADD_TAGS: ['use', 'gl-emoji', 'copy-code'], + }, + i18n: { + copyCodeTitle: __('Copy code'), + }, + fallbackLanguage: 'text', +}; +</script> +<template> + <div> + <template v-for="(block, index) in markdownBlocks"> + <div + v-if="block.type === 'code'" + :key="`code-${index}`" + :class="{ 'gl-relative': true, 'gl-mb-4': !isLastElement(index) }" + data-testid="code-block-wrapper" + @mouseenter="setHoverOn(`code-${index}`)" + @mouseleave="setHoverOff(`code-${index}`)" + > + <modal-copy-button + v-if="hoverMap[`code-${index}`]" + :title="$options.i18n.copyCodeTitle" + :text="block.text" + class="gl-absolute gl-top-3 gl-right-3 gl-z-index-1 gl-transition-duration-medium" + /> + <code-block-highlighted + class="gl-border gl-rounded-0! gl-p-4 gl-mb-0 gl-overflow-y-auto" + :language="block.lang || $options.fallbackLanguage" + :code="block.text" + /> + </div> + <div + v-else + :key="`text-${index}`" + v-safe-html:[$options.safeHtmlConfig]="getSafeHtml(block.raw)" + :class="{ 'non-gfm-markdown-block': true, 'gl-mb-4': !isLastElement(index) }" + data-testid="non-code-markdown" + ></div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 6d1cadf15be..4423b26560f 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -40,7 +40,8 @@ export default { }, defaultCommitMessage: { type: String, - required: true, + required: false, + default: null, }, suggestionsCount: { type: Number, @@ -124,7 +125,7 @@ export default { suggestion, batchSuggestionsInfo, helpPagePath, - defaultCommitMessage, + defaultCommitMessage: defaultCommitMessage || '', suggestionsCount, failedToLoadMetadata, }, diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 4733afb7504..d4b1abedc02 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,24 +1,26 @@ <script> -import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { updateText } from '~/lib/utils/text_markdown'; +import { __, sprintf } from '~/locale'; +import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; +import EditorModeSwitcher from './editor_mode_switcher.vue'; export default { components: { GlButton, - GlLink, GlLoadingIcon, GlSprintf, GlIcon, + EditorModeSwitcher, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { markdownDocsPath: { type: String, required: true, }, - quickActionsDocsPath: { - type: String, - required: false, - default: '', - }, canAttachFile: { type: Boolean, required: false, @@ -29,10 +31,46 @@ export default { required: false, default: true, }, + showContentEditorSwitcher: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - hasQuickActionsDocsPath() { - return this.quickActionsDocsPath !== ''; + showEditorModeSwitcher() { + return this.showContentEditorSwitcher; + }, + }, + methods: { + insertIntoTextarea(...lines) { + const text = lines.join('\n'); + const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); + if (textArea && !textArea.value) { + updateText({ + textArea, + tag: text, + cursorOffset: 0, + wrap: false, + }); + } + }, + handleEditorModeChanged(isFirstSwitch) { + if (isFirstSwitch) { + this.insertIntoTextarea( + __(`### Rich text editor`), + '', + sprintf( + __( + 'Try out **styling** _your_ content right here or read the [direction](%{directionUrl}).', + ), + { + directionUrl: `${PROMO_URL}/direction/plan/knowledge/content_editor/`, + }, + ), + ); + } + this.$emit('enableContentEditor'); }, }, }; @@ -41,94 +79,80 @@ export default { <template> <div v-if="showCommentToolBar" - class="comment-toolbar gl-mx-2 gl-mb-2 gl-px-4 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base clearfix" + class="comment-toolbar gl-display-flex gl-flex-direction-row gl-px-2 gl-rounded-bottom-left-base gl-rounded-bottom-right-base" + :class=" + showContentEditorSwitcher + ? 'gl-justify-content-space-between gl-align-items-center gl-border-t gl-border-gray-100' + : 'gl-justify-content-end gl-my-2' + " > - <div class="toolbar-text gl-font-sm"> - <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <gl-sprintf - :message=" - s__('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}') - " - > - <template #markdownDocsLink="{ content }"> - <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </template> - <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> - <gl-sprintf - :message=" - s__( - 'NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.', - ) - " - > - <template #markdownDocsLink="{ content }"> - <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - <template #keyboard="{ content }"> - <kbd>{{ content }}</kbd> - </template> - <template #quickActionsDocsLink="{ content }"> - <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </template> - </div> - <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32"> - <span class="uploading-progress-container hide"> - <gl-icon name="paperclip" /> - <span class="attaching-file-message"></span> - <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> - <span class="uploading-progress">0%</span> - <gl-loading-icon size="sm" inline /> - </span> - <span class="uploading-error-container hide"> - <span class="uploading-error-icon"> + <editor-mode-switcher + v-if="showEditorModeSwitcher" + size="small" + value="markdown" + @switch="handleEditorModeChanged" + /> + <div class="gl-display-flex"> + <div v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32 gl-mr-3"> + <span class="uploading-progress-container hide"> <gl-icon name="paperclip" /> + <span class="attaching-file-message"></span> + <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> + <span class="uploading-progress">0%</span> + <gl-loading-icon size="sm" inline /> </span> - <span class="uploading-error-message"></span> + <span class="uploading-error-container hide"> + <span class="uploading-error-icon"> + <gl-icon name="paperclip" /> + </span> + <span class="uploading-error-message"></span> - <gl-sprintf - :message=" - __( - '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.', - ) - " + <gl-sprintf + :message=" + __( + '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.', + ) + " + > + <template #retryButton="{ content }"> + <gl-button + variant="link" + category="primary" + class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!" + > + {{ content }} + </gl-button> + </template> + <template #newFileButton="{ content }"> + <gl-button + variant="link" + category="primary" + class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!" + > + {{ content }} + </gl-button> + </template> + </gl-sprintf> + </span> + <gl-button + variant="link" + category="primary" + class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!" > - <template #retryButton="{ content }"> - <gl-button - variant="link" - category="primary" - class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!" - > - {{ content }} - </gl-button> - </template> - <template #newFileButton="{ content }"> - <gl-button - variant="link" - category="primary" - class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!" - > - {{ content }} - </gl-button> - </template> - </gl-sprintf> - </span> + {{ __('Cancel') }} + </gl-button> + </div> <gl-button - variant="link" - category="primary" - class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!" - > - {{ __('Cancel') }} - </gl-button> - </span> + v-if="markdownDocsPath" + v-gl-tooltip + icon="markdown-mark" + :href="markdownDocsPath" + target="_blank" + category="tertiary" + size="small" + title="Markdown is supported" + class="gl-px-3!" + /> + </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/tracking.js b/app/assets/javascripts/vue_shared/components/markdown/tracking.js new file mode 100644 index 00000000000..2628054ae5f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/tracking.js @@ -0,0 +1,14 @@ +import Tracking from '~/tracking'; + +export const EDITOR_TRACKING_LABEL = 'editor_tracking'; +export const EDITOR_TYPE_ACTION = 'editor_type_used'; +export const EDITOR_TYPE_PLAIN_TEXT_EDITOR = 'editor_type_plain_text_editor'; +export const EDITOR_TYPE_RICH_TEXT_EDITOR = 'editor_type_rich_text_editor'; + +export const trackSavedUsingEditor = (isRichText, context) => { + Tracking.event(undefined, EDITOR_TYPE_ACTION, { + label: EDITOR_TRACKING_LABEL, + editorType: isRichText ? EDITOR_TYPE_RICH_TEXT_EDITOR : EDITOR_TYPE_PLAIN_TEXT_EDITOR, + context, + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/markdown/utils.js b/app/assets/javascripts/vue_shared/components/markdown/utils.js new file mode 100644 index 00000000000..0227d5a0fbc --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/utils.js @@ -0,0 +1,7 @@ +let i = 0; + +export const counter = () => { + const n = i; + i += 1; + return n; +}; diff --git a/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue b/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue index 064458cfc1f..ba557878246 100644 --- a/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue @@ -51,6 +51,8 @@ export default { SidebarSubscriptionsWidget, AbuseCategorySelector, NewHeaderActionsPopover, + SummaryNotesToggle: () => + import('ee_component/merge_requests/components/summary_notes_toggle.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -60,6 +62,9 @@ export default { reportAbusePath: { default: '', }, + showSummaryNotesToggle: { + default: false, + }, }, props: { mr: { @@ -71,6 +76,11 @@ export default { default: '', required: false, }, + url: { + type: String, + default: '', + required: false, + }, editUrl: { type: String, default: '', @@ -116,11 +126,6 @@ export default { default: 0, required: false, }, - reportedFromUrl: { - type: String, - default: '', - required: false, - }, }, data() { return { @@ -156,7 +161,7 @@ export default { this.isLoadingDraft = true; axios - .put(`?merge_request[wip_event]=${this.draftState}`, null, { + .put(`${this.url}?merge_request[wip_event]=${this.draftState}`, null, { params: { format: 'json' }, }) .then(({ data }) => { @@ -226,10 +231,12 @@ export default { :auto-close="false" > <template #toggle> - <div class="gl-min-h-7 gl-mb-2 gl-md-mb-0!" :aria-label="$options.i18n.mergeRequestActions"> + <div class="gl-min-h-7 gl-mb-2 gl-md-mb-0!"> <gl-button class="gl-md-display-none! gl-new-dropdown-toggle gl-absolute gl-top-0 gl-left-0 gl-w-full" category="secondary" + :aria-label="$options.i18n.mergeRequestActions" + :title="$options.i18n.mergeRequestActions" > <span class="">{{ $options.i18n.mergeRequestActions }}</span> <gl-icon class="dropdown-chevron" name="chevron-down" /> @@ -238,6 +245,8 @@ export default { class="gl-display-none gl-md-display-flex! gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret gl-ml-3" category="tertiary" icon="ellipsis_v" + :aria-label="$options.i18n.mergeRequestActions" + :title="$options.i18n.mergeRequestActions" /> </div> </template> @@ -329,6 +338,8 @@ export default { {{ $options.i18n.copyReferenceText }} </template> </gl-disclosure-dropdown-item> + + <summary-notes-toggle v-if="showSummaryNotesToggle" @action="closeActionsDropdown" /> </gl-disclosure-dropdown-group> <gl-disclosure-dropdown-group @@ -353,7 +364,7 @@ export default { <abuse-category-selector v-if="!isCurrentUser && isReportAbuseDrawerOpen" :reported-user-id="reportedUserId" - :reported-from-url="reportedFromUrl" + :reported-from-url="url" :show-drawer="isReportAbuseDrawerOpen" @close-drawer="reportAbuseAction(false)" /> diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue index 57e3a97244e..ab9e6e092d9 100644 --- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue @@ -116,6 +116,7 @@ export default { unique: true, symbol: '@', token: UserToken, + dataType: 'user', operators: OPERATORS_IS, fetchPath: this.projectPath, fetchUsers: Api.projectUsers.bind(Api), @@ -127,6 +128,7 @@ export default { unique: true, symbol: '@', token: UserToken, + dataType: 'user', operators: OPERATORS_IS, fetchPath: this.projectPath, fetchUsers: Api.projectUsers.bind(Api), diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue index 11aa7b91745..cb8220a0407 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue @@ -30,12 +30,22 @@ export default { type: Array, required: true, }, + showProjectIcon: { + type: Boolean, + required: false, + default: false, + }, }, }; </script> <template> <ul class="gl-p-0 gl-list-style-none"> - <projects-list-item v-for="project in projects" :key="project.id" :project="project" /> + <projects-list-item + v-for="project in projects" + :key="project.id" + :project="project" + :show-project-icon="showProjectIcon" + /> </ul> </template> diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue index 266cce29e50..d919f76e684 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue @@ -34,6 +34,7 @@ export default { moreTopics: __('More topics'), updated: __('Updated'), }, + avatarSize: { default: 32, md: 48 }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'], }, @@ -78,6 +79,11 @@ export default { type: Object, required: true, }, + showProjectIcon: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -85,11 +91,14 @@ export default { }; }, computed: { + visibility() { + return this.project.visibility; + }, visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.project.visibility]; + return VISIBILITY_TYPE_ICON[this.visibility]; }, visibilityTooltip() { - return PROJECT_VISIBILITY_TYPE[this.project.visibility]; + return PROJECT_VISIBILITY_TYPE[this.visibility]; }, accessLevel() { return this.project.permissions?.projectAccess?.accessLevel; @@ -150,71 +159,87 @@ export default { <template> <li class="projects-list-item gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b"> - <gl-avatar-labeled - class="gl-flex-grow-1" - :entity-id="project.id" - :entity-name="project.name" - :label="project.name" - :label-link="project.webUrl" - shape="rect" - :size="48" - > - <template #meta> - <gl-icon - v-gl-tooltip="visibilityTooltip" - :name="visibilityIcon" - class="gl-text-secondary gl-ml-3" - /> - <user-access-role-badge v-if="shouldShowAccessLevel" class="gl-ml-3">{{ - accessLevelLabel - }}</user-access-role-badge> - </template> - <div - v-if="project.descriptionHtml" - v-safe-html:[$options.safeHtmlConfig]="project.descriptionHtml" - class="gl-font-sm gl-overflow-hidden gl-line-height-20 description" - data-testid="project-description" - ></div> - <div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics"> - <div - class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2" - > - <span class="gl-p-2 gl-text-secondary">{{ $options.i18n.topics }}:</span> - <div v-for="topic in visibleTopics" :key="topic" class="gl-p-2"> - <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)"> - {{ topicTitle(topic) }} - </gl-badge> + <div class="gl-display-flex gl-flex-grow-1"> + <gl-icon + v-if="showProjectIcon" + class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary" + name="project" + /> + <gl-avatar-labeled + :entity-id="project.id" + :entity-name="project.name" + :label="project.name" + :label-link="project.webUrl" + shape="rect" + :size="$options.avatarSize" + > + <template #meta> + <div class="gl-px-2"> + <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap"> + <div class="gl-px-2"> + <gl-icon + v-if="visibility" + v-gl-tooltip="visibilityTooltip" + :name="visibilityIcon" + class="gl-text-secondary" + /> + </div> + <div class="gl-px-2"> + <user-access-role-badge v-if="shouldShowAccessLevel">{{ + accessLevelLabel + }}</user-access-role-badge> + </div> + </div> </div> - <template v-if="popoverTopics.length"> - <div - :id="topicsPopoverTarget" - class="gl-p-2 gl-text-secondary" - role="button" - tabindex="0" - > - <gl-sprintf :message="$options.i18n.topicsPopoverTargetText"> - <template #count>{{ popoverTopics.length }}</template> - </gl-sprintf> + </template> + <div + v-if="project.descriptionHtml" + v-safe-html:[$options.safeHtmlConfig]="project.descriptionHtml" + class="gl-font-sm gl-overflow-hidden gl-line-height-20 description md" + data-testid="project-description" + ></div> + <div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics"> + <div + class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2" + > + <span class="gl-p-2 gl-text-secondary">{{ $options.i18n.topics }}:</span> + <div v-for="topic in visibleTopics" :key="topic" class="gl-p-2"> + <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)"> + {{ topicTitle(topic) }} + </gl-badge> </div> - <gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics"> - <div class="gl-font-base gl-font-weight-normal gl-mx-n2 gl-my-n2"> - <div - v-for="topic in popoverTopics" - :key="topic" - class="gl-p-2 gl-display-inline-block" - > - <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)"> - {{ topicTitle(topic) }} - </gl-badge> - </div> + <template v-if="popoverTopics.length"> + <div + :id="topicsPopoverTarget" + class="gl-p-2 gl-text-secondary" + role="button" + tabindex="0" + > + <gl-sprintf :message="$options.i18n.topicsPopoverTargetText"> + <template #count>{{ popoverTopics.length }}</template> + </gl-sprintf> </div> - </gl-popover> - </template> + <gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics"> + <div class="gl-font-base gl-font-weight-normal gl-mx-n2 gl-my-n2"> + <div + v-for="topic in popoverTopics" + :key="topic" + class="gl-p-2 gl-display-inline-block" + > + <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)"> + {{ topicTitle(topic) }} + </gl-badge> + </div> + </div> + </gl-popover> + </template> + </div> </div> - </div> - </gl-avatar-labeled> + </gl-avatar-labeled> + </div> <div - class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-pl-10 gl-md-pl-0 gl-md-mt-0" + class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0" + :class="showProjectIcon ? 'gl-pl-11' : 'gl-pl-8'" > <div class="gl-display-flex gl-align-items-center gl-gap-x-3"> <gl-badge v-if="project.archived" variant="warning">{{ $options.i18n.archived }}</gl-badge> @@ -248,7 +273,10 @@ export default { <span>{{ numberToMetricPrefix(project.openIssuesCount) }}</span> </gl-link> </div> - <div class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3"> + <div + v-if="project.updatedAt" + class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3" + > <span>{{ $options.i18n.updated }}</span> <time-ago-tooltip :time="project.updatedAt" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue index 5d0ee6adffe..ccda8c5fea7 100644 --- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue +++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue @@ -1,9 +1,13 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; export default { name: 'ListItem', components: { GlButton }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { first: { type: Boolean, @@ -27,6 +31,9 @@ export default { detailsSlots: [], }; }, + i18n: { + toggleDetailsLabel: __('Toggle details'), + }, computed: { optionalClasses() { return { @@ -75,10 +82,14 @@ export default { <slot name="left-primary"></slot> <gl-button v-if="detailsSlots.length > 0" + v-gl-tooltip :selected="isDetailsShown" icon="ellipsis_h" size="small" class="gl-ml-2 gl-display-none gl-sm-display-block" + :title="$options.i18n.toggleDetailsLabel" + :aria-label="$options.i18n.toggleDetailsLabel" + :aria-expanded="isDetailsShown" @click="toggleDetails" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue index ff7e803af2a..5d04fd1d8e7 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue @@ -1,6 +1,7 @@ <script> import { GlButton, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; +import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility'; export default { components: { @@ -16,7 +17,7 @@ export default { 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation', ), I18N_VIEW_INSTRUCTIONS: s__('Runners|View installation instructions'), - HELP_URL: 'https://docs.gitlab.com/runner/install/docker.html', + HELP_URL: `${DOCS_URL}/runner/install/docker.html`, }; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue index ee41dab0cec..a769b4a6ad8 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue @@ -1,6 +1,7 @@ <script> import { GlButton, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; +import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility'; export default { components: { @@ -16,7 +17,7 @@ export default { 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.', ), I18N_VIEW_INSTRUCTIONS: s__('Runners|View installation instructions'), - HELP_URL: 'https://docs.gitlab.com/runner/install/kubernetes.html', + HELP_URL: `${DOCS_URL}/runner/install/kubernetes.html`, }; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue index 94aa7bd2f88..3b5086b3c7e 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue @@ -122,14 +122,6 @@ export default { return null; } }, - showDeprecationAlert() { - return ( - // create_runner_workflow_for_admin - this.glFeatures.createRunnerWorkflowForAdmin || - // create_runner_workflow_for_namespace - this.glFeatures.createRunnerWorkflowForNamespace - ); - }, }, updated() { // Refocus on dom changes, after loading data @@ -200,12 +192,7 @@ export default { v-on="$listeners" @shown="onShown" > - <gl-alert - v-if="showDeprecationAlert" - :title="$options.i18n.deprecationAlertTitle" - variant="warning" - :dismissible="false" - > + <gl-alert :title="$options.i18n.deprecationAlertTitle" variant="warning" :dismissible="false"> <gl-sprintf :message="$options.i18n.deprecationAlertContent"> <template #link="{ content }"> <gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL" diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue index d77061d4b31..b89fa3f8292 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue @@ -63,9 +63,6 @@ export default { shouldHighlight() { return Boolean(this.highlightedContent) && (this.hasAppeared || this.isHighlighted); }, - lines() { - return this.content.split('\n'); - }, pageSearchString() { const page = getPageParamValue(this.number); return getPageSearchString(this.blamePath, page); @@ -123,7 +120,7 @@ export default { <pre class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0" - ><code v-if="shouldHighlight" v-once v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre> + ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre> </div> </gl-intersection-observer> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js index d694adf7147..3f8a9258fc3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js @@ -1,6 +1,7 @@ import wrapChildNodes from './wrap_child_nodes'; import linkDependencies from './link_dependencies'; import wrapBidiChars from './wrap_bidi_chars'; +import wrapLines from './wrap_lines'; export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight'; @@ -11,10 +12,11 @@ export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight'; * * @param {Object} hljs - the Highlight.js instance. */ -export const registerPlugins = (hljs, fileType, rawContent) => { +export const registerPlugins = (hljs, fileType, rawContent, shouldWrapLines) => { hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes }); hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars }); hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent), }); + if (shouldWrapLines) hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapLines }); }; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js index a79e88a1132..b972d8ece91 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js @@ -22,7 +22,7 @@ const format = (node, scope = '') => { .split(newlineRegex) .map((newline) => generateHLJSTag(scope, newline, true)) .join('\n'); - } else if (node.scope || node.sublanguage) { + } else if (node.children) { const { children } = node; if (children.length && children.length === 1) { buffer += format(children[0], node.scope); diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_lines.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_lines.js new file mode 100644 index 00000000000..384ada30001 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_lines.js @@ -0,0 +1,20 @@ +/** + * Highlight.js plugin for wrapping lines in the correct classes and attributes. + * Needed for things like hash highlighting to work. + * + * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst + * + * @param {Object} Result - an object that represents the highlighted result from Highlight.js + */ + +function wrapLine(content, number, language) { + return `<div id="LC${number}" lang="${language}" class="line">${content}</div>`; +} + +export default (result) => { + // eslint-disable-next-line no-param-reassign + result.value = result.value + .split(/\r?\n/) + .map((content, index) => wrapLine(content, index + 1, result.language)) + .join('\n'); +}; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue index 7e18c8414d5..8e4c438719e 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue @@ -2,6 +2,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html'; import Tracking from '~/tracking'; import addBlobLinksTracking from '~/blob/blob_links_tracking'; +import LineHighlighter from '~/blob/line_highlighter'; import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants'; import Chunk from './components/chunk_new.vue'; @@ -19,9 +20,6 @@ export default { SafeHtml, }, mixins: [Tracking.mixin()], - inject: { - highlightWorker: { default: null }, - }, props: { blob: { type: Object, @@ -33,6 +31,11 @@ export default { default: () => [], }, }, + data() { + return { + lineHighlighter: new LineHighlighter(), + }; + }, created() { this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language }); addBlobLinksTracking(); diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js index 142c135e9c1..8d8e945cd5f 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js @@ -1,9 +1,13 @@ -import hljs from 'highlight.js'; +import hljs from 'highlight.js/lib/core'; +import json from 'highlight.js/lib/languages/json'; import { registerPlugins } from '../plugins/index'; import { LINES_PER_CHUNK, NEWLINE, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants'; -const initHighlightJs = (fileType, content) => { - registerPlugins(hljs, fileType, content); +const initHighlightJs = (fileType, content, language) => { + // The Highlight Worker is currently scoped to JSON files. + // See the following issue for more: https://gitlab.com/gitlab-org/gitlab/-/issues/415753 + hljs.registerLanguage(language, json); + registerPlugins(hljs, fileType, content, true); }; const splitByLineBreaks = (content = '') => content.split(/\r?\n/); diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js index 535e857d7a9..535e857d7a9 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 00720f27934..0949071d4dc 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -18,6 +18,7 @@ */ import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import UserAvatarImage from './user_avatar_image.vue'; export default { @@ -74,6 +75,16 @@ export default { required: false, default: 'top', }, + popoverUserId: { + type: [String, Number], + required: false, + default: '', + }, + popoverUsername: { + type: String, + required: false, + default: '', + }, username: { type: String, required: false, @@ -81,10 +92,17 @@ export default { }, }, computed: { + userId() { + return getIdFromGraphQLId(this.popoverUserId); + }, shouldShowUsername() { return this.username.length > 0; }, avatarTooltipText() { + // Prevent showing tooltip when popoverUserId is present + if (this.popoverUserId) { + return ''; + } return this.shouldShowUsername ? '' : this.tooltipText; }, }, @@ -92,7 +110,12 @@ export default { </script> <template> - <gl-avatar-link :href="linkHref" class="user-avatar-link"> + <gl-avatar-link + :href="linkHref" + :data-user-id="userId" + :data-username="popoverUsername" + class="user-avatar-link js-user-link" + > <user-avatar-image :class="imgCssWrapperClasses" :img-src="imgSrc" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue index 335f9ab1df4..258e8b1a6c5 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue @@ -81,6 +81,8 @@ export default { :img-alt="item.name" :tooltip-text="item.name" :img-size="imgSize" + :popover-user-id="item.id" + :popover-username="item.username" img-css-classes="gl-mr-3" /> <template v-if="hasBreakpoint"> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index e09f193310b..446c8c97df0 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -91,6 +91,9 @@ export default { return ''; }, + userCannotMerge() { + return this.target.dataset.cannotMerge; + }, userIsLoading() { return !this.user?.loaded; }, @@ -123,6 +126,15 @@ export default { username() { return `@${this.user?.username}`; }, + cssClasses() { + const classList = ['user-popover', 'gl-max-w-48', 'gl-overflow-hidden']; + + if (this.userCannotMerge) { + classList.push('user-popover-cannot-merge'); + } + + return classList; + }, }, methods: { async toggleFollow() { @@ -181,7 +193,7 @@ export default { <template> <!-- Delayed so not every mouseover triggers Popover --> <gl-popover - :css-classes="['gl-max-w-48']" + :css-classes="cssClasses" :show="show" :target="target" :delay="$options.USER_POPOVER_DELAY" @@ -190,6 +202,12 @@ export default { triggers="hover focus manual" data-testid="user-popover" > + <template v-if="userCannotMerge" #title> + <div class="gl-pb-3 gl-display-flex gl-align-items-center" data-testid="cannot-merge"> + <gl-icon name="warning-solid" class="gl-mr-2 gl-text-orange-400" /> + <span class="gl-font-weight-normal">{{ __('Cannot merge') }}</span> + </div> + </template> <div class="gl-mb-3"> <div v-if="userIsLoading" class="gl-w-20"> <gl-skeleton-loader :width="160" :height="64"> @@ -204,6 +222,7 @@ export default { :src="user.avatarUrl" :label="user.name" :sub-label="username" + class="gl-w-full" > <template v-if="isBlocked"> <span class="gl-mt-4 gl-font-style-italic">{{ $options.I18N_USER_BLOCKED }}</span> diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 82f4edcbd5f..9a06c0ecf30 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -23,6 +23,7 @@ export const i18n = { }; export default { + name: 'CEWebIdeLink', components: { ActionsButton, GlModal, @@ -319,7 +320,11 @@ export default { :toggle-text="$options.i18n.toggleText" :variant="isBlob ? 'confirm' : 'default'" :category="isBlob ? 'primary' : 'secondary'" - /> + @hidden="$emit('hidden')" + @shown="$emit('shown')" + > + <slot></slot> + </actions-button> <gl-modal v-if="computedShowGitpodButton && !gitpodEnabled" v-model="showEnableGitpodModal" diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 3896e963a53..8946a02e663 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -94,6 +94,7 @@ export const confidentialityInfoText = (workspaceType, issuableType) => }, ); +export const EDITING_MODE_KEY = 'gl-markdown-editor-mode'; export const EDITING_MODE_MARKDOWN_FIELD = 'markdownField'; export const EDITING_MODE_CONTENT_EDITOR = 'contentEditor'; export const CLEAR_AUTOSAVE_ENTRY_EVENT = 'markdown_clear_autosave_entry'; diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js index 4211b9578a2..a693d4f114d 100644 --- a/app/assets/javascripts/vue_shared/global_search/constants.js +++ b/app/assets/javascripts/vue_shared/global_search/constants.js @@ -75,3 +75,23 @@ export const SEARCH_RESULTS_ORDER = [ HELP_CATEGORY, ]; export const DROPDOWN_ORDER = SEARCH_RESULTS_ORDER; + +export const SEARCH_LABELS = s__('GlobalSearch|Search labels'); + +export const DROPDOWN_HEADER = s__('GlobalSearch|Labels'); + +export const AGGREGATIONS_ERROR_MESSAGE = s__('GlobalSearch|Fetching aggregations error.'); + +export const NO_LABELS_FOUND = s__('GlobalSearch|No labels found'); + +export const I18N = { + SEARCH_DESCRIBED_BY_DEFAULT, + SEARCH_RESULTS_LOADING, + SEARCH_DESCRIBED_BY_UPDATED, + SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN, + SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN, + SEARCH_LABELS, + DROPDOWN_HEADER, + AGGREGATIONS_ERROR_MESSAGE, + NO_LABELS_FOUND, +}; diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue index 3d4eebb9524..53e976d698b 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue @@ -9,14 +9,16 @@ import { } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { STATUS_OPEN } from '~/issues/constants'; +import { issuableStatusText, STATUS_OPEN } from '~/issues/constants'; import { isExternal } from '~/lib/utils/url_utility'; import { n__, sprintf } from '~/locale'; +import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; export default { components: { + ConfidentialityBadge, GlIcon, GlBadge, GlButton, @@ -77,8 +79,16 @@ export default { required: false, default: false, }, + workspaceType: { + type: String, + required: false, + default: '', + }, }, computed: { + badgeText() { + return issuableStatusText[this.issuableState]; + }, badgeVariant() { return this.issuableState === STATUS_OPEN ? 'success' : 'info'; }, @@ -109,6 +119,7 @@ export default { }, methods: { handleRightSidebarToggleClick() { + this.$emit('toggle'); if (this.toggleSidebarButtonEl) { this.toggleSidebarButtonEl.dispatchEvent(new Event('click')); } @@ -118,21 +129,23 @@ export default { </script> <template> - <div class="detail-page-header"> + <div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row"> <div class="detail-page-header-body"> <gl-badge class="issuable-status-badge gl-mr-3" :variant="badgeVariant"> <gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" /> - <span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span> + <span class="gl-display-none gl-sm-display-block gl-ml-2"> + <slot name="status-badge">{{ badgeText }}</slot> + </span> </gl-badge> - <div class="issuable-meta gl-display-flex! gl-align-items-center"> - <div v-if="blocked || confidential" class="gl-display-inline-block"> - <div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline"> - <gl-icon name="lock" :aria-label="__('Blocked')" /> - </div> - <div v-if="confidential" data-testid="confidential" class="issuable-warning-icon inline"> - <gl-icon name="eye-slash" :aria-label="__('Confidential')" /> - </div> + <div class="issuable-meta gl-display-flex! gl-align-items-center gl-flex-wrap"> + <div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline"> + <gl-icon name="lock" :aria-label="__('Blocked')" /> </div> + <confidentiality-badge + v-if="confidential" + :issuable-type="issuableType" + :workspace-type="workspaceType" + /> <span> <template v-if="showWorkItemTypeIcon"> <work-item-type-icon :work-item-type="issuableType" show-text /> @@ -182,10 +195,7 @@ export default { @click="handleRightSidebarToggleClick" /> </div> - <div - data-testid="header-actions" - class="detail-page-header-actions gl-display-flex gl-md-display-block" - > + <div data-testid="header-actions" class="detail-page-header-actions gl-display-flex"> <slot name="header-actions"></slot> </div> </div> diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue index c33e803c7e1..841d92fd63d 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue @@ -81,9 +81,12 @@ export default { data-testid="header" > <div - class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5" + class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto gl-px-5" > - <gl-badge class="gl-white-space-nowrap gl-mr-3" :variant="badgeVariant"> + <gl-badge + class="gl-white-space-nowrap gl-mr-3 gl-align-self-center" + :variant="badgeVariant" + > <gl-icon v-if="statusIcon" class="gl-sm-display-none" :name="statusIcon" /> <span class="gl-display-none gl-sm-display-block"> <slot name="status-badge"></slot> diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue index caa85d3eaaf..1b4da047057 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue @@ -1,11 +1,7 @@ <script> -import SafeHtml from '~/vue_shared/directives/safe_html'; import Tracking from '~/tracking'; export default { - directives: { - SafeHtml, - }, mixins: [Tracking.mixin()], props: { title: { @@ -38,9 +34,10 @@ export default { @click="track('click_tab', { label: panel.name })" > <div - v-safe-html="panel.illustration" - class="new-namespace-panel-illustration gl-text-white gl-display-flex gl-flex-shrink-0 gl-justify-content-center" - ></div> + class="new-namespace-panel-illustration gl-display-flex gl-flex-shrink-0 gl-justify-content-center" + > + <img aria-hidden :src="panel.imageSrc" /> + </div> <div class="gl-pl-4"> <h3 class="gl-font-size-h2 gl-reset-color"> {{ panel.title }} diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue index 5ab2e346a7a..4503ba6e561 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue @@ -1,6 +1,5 @@ <script> import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; -import SafeHtml from '~/vue_shared/directives/safe_html'; import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue'; import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue'; @@ -18,9 +17,6 @@ export default { LegacyContainer, SuperSidebarToggle, }, - directives: { - SafeHtml, - }, props: { title: { type: String, @@ -137,7 +133,9 @@ export default { <template v-if="activePanel"> <div class="gl-display-flex gl-align-items-center gl-py-5"> - <div v-safe-html="activePanel.illustration" class="gl-text-white col-auto"></div> + <div class="col-auto"> + <img aria-hidden :src="activePanel.imageSrc" /> + </div> <div class="col"> <h4>{{ activePanel.title }}</h4> diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue deleted file mode 100644 index 4c2b082242b..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script> -import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants'; -import { createAlert } from '~/alert'; -import { s__ } from '~/locale'; -import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; -import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils'; - -export default { - components: { - SecurityReportDownloadDropdown, - }, - props: { - reportTypes: { - type: Array, - required: true, - validator: (reportType) => { - return reportType.every((report) => reportTypeToSecurityReportTypeEnum[report]); - }, - }, - targetProjectFullPath: { - type: String, - required: true, - }, - mrIid: { - type: Number, - required: true, - }, - injectedArtifacts: { - type: Array, - required: false, - default: () => [], - }, - }, - data() { - return { - reportArtifacts: [], - }; - }, - apollo: { - reportArtifacts: { - query: securityReportMergeRequestDownloadPathsQuery, - variables() { - return { - projectPath: this.targetProjectFullPath, - iid: String(this.mrIid), - reportTypes: this.reportTypes.map( - (reportType) => reportTypeToSecurityReportTypeEnum[reportType], - ), - }; - }, - update(data) { - return extractSecurityReportArtifactsFromMergeRequest(this.reportTypes, data); - }, - error(error) { - this.showError(error); - }, - }, - }, - computed: { - isLoadingReportArtifacts() { - return this.$apollo.queries.reportArtifacts.loading; - }, - mergedReportArtifacts() { - return [...this.reportArtifacts, ...this.injectedArtifacts]; - }, - }, - methods: { - showError(error) { - createAlert({ - message: this.$options.i18n.apiError, - captureError: true, - error, - }); - }, - }, - i18n: { - apiError: s__( - 'SecurityReports|Failed to get security report information. Please reload the page or try again later.', - ), - }, -}; -</script> - -<template> - <security-report-download-dropdown - :title="s__('SecurityReports|Download results')" - :artifacts="mergedReportArtifacts" - :loading="isLoadingReportArtifacts" - /> -</template> diff --git a/app/assets/javascripts/vue_shared/security_reports/components/constants.js b/app/assets/javascripts/vue_shared/security_reports/components/constants.js deleted file mode 100644 index 5e8199c1bcd..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/constants.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SEVERITY_CLASS_NAME_MAP = { - critical: 'gl-text-red-800', - high: 'gl-text-red-600', - medium: 'gl-text-orange-400', - low: 'gl-text-orange-300', - info: 'gl-text-blue-400', - unknown: 'gl-text-gray-400', -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue b/app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue deleted file mode 100644 index eed1c86c318..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue +++ /dev/null @@ -1,58 +0,0 @@ -<script> -import { GlButton, GlIcon, GlLink, GlPopover } from '@gitlab/ui'; -import { s__ } from '~/locale'; - -export default { - components: { - GlButton, - GlIcon, - GlLink, - GlPopover, - }, - props: { - helpPath: { - type: String, - required: true, - }, - discoverProjectSecurityPath: { - type: String, - required: false, - default: '', - }, - }, - i18n: { - securityReportsHelp: s__('SecurityReports|Security reports help page link'), - upgradeToManageVulnerabilities: s__('SecurityReports|Upgrade to manage vulnerabilities'), - upgradeToInteract: s__( - 'SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI.', - ), - }, -}; -</script> - -<template> - <span v-if="discoverProjectSecurityPath"> - <gl-button - ref="discoverProjectSecurity" - icon="question-o" - category="tertiary" - :aria-label="$options.i18n.upgradeToManageVulnerabilities" - /> - - <gl-popover - :target="() => $refs.discoverProjectSecurity.$el" - :title="$options.i18n.upgradeToManageVulnerabilities" - placement="top" - triggers="click blur" - > - {{ $options.i18n.upgradeToInteract }} - <gl-link :href="discoverProjectSecurityPath" target="_blank" class="gl-font-sm">{{ - __('Learn more') - }}</gl-link> - </gl-popover> - </span> - - <gl-link v-else target="_blank" :href="helpPath" :aria-label="$options.i18n.securityReportsHelp"> - <gl-icon name="question-o" /> - </gl-link> -</template> diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue deleted file mode 100644 index e3aa25a294e..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue +++ /dev/null @@ -1,59 +0,0 @@ -<script> -import { GlSprintf } from '@gitlab/ui'; -import { SEVERITY_CLASS_NAME_MAP } from './constants'; - -export default { - components: { - GlSprintf, - }, - props: { - message: { - type: Object, - required: true, - }, - }, - computed: { - shouldShowCountMessage() { - return !this.message.status && Boolean(this.message.countMessage); - }, - }, - methods: { - getSeverityClass(severity) { - return SEVERITY_CLASS_NAME_MAP[severity]; - }, - }, - slotNames: ['critical', 'high', 'other'], - spacingClasses: { - critical: 'gl-pl-4', - high: 'gl-px-2', - other: 'gl-px-2', - }, -}; -</script> - -<template> - <span> - <gl-sprintf :message="message.message"> - <template #total="{ content }"> - <strong>{{ content }}</strong> - </template> - </gl-sprintf> - <span v-if="shouldShowCountMessage" class="gl-font-sm"> - <gl-sprintf :message="message.countMessage"> - <template v-for="slotName in $options.slotNames" #[slotName]="{ content }"> - <span :key="slotName"> - <strong - v-if="message[slotName] > 0" - :class="[getSeverityClass(slotName), $options.spacingClasses[slotName]]" - > - {{ content }} - </strong> - <span v-else :class="$options.spacingClasses[slotName]"> - {{ content }} - </span> - </span> - </template> - </gl-sprintf> - </span> - </span> -</template> diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index a1d75e08be9..56c6affebd7 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -28,7 +28,6 @@ export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning'; export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing'; export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management'; export const REPORT_TYPE_API_FUZZING = 'api_fuzzing'; -export const REPORT_TYPE_MANUALLY_ADDED = 'generic'; /** * SecurityReportTypeEnum values for use with GraphQL. diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue deleted file mode 100644 index 0cff5edf628..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ /dev/null @@ -1,238 +0,0 @@ -<script> -import { mapActions, mapGetters } from 'vuex'; -import { createAlert } from '~/alert'; -import { s__ } from '~/locale'; -import ReportSection from '~/ci/reports/components/report_section.vue'; -import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/ci/reports/constants'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import HelpIcon from './components/help_icon.vue'; -import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue'; -import SecuritySummary from './components/security_summary.vue'; -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, - reportTypeToSecurityReportTypeEnum, -} from './constants'; -import securityReportMergeRequestDownloadPathsQuery from './graphql/queries/security_report_merge_request_download_paths.query.graphql'; -import store from './store'; -import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants'; -import { extractSecurityReportArtifactsFromMergeRequest } from './utils'; - -export default { - store, - components: { - ReportSection, - HelpIcon, - SecurityReportDownloadDropdown, - SecuritySummary, - }, - mixins: [glFeatureFlagsMixin()], - props: { - pipelineId: { - type: Number, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - securityReportsDocsPath: { - type: String, - required: true, - }, - discoverProjectSecurityPath: { - type: String, - required: false, - default: '', - }, - sastComparisonPath: { - type: String, - required: false, - default: '', - }, - secretDetectionComparisonPath: { - type: String, - required: false, - default: '', - }, - targetProjectFullPath: { - type: String, - required: false, - default: '', - }, - mrIid: { - type: Number, - required: false, - default: 0, - }, - canDiscoverProjectSecurity: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - availableSecurityReports: [], - canShowCounts: false, - - // When core_security_mr_widget_counts is not enabled, the - // error state is shown even when successfully loaded, since success - // state suggests that the security scans detected no security problems, - // which is not necessarily the case. A future iteration will actually - // check whether problems were found and display the appropriate status. - status: ERROR, - }; - }, - apollo: { - reportArtifacts: { - query: securityReportMergeRequestDownloadPathsQuery, - variables() { - return { - projectPath: this.targetProjectFullPath, - iid: String(this.mrIid), - reportTypes: this.$options.reportTypes.map( - (reportType) => reportTypeToSecurityReportTypeEnum[reportType], - ), - }; - }, - update(data) { - return extractSecurityReportArtifactsFromMergeRequest(this.$options.reportTypes, data); - }, - error(error) { - this.showError(error); - }, - result({ loading, data }) { - if (loading || !data) { - return; - } - - // Query has completed, so populate the availableSecurityReports. - this.onCheckingAvailableSecurityReports( - this.reportArtifacts.map(({ reportType }) => reportType), - ); - }, - }, - }, - computed: { - ...mapGetters(['groupedSummaryText', 'summaryStatus']), - hasSecurityReports() { - return this.availableSecurityReports.length > 0; - }, - hasSastReports() { - return this.availableSecurityReports.includes(REPORT_TYPE_SAST); - }, - hasSecretDetectionReports() { - return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION); - }, - isLoadingReportArtifacts() { - return this.$apollo.queries.reportArtifacts.loading; - }, - }, - methods: { - ...mapActions(MODULE_SAST, { - setSastDiffEndpoint: 'setDiffEndpoint', - fetchSastDiff: 'fetchDiff', - }), - ...mapActions(MODULE_SECRET_DETECTION, { - setSecretDetectionDiffEndpoint: 'setDiffEndpoint', - fetchSecretDetectionDiff: 'fetchDiff', - }), - fetchCounts() { - if (!this.glFeatures.coreSecurityMrWidgetCounts) { - return; - } - - if (this.sastComparisonPath && this.hasSastReports) { - this.setSastDiffEndpoint(this.sastComparisonPath); - this.fetchSastDiff(); - this.canShowCounts = true; - } - - if (this.secretDetectionComparisonPath && this.hasSecretDetectionReports) { - this.setSecretDetectionDiffEndpoint(this.secretDetectionComparisonPath); - this.fetchSecretDetectionDiff(); - this.canShowCounts = true; - } - }, - onCheckingAvailableSecurityReports(availableSecurityReports) { - this.availableSecurityReports = availableSecurityReports; - this.fetchCounts(); - }, - showError(error) { - createAlert({ - message: this.$options.i18n.apiError, - captureError: true, - error, - }); - }, - }, - reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION], - i18n: { - apiError: s__( - 'SecurityReports|Failed to get security report information. Please reload the page or try again later.', - ), - scansHaveRun: s__('SecurityReports|Security scans have run'), - }, - summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR], -}; -</script> -<template> - <report-section - v-if="canShowCounts" - :status="summaryStatus" - :has-issues="false" - class="mr-widget-border-top mr-report" - data-testid="security-mr-widget" - track-action="users_expanding_secure_security_report" - > - <template v-for="slot in $options.summarySlots" #[slot]> - <span :key="slot"> - <security-summary :message="groupedSummaryText" /> - - <help-icon - class="gl-ml-3" - :help-path="securityReportsDocsPath" - :discover-project-security-path="discoverProjectSecurityPath" - /> - </span> - </template> - - <template #action-buttons> - <security-report-download-dropdown - :text="s__('SecurityReports|Download results')" - :artifacts="reportArtifacts" - :loading="isLoadingReportArtifacts" - /> - </template> - </report-section> - - <!-- TODO: Remove this section when removing core_security_mr_widget_counts - feature flag. See https://gitlab.com/gitlab-org/gitlab/-/issues/284097 --> - <report-section - v-else-if="hasSecurityReports" - :status="status" - :has-issues="false" - class="mr-widget-border-top mr-report" - data-testid="security-mr-widget" - track-action="users_expanding_secure_security_report" - > - <template #error> - {{ $options.i18n.scansHaveRun }} - - <help-icon - class="gl-ml-3" - :help-path="securityReportsDocsPath" - :discover-project-security-path="discoverProjectSecurityPath" - /> - </template> - - <template #action-buttons> - <security-report-download-dropdown - :text="s__('SecurityReports|Download results')" - :artifacts="reportArtifacts" - :loading="isLoadingReportArtifacts" - /> - </template> - </report-section> -</template> diff --git a/app/assets/javascripts/vue_shared/security_reports/store/constants.js b/app/assets/javascripts/vue_shared/security_reports/store/constants.js deleted file mode 100644 index 6aeab56eea2..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/constants.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Vuex module names corresponding to security scan types. These are similar to - * the snake_case report types from the backend, but should not be considered - * to be equivalent. - */ -export const MODULE_SAST = 'sast'; -export const MODULE_SECRET_DETECTION = 'secretDetection'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js deleted file mode 100644 index c274f531139..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js +++ /dev/null @@ -1,66 +0,0 @@ -import { s__, sprintf } from '~/locale'; -import { LOADING, ERROR, SUCCESS } from '~/ci/reports/constants'; -import { TRANSLATION_IS_LOADING } from './messages'; -import { countVulnerabilities, groupedTextBuilder } from './utils'; - -export const summaryCounts = (state) => - countVulnerabilities( - state.reportTypes.reduce((acc, reportType) => { - acc.push(...state[reportType].newIssues); - return acc; - }, []), - ); - -export const groupedSummaryText = (state, getters) => { - const reportType = s__('ciReport|Security scanning'); - let status = ''; - - // All reports are loading - if (getters.areAllReportsLoading) { - return { message: sprintf(TRANSLATION_IS_LOADING, { reportType }) }; - } - - // All reports returned error - if (getters.allReportsHaveError) { - return { message: s__('ciReport|Security scanning failed loading any results') }; - } - - if (getters.areReportsLoading && getters.anyReportHasError) { - status = s__('ciReport|is loading, errors when loading results'); - } else if (getters.areReportsLoading && !getters.anyReportHasError) { - status = s__('ciReport|is loading'); - } else if (!getters.areReportsLoading && getters.anyReportHasError) { - status = s__('ciReport|: Loading resulted in an error'); - } - - const { critical, high, other } = getters.summaryCounts; - - return groupedTextBuilder({ reportType, status, critical, high, other }); -}; - -export const summaryStatus = (state, getters) => { - if (getters.areReportsLoading) { - return LOADING; - } - - if (getters.anyReportHasError || getters.anyReportHasIssues) { - return ERROR; - } - - return SUCCESS; -}; - -export const areReportsLoading = (state) => - state.reportTypes.some((reportType) => state[reportType].isLoading); - -export const areAllReportsLoading = (state) => - state.reportTypes.every((reportType) => state[reportType].isLoading); - -export const allReportsHaveError = (state) => - state.reportTypes.every((reportType) => state[reportType].hasError); - -export const anyReportHasError = (state) => - state.reportTypes.some((reportType) => state[reportType].hasError); - -export const anyReportHasIssues = (state) => - state.reportTypes.some((reportType) => state[reportType].newIssues.length > 0); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/index.js b/app/assets/javascripts/vue_shared/security_reports/store/index.js deleted file mode 100644 index 164faa86744..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import Vuex from 'vuex'; -import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants'; -import * as getters from './getters'; -import sast from './modules/sast'; -import secretDetection from './modules/secret_detection'; -import state from './state'; - -export default () => - new Vuex.Store({ - modules: { - [MODULE_SAST]: sast, - [MODULE_SECRET_DETECTION]: secretDetection, - }, - getters, - state, - }); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/messages.js b/app/assets/javascripts/vue_shared/security_reports/store/messages.js deleted file mode 100644 index c25e252a768..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/messages.js +++ /dev/null @@ -1,4 +0,0 @@ -import { s__ } from '~/locale'; - -export const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading'); -export const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error'); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js deleted file mode 100644 index 8aefc13a5fa..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js +++ /dev/null @@ -1,26 +0,0 @@ -import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants'; -import { fetchDiffData } from '../../utils'; -import * as types from './mutation_types'; - -export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path); - -export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF); - -export const receiveDiffSuccess = ({ commit }, response) => - commit(types.RECEIVE_DIFF_SUCCESS, response); - -export const receiveDiffError = ({ commit }, response) => - commit(types.RECEIVE_DIFF_ERROR, response); - -export const fetchDiff = ({ state, rootState, dispatch }) => { - dispatch('requestDiff'); - - return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SAST) - .then((data) => { - dispatch('receiveDiffSuccess', data); - return data; - }) - .catch(() => { - dispatch('receiveDiffError'); - }); -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js deleted file mode 100644 index 1d5af1d4fe5..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - mutations, - actions, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js deleted file mode 100644 index aacec0fb679..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js +++ /dev/null @@ -1,4 +0,0 @@ -export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS'; -export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR'; -export const REQUEST_DIFF = 'REQUEST_DIFF'; -export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js deleted file mode 100644 index 11aa71d2b6b..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js +++ /dev/null @@ -1,31 +0,0 @@ -import Vue from 'vue'; -import { parseDiff } from '../../utils'; -import * as types from './mutation_types'; - -export default { - [types.SET_DIFF_ENDPOINT](state, path) { - Vue.set(state.paths, 'diffEndpoint', path); - }, - - [types.REQUEST_DIFF](state) { - state.isLoading = true; - }, - - [types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) { - const { added, fixed, existing } = parseDiff(diff, enrichData); - const baseReportOutofDate = diff.base_report_out_of_date || false; - const hasBaseReport = Boolean(diff.base_report_created_at); - - state.isLoading = false; - state.newIssues = added; - state.resolvedIssues = fixed; - state.allIssues = existing; - state.baseReportOutofDate = baseReportOutofDate; - state.hasBaseReport = hasBaseReport; - }, - - [types.RECEIVE_DIFF_ERROR](state) { - state.isLoading = false; - state.hasError = true; - }, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js deleted file mode 100644 index c1b3f546431..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js +++ /dev/null @@ -1,14 +0,0 @@ -export default () => ({ - paths: { - diffEndpoint: null, - }, - - isLoading: false, - hasError: false, - - newIssues: [], - resolvedIssues: [], - allIssues: [], - baseReportOutofDate: false, - hasBaseReport: false, -}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js deleted file mode 100644 index 13ca154bfa7..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js +++ /dev/null @@ -1,26 +0,0 @@ -import { REPORT_TYPE_SECRET_DETECTION } from '~/vue_shared/security_reports/constants'; -import { fetchDiffData } from '../../utils'; -import * as types from './mutation_types'; - -export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path); - -export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF); - -export const receiveDiffSuccess = ({ commit }, response) => - commit(types.RECEIVE_DIFF_SUCCESS, response); - -export const receiveDiffError = ({ commit }, response) => - commit(types.RECEIVE_DIFF_ERROR, response); - -export const fetchDiff = ({ state, rootState, dispatch }) => { - dispatch('requestDiff'); - - return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SECRET_DETECTION) - .then((data) => { - dispatch('receiveDiffSuccess', data); - return data; - }) - .catch(() => { - dispatch('receiveDiffError'); - }); -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js deleted file mode 100644 index 1d5af1d4fe5..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - mutations, - actions, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js deleted file mode 100644 index aacec0fb679..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js +++ /dev/null @@ -1,4 +0,0 @@ -export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS'; -export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR'; -export const REQUEST_DIFF = 'REQUEST_DIFF'; -export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js deleted file mode 100644 index ee943b0621c..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js +++ /dev/null @@ -1,30 +0,0 @@ -import { parseDiff } from '~/vue_shared/security_reports/store/utils'; -import * as types from './mutation_types'; - -export default { - [types.SET_DIFF_ENDPOINT](state, path) { - state.paths.diffEndpoint = path; - }, - - [types.REQUEST_DIFF](state) { - state.isLoading = true; - }, - - [types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) { - const { added, fixed, existing } = parseDiff(diff, enrichData); - const baseReportOutofDate = diff.base_report_out_of_date || false; - const hasBaseReport = Boolean(diff.base_report_created_at); - - state.isLoading = false; - state.newIssues = added; - state.resolvedIssues = fixed; - state.allIssues = existing; - state.baseReportOutofDate = baseReportOutofDate; - state.hasBaseReport = hasBaseReport; - }, - - [types.RECEIVE_DIFF_ERROR](state) { - state.isLoading = false; - state.hasError = true; - }, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js deleted file mode 100644 index c1b3f546431..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js +++ /dev/null @@ -1,14 +0,0 @@ -export default () => ({ - paths: { - diffEndpoint: null, - }, - - isLoading: false, - hasError: false, - - newIssues: [], - resolvedIssues: [], - allIssues: [], - baseReportOutofDate: false, - hasBaseReport: false, -}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/state.js b/app/assets/javascripts/vue_shared/security_reports/store/state.js deleted file mode 100644 index 5dc4d1ad2fb..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/state.js +++ /dev/null @@ -1,5 +0,0 @@ -import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants'; - -export default () => ({ - reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION], -}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js deleted file mode 100644 index f620bad8dba..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ /dev/null @@ -1,154 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; -import pollUntilComplete from '~/lib/utils/poll_until_complete'; -import { __, n__, sprintf } from '~/locale'; -import { CRITICAL, HIGH } from '~/vulnerabilities/constants'; -import { - FEEDBACK_TYPE_DISMISSAL, - FEEDBACK_TYPE_ISSUE, - FEEDBACK_TYPE_MERGE_REQUEST, -} from '../constants'; - -export const fetchDiffData = (state, endpoint, category) => { - const requests = [pollUntilComplete(endpoint)]; - - if (state.canReadVulnerabilityFeedback) { - requests.push(axios.get(state.vulnerabilityFeedbackPath, { params: { category } })); - } - - return Promise.all(requests).then(([diffResponse, enrichResponse]) => ({ - diff: diffResponse.data, - enrichData: enrichResponse?.data ?? [], - })); -}; - -/** - * Returns given vulnerability enriched with the corresponding - * feedback (`dismissal` or `issue` type) - * @param {Object} vulnerabilityObject - * @param {Array} feedbackList - */ -export const enrichVulnerabilityWithFeedback = (vulnerabilityObject, feedbackList = []) => { - const vulnerability = { ...vulnerabilityObject }; - // Some records may have a null `uuid`, we need to fallback to using `project_fingerprint` in those cases. Once all entries have been fixed, we can remove the fallback. - // related epic: https://gitlab.com/groups/gitlab-org/-/epics/2791 - feedbackList - .filter((fb) => - fb.finding_uuid - ? fb.finding_uuid === vulnerability.uuid - : fb.project_fingerprint === vulnerability.project_fingerprint, - ) - .forEach((feedback) => { - if (feedback.feedback_type === FEEDBACK_TYPE_DISMISSAL) { - vulnerability.isDismissed = true; - vulnerability.dismissalFeedback = feedback; - } else if (feedback.feedback_type === FEEDBACK_TYPE_ISSUE && feedback.issue_iid) { - vulnerability.hasIssue = true; - vulnerability.issue_feedback = feedback; - } else if ( - feedback.feedback_type === FEEDBACK_TYPE_MERGE_REQUEST && - feedback.merge_request_iid - ) { - vulnerability.hasMergeRequest = true; - vulnerability.merge_request_feedback = feedback; - } - }); - - return vulnerability; -}; - -/** - * Generates the added, fixed, and existing vulnerabilities from the API report. - * - * @param {Object} diff The original reports. - * @param {Object} enrichData Feedback data to add to the reports. - * @returns {Object} - */ -export const parseDiff = (diff, enrichData) => { - const enrichVulnerability = (vulnerability) => ({ - ...enrichVulnerabilityWithFeedback(vulnerability, enrichData), - category: vulnerability.report_type, - title: vulnerability.message || vulnerability.name, - }); - - return { - added: diff.added ? diff.added.map(enrichVulnerability) : [], - fixed: diff.fixed ? diff.fixed.map(enrichVulnerability) : [], - existing: diff.existing ? diff.existing.map(enrichVulnerability) : [], - }; -}; - -const createCountMessage = ({ critical, high, other, total }) => { - const otherMessage = n__('%d Other', '%d Others', other); - const countMessage = __( - '%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}', - ); - return total ? sprintf(countMessage, { critical, high, otherMessage }) : ''; -}; - -const createStatusMessage = ({ reportType, status, total }) => { - const vulnMessage = n__('vulnerability', 'vulnerabilities', total); - let message; - if (status) { - message = __('%{reportType} %{status}'); - } else if (!total) { - message = __('%{reportType} detected no new vulnerabilities.'); - } else { - message = __( - '%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}', - ); - } - return sprintf(message, { reportType, status, total, vulnMessage }); -}; - -/** - * Counts vulnerabilities. - * Returns the amount of critical, high, and other vulnerabilities. - * - * @param {Array} vulnerabilities The raw vulnerabilities to parse - * @returns {{critical: number, high: number, other: number}} - */ -export const countVulnerabilities = (vulnerabilities = []) => - vulnerabilities.reduce( - (acc, { severity }) => { - if (severity === CRITICAL) { - acc.critical += 1; - } else if (severity === HIGH) { - acc.high += 1; - } else { - acc.other += 1; - } - - return acc; - }, - { critical: 0, high: 0, other: 0 }, - ); - -/** - * Takes an object of options and returns the object with an externalized string representing - * the critical, high, and other severity vulnerabilities for a given report. - * - * The resulting string _may_ still contain sprintf-style placeholders. These - * are left in place so they can be replaced with markup, via the - * SecuritySummary component. - * @param {{reportType: string, status: string, critical: number, high: number, other: number}} options - * @returns {Object} the parameters with an externalized string - */ -export const groupedTextBuilder = ({ - reportType = '', - status = '', - critical = 0, - high = 0, - other = 0, -} = {}) => { - const total = critical + high + other; - - return { - countMessage: createCountMessage({ critical, high, other, total }), - message: createStatusMessage({ reportType, status, total }), - critical, - high, - other, - status, - total, - }; -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/utils.js b/app/assets/javascripts/vue_shared/security_reports/utils.js index 0add91c402e..aa4f6978552 100644 --- a/app/assets/javascripts/vue_shared/security_reports/utils.js +++ b/app/assets/javascripts/vue_shared/security_reports/utils.js @@ -39,13 +39,3 @@ export const extractSecurityReportArtifacts = (reportTypes, jobs) => { return acc; }, []); }; - -export const extractSecurityReportArtifactsFromPipeline = (reportTypes, data) => { - const jobs = data.project?.pipeline?.jobs?.nodes ?? []; - return extractSecurityReportArtifacts(reportTypes, jobs); -}; - -export const extractSecurityReportArtifactsFromMergeRequest = (reportTypes, data) => { - const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? []; - return extractSecurityReportArtifacts(reportTypes, jobs); -}; |