diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 18:09:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-07 18:09:29 +0300 |
commit | f35a7a3b8e97d7af2ec1505d3fbcd6ffdd869fd2 (patch) | |
tree | 3a31002cc98598aed02c21606b21a5a123afaad2 /app/assets | |
parent | 896b68514b43b9646d763e67f63fbe8f9ef2f723 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
30 files changed, 770 insertions, 66 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue index e29835da117..f1716182e5f 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue @@ -8,10 +8,15 @@ import { GlIcon, GlNewDropdown, GlNewDropdownItem, + GlTabs, + GlTab, + GlBadge, } from '@gitlab/ui'; import { s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import getAlerts from '../graphql/queries/getAlerts.query.graphql'; +import { ALERTS_STATUS, ALERTS_STATUS_TABS } from '../constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const tdClass = 'table-col d-flex d-md-table-cell align-items-center'; @@ -59,10 +64,11 @@ export default { }, ], statuses: { - triggered: s__('AlertManagement|Triggered'), - acknowledged: s__('AlertManagement|Acknowledged'), - resolved: s__('AlertManagement|Resolved'), + [ALERTS_STATUS.TRIGGERED]: s__('AlertManagement|Triggered'), + [ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'), + [ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'), }, + statusTabs: ALERTS_STATUS_TABS, components: { GlEmptyState, GlLoadingIcon, @@ -73,7 +79,11 @@ export default { GlNewDropdown, GlNewDropdownItem, GlIcon, + GlTabs, + GlTab, + GlBadge, }, + mixins: [glFeatureFlagsMixin()], props: { projectPath: { type: String, @@ -102,6 +112,7 @@ export default { variables() { return { projectPath: this.projectPath, + status: this.statusFilter, }; }, update(data) { @@ -118,6 +129,7 @@ export default { errored: false, isAlertDismissed: false, isErrorAlertDismissed: false, + statusFilter: this.$options.statusTabs[0].status, }; }, computed: { @@ -131,6 +143,11 @@ export default { return this.$apollo.queries.alerts.loading; }, }, + methods: { + filterALertsByStatus(tabIndex) { + this.statusFilter = this.$options.statusTabs[tabIndex].status; + }, + }, }; </script> @@ -144,6 +161,17 @@ export default { {{ $options.i18n.errorMsg }} </gl-alert> + <gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterALertsByStatus"> + <gl-tab v-for="tab in $options.statusTabs" :key="tab.status"> + <template slot="title"> + <span>{{ tab.title }}</span> + <gl-badge v-if="alerts" size="sm" class="gl-tab-counter-badge"> + {{ alerts.length }} + </gl-badge> + </template> + </gl-tab> + </gl-tabs> + <h4 class="d-block d-md-none my-3"> {{ s__('AlertManagement|Alerts') }} </h4> diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js new file mode 100644 index 00000000000..c95a3c29f04 --- /dev/null +++ b/app/assets/javascripts/alert_management/constants.js @@ -0,0 +1,32 @@ +import { s__ } from '~/locale'; + +export const ALERTS_STATUS = { + OPEN: 'open', + TRIGGERED: 'triggered', + ACKNOWLEDGED: 'acknowledged', + RESOLVED: 'resolved', + ALL: 'all', +}; + +export const ALERTS_STATUS_TABS = [ + { + title: s__('AlertManagement|Open'), + status: ALERTS_STATUS.OPEN, + }, + { + title: s__('AlertManagement|Triggered'), + status: ALERTS_STATUS.TRIGGERED, + }, + { + title: s__('AlertManagement|Acknowledged'), + status: ALERTS_STATUS.ACKNOWLEDGED, + }, + { + title: s__('AlertManagement|Resolved'), + status: ALERTS_STATUS.RESOLVED, + }, + { + title: s__('AlertManagement|All alerts'), + status: ALERTS_STATUS.ALL, + }, +]; diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js index f5b96145314..7b2669691bd 100644 --- a/app/assets/javascripts/code_navigation/store/actions.js +++ b/app/assets/javascripts/code_navigation/store/actions.js @@ -30,7 +30,9 @@ export default { }); }, showBlobInteractionZones({ state }, path) { - Object.values(state.data[path]).forEach(d => addInteractionClass(path, d)); + if (state.data && state.data[path]) { + Object.values(state.data[path]).forEach(d => addInteractionClass(path, d)); + } }, showDefinition({ commit, state }, { target: el }) { let definition; diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue index 9ba69e34494..024957abe46 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue @@ -103,7 +103,13 @@ export default { class="design-discussion bordered-box position-relative" data-qa-selector="design_discussion_content" > - <design-note v-for="note in discussion.notes" :key="note.id" :note="note" /> + <design-note + v-for="note in discussion.notes" + :key="note.id" + :note="note" + :markdown-preview-path="markdownPreviewPath" + @error="$emit('updateNoteError', $event)" + /> <div class="reply-wrapper"> <reply-placeholder v-if="!isFormRendered" diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index e4bc0889a5c..f87140b23c9 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -1,20 +1,42 @@ <script> +import { ApolloMutation } from 'vue-apollo'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import DesignReplyForm from './design_reply_form.vue'; import { findNoteId } from '../../utils/design_management_utils'; +import { hasErrors } from '../../utils/cache_update'; export default { components: { UserAvatarLink, TimelineEntryItem, TimeAgoTooltip, + DesignReplyForm, + ApolloMutation, + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { note: { type: Object, required: true, }, + markdownPreviewPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + noteText: this.note.body, + isEditing: false, + }; }, computed: { author() { @@ -26,12 +48,31 @@ export default { isNoteLinked() { return this.$route.hash === `#note_${this.noteAnchorId}`; }, + mutationPayload() { + return { + id: this.note.id, + body: this.noteText, + }; + }, }, mounted() { if (this.isNoteLinked) { this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' }); } }, + methods: { + hideForm() { + this.isEditing = false; + this.noteText = this.note.body; + }, + onDone({ data }) { + this.hideForm(); + if (hasErrors(data.updateNote)) { + this.$emit('error', data.errors[0]); + } + }, + }, + updateNoteMutation, }; </script> @@ -43,26 +84,65 @@ export default { :img-alt="author.username" :img-size="40" /> - <a - v-once - :href="author.webUrl" - class="js-user-link" - :data-user-id="author.id" - :data-username="author.username" - > - <span class="note-header-author-name bold">{{ author.name }}</span> - <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> - <span class="note-headline-light">@{{ author.username }}</span> - </a> - <span class="note-headline-light note-headline-meta"> - <span class="system-note-message"> <slot></slot> </span> - <template v-if="note.createdAt"> - <span class="system-note-separator"></span> - <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`"> - <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> + <div class="d-flex justify-content-between"> + <div> + <a + v-once + :href="author.webUrl" + class="js-user-link" + :data-user-id="author.id" + :data-username="author.username" + > + <span class="note-header-author-name bold">{{ author.name }}</span> + <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> + <span class="note-headline-light">@{{ author.username }}</span> </a> - </template> - </span> - <div class="note-text md" data-qa-selector="note_content" v-html="note.bodyHtml"></div> + <span class="note-headline-light note-headline-meta"> + <span class="system-note-message"> <slot></slot> </span> + <template v-if="note.createdAt"> + <span class="system-note-separator"></span> + <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`"> + <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> + </a> + </template> + </span> + </div> + <button + v-if="!isEditing" + v-gl-tooltip + type="button" + title="Edit comment" + class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" + @click="isEditing = true" + > + <gl-icon name="pencil" class="link-highlight" /> + </button> + </div> + <div + v-if="!isEditing" + class="note-text js-note-text md" + data-qa-selector="note_content" + v-html="note.bodyHtml" + ></div> + <apollo-mutation + v-else + #default="{ mutate, loading }" + :mutation="$options.updateNoteMutation" + :variables="{ + input: mutationPayload, + }" + @error="$emit('error', $event)" + @done="onDone" + > + <design-reply-form + v-model="noteText" + :is-saving="loading" + :markdown-preview-path="markdownPreviewPath" + :is-new-comment="false" + class="mt-5" + @submitForm="mutate" + @cancelForm="hideForm" + /> + </apollo-mutation> </timeline-entry-item> </template> diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 3736b0204e3..40be9867fee 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -1,6 +1,7 @@ <script> import { GlDeprecatedButton, GlModal } from '@gitlab/ui'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { s__ } from '~/locale'; export default { name: 'DesignReplyForm', @@ -23,11 +24,42 @@ export default { type: Boolean, required: true, }, + isNewComment: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + formText: this.value, + }; }, computed: { hasValue() { return this.value.trim().length > 0; }, + modalSettings() { + if (this.isNewComment) { + return { + title: s__('DesignManagement|Cancel comment confirmation'), + okTitle: s__('DesignManagement|Discard comment'), + cancelTitle: s__('DesignManagement|Keep comment'), + content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'), + }; + } + return { + title: s__('DesignManagement|Cancel comment update confirmation'), + okTitle: s__('DesignManagement|Cancel changes'), + cancelTitle: s__('DesignManagement|Keep changes'), + content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'), + }; + }, + buttonText() { + return this.isNewComment + ? s__('DesignManagement|Comment') + : s__('DesignManagement|Save comment'); + }, }, mounted() { this.$refs.textarea.focus(); @@ -37,7 +69,7 @@ export default { if (this.hasValue) this.$emit('submitForm'); }, cancelComment() { - if (this.hasValue) { + if (this.hasValue && this.formText !== this.value) { this.$refs.cancelCommentModal.show(); } else { this.$emit('cancelForm'); @@ -85,7 +117,7 @@ export default { data-qa-selector="save_comment_button" @click="$emit('submitForm')" > - {{ __('Comment') }} + {{ buttonText }} </gl-deprecated-button> <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{ __('Cancel') @@ -94,12 +126,12 @@ export default { <gl-modal ref="cancelCommentModal" ok-variant="danger" - :title="s__('DesignManagement|Cancel comment confirmation')" - :ok-title="s__('DesignManagement|Discard comment')" - :cancel-title="s__('DesignManagement|Keep comment')" + :title="modalSettings.title" + :ok-title="modalSettings.okTitle" + :cancel-title="modalSettings.cancelTitle" modal-id="cancel-comment-modal" @ok="$emit('cancelForm')" - >{{ s__('DesignManagement|Are you sure you want to cancel creating this comment?') }} + >{{ modalSettings.content }} </gl-modal> </form> </template> diff --git a/app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql new file mode 100644 index 00000000000..d96b2f3934a --- /dev/null +++ b/app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/designNote.fragment.graphql" + +mutation updateNote($input: UpdateNoteInput!) { + updateNote(input: $input) { + note { + ...DesignNote + } + errors + } +} diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 38186bbfbf8..d51bdc4687a 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -32,6 +32,7 @@ import { UPDATE_IMAGE_DIFF_NOTE_ERROR, DESIGN_NOT_FOUND_ERROR, DESIGN_VERSION_NOT_EXIST_ERROR, + UPDATE_NOTE_ERROR, designDeletionError, } from '../../utils/error_messages'; import { DESIGNS_ROUTE_NAME } from '../../router/constants'; @@ -231,6 +232,9 @@ export default { onCreateImageDiffNoteError(e) { this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e); }, + onUpdateNoteError(e) { + this.onError(UPDATE_NOTE_ERROR, e); + }, onDesignDiscussionError(e) { this.onError(ADD_DISCUSSION_COMMENT_ERROR, e); }, @@ -329,6 +333,7 @@ export default { :discussion-index="index + 1" :markdown-preview-path="markdownPreviewPath" @error="onDesignDiscussionError" + @updateNoteError="onUpdateNoteError" /> <apollo-mutation v-if="annotationCoordinates" @@ -345,7 +350,7 @@ export default { v-model="comment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate()" + @submitForm="mutate" @cancelForm="closeCommentForm" /> </apollo-mutation> diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index a8d650e39ca..01c073bddc2 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -214,7 +214,7 @@ const onError = (data, message) => { throw new Error(data.errors); }; -const hasErrors = ({ errors = [] }) => errors?.length; +export const hasErrors = ({ errors = [] }) => errors?.length; /** * Updates a store after design deletion diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js index 5e91966931a..7666c726c2f 100644 --- a/app/assets/javascripts/design_management/utils/error_messages.js +++ b/app/assets/javascripts/design_management/utils/error_messages.js @@ -12,6 +12,8 @@ export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__( 'DesignManagement|Could not update discussion. Please try again.', ); +export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.'); + export const UPLOAD_DESIGN_ERROR = s__( 'DesignManagement|Error uploading a new design. Please try again.', ); diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js index 663d14bcfcb..020ed6dc867 100644 --- a/app/assets/javascripts/editor/editor_lite.js +++ b/app/assets/javascripts/editor/editor_lite.js @@ -1,6 +1,8 @@ import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor'; import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; +import languages from '~/ide/lib/languages'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; +import { registerLanguages } from '~/ide/utils'; import { clearDomElement } from './utils'; export default class Editor { @@ -17,6 +19,8 @@ export default class Editor { }; Editor.setupMonacoTheme(); + + registerLanguages(...languages); } static setupMonacoTheme() { diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 3aff4d30d81..25224abd77c 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -7,8 +7,10 @@ import Disposable from './common/disposable'; import ModelManager from './common/model_manager'; import editorOptions, { defaultEditorOptions } from './editor_options'; import { themes } from './themes'; +import languages from './languages'; import keymap from './keymap.json'; import { clearDomElement } from '~/editor/utils'; +import { registerLanguages } from '../utils'; function setupThemes() { themes.forEach(theme => { @@ -37,6 +39,7 @@ export default class Editor { }; setupThemes(); + registerLanguages(...languages); this.debouncedUpdate = debounce(() => { this.updateDimensions(); diff --git a/app/assets/javascripts/ide/lib/languages/index.js b/app/assets/javascripts/ide/lib/languages/index.js new file mode 100644 index 00000000000..0c85a1104fc --- /dev/null +++ b/app/assets/javascripts/ide/lib/languages/index.js @@ -0,0 +1,5 @@ +import vue from './vue'; + +const languages = [vue]; + +export default languages; diff --git a/app/assets/javascripts/ide/lib/languages/vue.js b/app/assets/javascripts/ide/lib/languages/vue.js new file mode 100644 index 00000000000..b9ff5c5d776 --- /dev/null +++ b/app/assets/javascripts/ide/lib/languages/vue.js @@ -0,0 +1,306 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md + *--------------------------------------------------------------------------------------------*/ + +// Based on handlebars template in https://github.com/microsoft/monaco-languages/blob/master/src/handlebars/handlebars.ts +// Look for "vuejs template attributes" in this file for Vue specific syntax. + +import { languages } from 'monaco-editor'; + +/* eslint-disable no-useless-escape */ +/* eslint-disable @gitlab/require-i18n-strings */ + +const EMPTY_ELEMENTS = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'menuitem', + 'meta', + 'param', + 'source', + 'track', + 'wbr', +]; + +const conf = { + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, + + comments: { + blockComment: ['{{!--', '--}}'], + }, + + brackets: [['<!--', '-->'], ['<', '>'], ['{{', '}}'], ['{', '}'], ['(', ')']], + + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + + surroundingPairs: [ + { open: '<', close: '>' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + + onEnterRules: [ + { + beforeText: new RegExp( + `<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, + 'i', + ), + afterText: /^<\/(\w[\w\d]*)\s*>$/i, + action: { indentAction: languages.IndentAction.IndentOutdent }, + }, + { + beforeText: new RegExp( + `<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, + 'i', + ), + action: { indentAction: languages.IndentAction.Indent }, + }, + ], +}; + +const language = { + defaultToken: '', + tokenPostfix: '', + // ignoreCase: true, + + // The main tokenizer for our languages + tokenizer: { + root: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.root' }], + [/<!DOCTYPE/, 'metatag.html', '@doctype'], + [/<!--/, 'comment.html', '@comment'], + [/(<)([\w]+)(\/>)/, ['delimiter.html', 'tag.html', 'delimiter.html']], + [/(<)(script)/, ['delimiter.html', { token: 'tag.html', next: '@script' }]], + [/(<)(style)/, ['delimiter.html', { token: 'tag.html', next: '@style' }]], + [/(<)([:\w]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]], + [/(<\/)([\w]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]], + [/</, 'delimiter.html'], + [/\{/, 'delimiter.html'], + [/[^<{]+/], // text + ], + + doctype: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.comment' }], + [/[^>]+/, 'metatag.content.html'], + [/>/, 'metatag.html', '@pop'], + ], + + comment: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.comment' }], + [/-->/, 'comment.html', '@pop'], + [/[^-]+/, 'comment.content.html'], + [/./, 'comment.content.html'], + ], + + otherTag: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.otherTag' }], + [/\/?>/, 'delimiter.html', '@pop'], + + // -- BEGIN vuejs template attributes + [/(v-|@|:)[\w\-\.\:\[\]]+="([^"]*)"/, 'variable'], + [/(v-|@|:)[\w\-\.\:\[\]]+='([^']*)'/, 'variable'], + + [/"([^"]*)"/, 'attribute.value'], + [/'([^']*)'/, 'attribute.value'], + + [/[\w\-\.\:\[\]]+/, 'attribute.name'], + // -- END vuejs template attributes + + [/=/, 'delimiter'], + [/[ \t\r\n]+/], // whitespace + ], + + // -- BEGIN <script> tags handling + + // After <script + script: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.script' }], + [/type/, 'attribute.name', '@scriptAfterType'], + [/"([^"]*)"/, 'attribute.value'], + [/'([^']*)'/, 'attribute.value'], + [/[\w\-]+/, 'attribute.name'], + [/=/, 'delimiter'], + [ + />/, + { + token: 'delimiter.html', + next: '@scriptEmbedded.text/javascript', + nextEmbedded: 'text/javascript', + }, + ], + [/[ \t\r\n]+/], // whitespace + [ + /(<\/)(script\s*)(>)/, + ['delimiter.html', 'tag.html', { token: 'delimiter.html', next: '@pop' }], + ], + ], + + // After <script ... type + scriptAfterType: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptAfterType' }], + [/=/, 'delimiter', '@scriptAfterTypeEquals'], + [ + />/, + { + token: 'delimiter.html', + next: '@scriptEmbedded.text/javascript', + nextEmbedded: 'text/javascript', + }, + ], // cover invalid e.g. <script type> + [/[ \t\r\n]+/], // whitespace + [/<\/script\s*>/, { token: '@rematch', next: '@pop' }], + ], + + // After <script ... type = + scriptAfterTypeEquals: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptAfterTypeEquals' }], + [/"([^"]*)"/, { token: 'attribute.value', switchTo: '@scriptWithCustomType.$1' }], + [/'([^']*)'/, { token: 'attribute.value', switchTo: '@scriptWithCustomType.$1' }], + [ + />/, + { + token: 'delimiter.html', + next: '@scriptEmbedded.text/javascript', + nextEmbedded: 'text/javascript', + }, + ], // cover invalid e.g. <script type=> + [/[ \t\r\n]+/], // whitespace + [/<\/script\s*>/, { token: '@rematch', next: '@pop' }], + ], + + // After <script ... type = $S2 + scriptWithCustomType: [ + [ + /\{\{/, + { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptWithCustomType.$S2' }, + ], + [/>/, { token: 'delimiter.html', next: '@scriptEmbedded.$S2', nextEmbedded: '$S2' }], + [/"([^"]*)"/, 'attribute.value'], + [/'([^']*)'/, 'attribute.value'], + [/[\w\-]+/, 'attribute.name'], + [/=/, 'delimiter'], + [/[ \t\r\n]+/], // whitespace + [/<\/script\s*>/, { token: '@rematch', next: '@pop' }], + ], + + scriptEmbedded: [ + [ + /\{\{/, + { + token: '@rematch', + switchTo: '@handlebarsInEmbeddedState.scriptEmbedded.$S2', + nextEmbedded: '@pop', + }, + ], + [/<\/script/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }], + ], + + // -- END <script> tags handling + + // -- BEGIN <style> tags handling + + // After <style + style: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.style' }], + [/type/, 'attribute.name', '@styleAfterType'], + [/"([^"]*)"/, 'attribute.value'], + [/'([^']*)'/, 'attribute.value'], + [/[\w\-]+/, 'attribute.name'], + [/=/, 'delimiter'], + [/>/, { token: 'delimiter.html', next: '@styleEmbedded.text/css', nextEmbedded: 'text/css' }], + [/[ \t\r\n]+/], // whitespace + [ + /(<\/)(style\s*)(>)/, + ['delimiter.html', 'tag.html', { token: 'delimiter.html', next: '@pop' }], + ], + ], + + // After <style ... type + styleAfterType: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleAfterType' }], + [/=/, 'delimiter', '@styleAfterTypeEquals'], + [/>/, { token: 'delimiter.html', next: '@styleEmbedded.text/css', nextEmbedded: 'text/css' }], // cover invalid e.g. <style type> + [/[ \t\r\n]+/], // whitespace + [/<\/style\s*>/, { token: '@rematch', next: '@pop' }], + ], + + // After <style ... type = + styleAfterTypeEquals: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleAfterTypeEquals' }], + [/"([^"]*)"/, { token: 'attribute.value', switchTo: '@styleWithCustomType.$1' }], + [/'([^']*)'/, { token: 'attribute.value', switchTo: '@styleWithCustomType.$1' }], + [/>/, { token: 'delimiter.html', next: '@styleEmbedded.text/css', nextEmbedded: 'text/css' }], // cover invalid e.g. <style type=> + [/[ \t\r\n]+/], // whitespace + [/<\/style\s*>/, { token: '@rematch', next: '@pop' }], + ], + + // After <style ... type = $S2 + styleWithCustomType: [ + [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleWithCustomType.$S2' }], + [/>/, { token: 'delimiter.html', next: '@styleEmbedded.$S2', nextEmbedded: '$S2' }], + [/"([^"]*)"/, 'attribute.value'], + [/'([^']*)'/, 'attribute.value'], + [/[\w\-]+/, 'attribute.name'], + [/=/, 'delimiter'], + [/[ \t\r\n]+/], // whitespace + [/<\/style\s*>/, { token: '@rematch', next: '@pop' }], + ], + + styleEmbedded: [ + [ + /\{\{/, + { + token: '@rematch', + switchTo: '@handlebarsInEmbeddedState.styleEmbedded.$S2', + nextEmbedded: '@pop', + }, + ], + [/<\/style/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }], + ], + + // -- END <style> tags handling + + handlebarsInSimpleState: [ + [/\{\{\{?/, 'delimiter.handlebars'], + [/\}\}\}?/, { token: 'delimiter.handlebars', switchTo: '@$S2.$S3' }], + { include: 'handlebarsRoot' }, + ], + + handlebarsInEmbeddedState: [ + [/\{\{\{?/, 'delimiter.handlebars'], + [/\}\}\}?/, { token: 'delimiter.handlebars', switchTo: '@$S2.$S3', nextEmbedded: '$S3' }], + { include: 'handlebarsRoot' }, + ], + + handlebarsRoot: [ + [/"[^"]*"/, 'string.handlebars'], + [/[#/][^\s}]+/, 'keyword.helper.handlebars'], + [/else\b/, 'keyword.helper.handlebars'], + [/[\s]+/], + [/[^}]/, 'variable.parameter.handlebars'], + ], + }, +}; + +export default { + id: 'vue', + extensions: ['.vue'], + aliases: ['Vue', 'vue'], + mimetypes: ['text/x-vue-template'], + conf, + language, +}; diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 64ac539a4ff..e7615be498b 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -68,3 +68,13 @@ export const createPathWithExt = p => { return `${p.substring(1, p.lastIndexOf('.') + 1 || p.length)}${ext || '.js'}`; }; + +export function registerLanguages(def, ...defs) { + if (defs.length) defs.forEach(lang => registerLanguages(lang)); + + const languageId = def.id; + + languages.register(def); + languages.setMonarchTokensProvider(languageId, def.language); + languages.setLanguageConfiguration(languageId, def.conf); +} diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 67835a5e356..f20674699f7 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,5 +1,5 @@ <script> -import { debounce, pickBy } from 'lodash'; +import { debounce } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import VueDraggable from 'vuedraggable'; import { @@ -32,7 +32,13 @@ import GroupEmptyState from './group_empty_state.vue'; import DashboardsDropdown from './dashboards_dropdown.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; -import { getAddMetricTrackingOptions, timeRangeToUrl, timeRangeFromUrl } from '../utils'; +import { + getAddMetricTrackingOptions, + timeRangeToUrl, + timeRangeFromUrl, + panelToUrl, + expandedPanelPayloadFromUrl, +} from '../utils'; import { metricStates } from '../constants'; import { defaultTimeRange, timeRanges } from '~/vue_shared/constants'; @@ -238,6 +244,23 @@ export default { return !this.environmentsLoading && this.filteredEnvironments.length === 0; }, }, + watch: { + dashboard(newDashboard) { + try { + const expandedPanel = expandedPanelPayloadFromUrl(newDashboard); + if (expandedPanel) { + this.setExpandedPanel(expandedPanel); + } + } catch { + createFlash( + s__( + 'Metrics|Link contains invalid chart information, please verify the link to see the expanded panel.', + ), + ); + } + }, + }, + created() { this.setInitialState({ metricsEndpoint: this.metricsEndpoint, @@ -299,15 +322,9 @@ export default { // As a fallback, switch to default time range instead this.selectedTimeRange = defaultTimeRange; }, - - generatePanelLink(group, graphData) { - if (!group || !graphData) { - return null; - } - const dashboard = this.currentDashboard || this.firstDashboard.path; - const { y_label, title } = graphData; - const params = pickBy({ dashboard, group, title, y_label }, value => value != null); - return mergeUrlParams(params, window.location.href); + generatePanelUrl(groupKey, panel) { + const dashboardPath = this.currentDashboard || this.firstDashboard.path; + return panelToUrl(dashboardPath, groupKey, panel); }, hideAddMetricModal() { this.$refs.addMetricModal.hide(); @@ -564,7 +581,7 @@ export default { v-show="expandedPanel.panel" ref="expandedPanel" :settings-path="settingsPath" - :clipboard-text="generatePanelLink(expandedPanel.group, expandedPanel.panel)" + :clipboard-text="generatePanelUrl(expandedPanel.group, expandedPanel.panel)" :graph-data="expandedPanel.panel" :alerts-endpoint="alertsEndpoint" :height="600" @@ -623,7 +640,7 @@ export default { <dashboard-panel :settings-path="settingsPath" - :clipboard-text="generatePanelLink(groupData.group, graphData)" + :clipboard-text="generatePanelUrl(groupData.group, graphData)" :graph-data="graphData" :alerts-endpoint="alertsEndpoint" :prometheus-alerts-available="prometheusAlertsAvailable" diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 717f4cd9d66..1db65f5e960 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -102,6 +102,13 @@ export const clearExpandedPanel = ({ commit }) => { // All Data +/** + * Fetch all dashboard data. + * + * @param {Object} store + * @returns A promise that resolves when the dashboard + * skeleton has been loaded. + */ export const fetchData = ({ dispatch }) => { dispatch('fetchEnvironmentsData'); dispatch('fetchDashboard'); diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 7c6cd19eb7b..4f90294ee3a 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -1,3 +1,4 @@ +import { pickBy } from 'lodash'; import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; import { timeRangeParamNames, @@ -28,7 +29,6 @@ export const graphDataValidatorForValues = (isValues, graphData) => { ); }; -/* eslint-disable @gitlab/require-i18n-strings */ /** * Checks that element that triggered event is located on cluster health check dashboard * @param {HTMLElement} element to check against @@ -36,6 +36,7 @@ export const graphDataValidatorForValues = (isValues, graphData) => { */ const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(':clusters:show'); +/* eslint-disable @gitlab/require-i18n-strings */ /** * Tracks snowplow event when user generates link to metric chart * @param {String} chart link that will be sent as a property for the event @@ -71,6 +72,7 @@ export const downloadCSVOptions = title => { return { category, action, label: 'Chart title', property: title }; }; +/* eslint-enable @gitlab/require-i18n-strings */ /** * Generate options for snowplow to track adding a new metric via the dashboard @@ -133,6 +135,68 @@ export const timeRangeToUrl = (timeRange, url = window.location.href) => { }; /** + * Locates a panel (and its corresponding group) given a (URL) search query. Returns + * it as payload for the store to set the right expandaded panel. + * + * Params used to locate a panel are: + * - group: Group identifier + * - title: Panel title + * - y_label: Panel y_label + * + * @param {Object} dashboard - Dashboard reference from the Vuex store + * @param {String} search - URL location search query + * @returns {Object} payload - Payload for expanded panel to be displayed + * @returns {String} payload.group - Group where panel is located + * @returns {Object} payload.panel - Dashboard panel (graphData) reference + * @throws Will throw an error if Panel cannot be located. + */ +export const expandedPanelPayloadFromUrl = (dashboard, search = window.location.search) => { + const params = queryToObject(search); + + // Search for the panel if any of the search params is identified + if (params.group || params.title || params.y_label) { + const panelGroup = dashboard.panelGroups.find(({ group }) => params.group === group); + const panel = panelGroup.panels.find( + // eslint-disable-next-line babel/camelcase + ({ y_label, title }) => y_label === params.y_label && title === params.title, + ); + + if (!panel) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Panel could no found by URL parameters.'); + } + return { group: panelGroup.group, panel }; + } + return null; +}; + +/** + * Convert panel information to a URL for the user to + * bookmark or share highlighting a specific panel. + * + * @param {String} dashboardPath - Dashboard path used as identifier + * @param {String} group - Group Identifier + * @param {?Object} panel - Panel object from the dashboard + * @param {?String} url - Base URL including current search params + * @returns Dashboard URL which expands a panel (chart) + */ +export const panelToUrl = (dashboardPath, group, panel, url = window.location.href) => { + if (!group || !panel) { + return null; + } + const params = pickBy( + { + dashboard: dashboardPath, + group, + title: panel.title, + y_label: panel.y_label, + }, + value => value != null, + ); + return mergeUrlParams(params, url); +}; + +/** * Get the metric value from first data point. * Currently only used for bar charts * diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue new file mode 100644 index 00000000000..72afcc30be6 --- /dev/null +++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue @@ -0,0 +1,21 @@ +<script> +import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue'; + +export default { + components: { + MarkdownFieldView, + }, + props: { + description: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> +<template> + <markdown-field-view class="snippet-description" data-qa-selector="snippet_description_field"> + <div class="md js-snippet-description" v-html="description"></div> + </markdown-field-view> +</template> diff --git a/app/assets/javascripts/snippets/components/snippet_title.vue b/app/assets/javascripts/snippets/components/snippet_title.vue index 06484ad5110..5267c3748ca 100644 --- a/app/assets/javascripts/snippets/components/snippet_title.vue +++ b/app/assets/javascripts/snippets/components/snippet_title.vue @@ -1,11 +1,14 @@ <script> -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { GlSprintf } from '@gitlab/ui'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import SnippetDescription from './snippet_description_view.vue'; + export default { components: { TimeAgoTooltip, GlSprintf, + SnippetDescription, }, props: { snippet: { @@ -20,13 +23,8 @@ export default { <h2 class="snippet-title prepend-top-0 mb-3" data-qa-selector="snippet_title"> {{ snippet.title }} </h2> - <div - v-if="snippet.description" - class="description" - data-qa-selector="snippet_description_field" - > - <div class="md js-snippet-description" v-html="snippet.descriptionHtml"></div> - </div> + + <snippet-description v-if="snippet.description" :description="snippet.descriptionHtml" /> <small v-if="snippet.updatedAt !== snippet.createdAt" class="edited-text"> <gl-sprintf :message="__('Edited %{timeago}')"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 42db1935123..6df53311ef0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -42,6 +42,10 @@ export default { type: String, required: false, }, + pipelineMustSucceed: { + type: Boolean, + required: false, + }, sourceBranchLink: { type: String, required: false, @@ -60,7 +64,10 @@ export default { return this.pipeline && Object.keys(this.pipeline).length > 0; }, hasCIError() { - return this.hasCi && !this.ciStatus; + return (this.hasCi && !this.ciStatus) || this.hasPipelineMustSucceedConflict; + }, + hasPipelineMustSucceedConflict() { + return !this.hasCi && this.pipelineMustSucceed; }, status() { return this.pipeline.details && this.pipeline.details.status @@ -76,9 +83,13 @@ export default { return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; }, errorText() { + if (this.hasPipelineMustSucceedConflict) { + return s__('Pipeline|No pipeline has been run for this commit.'); + } + return sprintf( s__( - 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}', + 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.', ), { linkStart: `<a href="${this.troubleshootingDocsPath}">`, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 46210d810bc..8fba0e2981f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -79,6 +79,7 @@ export default { :pipeline-coverage-delta="mr.pipelineCoverageDelta" :ci-status="mr.ciStatus" :has-ci="mr.hasCI" + :pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds" :source-branch="branch" :source-branch-link="branchLink" :troubleshooting-docs-path="mr.troubleshootingDocsPath" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 360a75c3946..82be5eeb5ff 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -1,6 +1,6 @@ <script> import { isEmpty } from 'lodash'; -import { GlIcon, GlDeprecatedButton } from '@gitlab/ui'; +import { GlIcon, GlDeprecatedButton, GlSprintf, GlLink } from '@gitlab/ui'; import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; @@ -26,6 +26,8 @@ export default { CommitEdit, CommitMessageDropdown, GlIcon, + GlSprintf, + GlLink, GlDeprecatedButton, MergeImmediatelyConfirmationDialog: () => import( @@ -56,7 +58,7 @@ export default { status() { const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr; - if (hasCI && !ciStatus) { + if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) { return 'failed'; } else if (this.isAutoMergeAvailable) { return 'pending'; @@ -97,6 +99,9 @@ export default { return __('Merge'); }, + hasPipelineMustSucceedConflict() { + return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds; + }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; }, @@ -343,9 +348,19 @@ export default { /> </template> <template v-else> - <span class="bold js-resolve-mr-widget-items-message"> - {{ mergeDisabledText }} - </span> + <div class="bold js-resolve-mr-widget-items-message"> + <gl-sprintf + v-if="hasPipelineMustSucceedConflict" + :message="pipelineMustSucceedConflictText" + > + <template #link="{ content }"> + <gl-link :href="mr.pipelineMustSucceedDocsPath" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="mergeDisabledText" /> + </div> </template> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index 32a2b7b83f4..39fa5e465b8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -1,6 +1,9 @@ import { __ } from '~/locale'; export const MERGE_DISABLED_TEXT = __('You can only merge once the items above are resolved.'); +export const PIPELINE_MUST_SUCCEED_CONFLICT_TEXT = __( + 'Pipelines must succeed for merge requests to be eligible to merge. Please enable pipelines for this project to continue. For more information, see the %{linkStart}documentation.%{linkEnd}', +); export default { computed: { @@ -16,6 +19,9 @@ export default { mergeDisabledText() { return MERGE_DISABLED_TEXT; }, + pipelineMustSucceedConflictText() { + return PIPELINE_MUST_SUCCEED_CONFLICT_TEXT; + }, autoMergeText() { // MWPS is currently the only auto merge strategy available in CE return __('Merge when pipeline succeeds'); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 8c45e613312..2396ceab6e8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -104,8 +104,11 @@ export default { shouldRenderMergeHelp() { return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; }, + hasPipelineMustSucceedConflict() { + return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds; + }, shouldRenderPipelines() { - return this.mr.hasCI; + return this.mr.hasCI || this.hasPipelineMustSucceedConflict; }, shouldSuggestPipelines() { return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath; @@ -432,7 +435,9 @@ export default { <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> </div> </div> - <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div> + <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"> + <mr-widget-merge-help /> + </div> </div> <mr-widget-pipeline-container v-if="shouldRenderMergedPipeline" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 5a985426abc..b08218732f6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -161,6 +161,7 @@ export default class MergeRequestStore { // Paths are set on the first load of the page and not auto-refreshed this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path; this.troubleshootingDocsPath = data.troubleshooting_docs_path; + this.pipelineMustSucceedDocsPath = data.pipeline_must_succeed_docs_path; this.mergeRequestBasicPath = data.merge_request_basic_path; this.mergeRequestWidgetPath = data.merge_request_widget_path; this.mergeRequestCachedWidgetPath = data.merge_request_cached_widget_path; 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 afbfb1e0ee2..52ce05f0d99 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 @@ -1,8 +1,12 @@ <script> +import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue'; import ViewerMixin from './mixins'; import { handleBlobRichViewer } from '~/blob/viewer'; export default { + components: { + MarkdownFieldView, + }, mixins: [ViewerMixin], mounted() { handleBlobRichViewer(this.$refs.content, this.type); @@ -10,5 +14,5 @@ export default { }; </script> <template> - <div ref="content" v-html="content"></div> + <markdown-field-view ref="content" v-html="content" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue new file mode 100644 index 00000000000..d77123371f2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue @@ -0,0 +1,19 @@ +<script> +import $ from 'jquery'; +import '~/behaviors/markdown/render_gfm'; + +export default { + mounted() { + this.renderGFM(); + }, + methods: { + renderGFM() { + $(this.$el).renderGFM(); + }, + }, +}; +</script> + +<template> + <div><slot></slot></div> +</template> diff --git a/app/assets/stylesheets/page_bundles/themes/_dark.scss b/app/assets/stylesheets/page_bundles/themes/_dark.scss index 634f18ee1bd..30822eb7de4 100644 --- a/app/assets/stylesheets/page_bundles/themes/_dark.scss +++ b/app/assets/stylesheets/page_bundles/themes/_dark.scss @@ -94,7 +94,7 @@ } .ide-pipeline svg { - --svg-status-bg: transparent; + --svg-status-bg: $background; } .multi-file-tab-close:hover { diff --git a/app/assets/stylesheets/pages/alerts_list.scss b/app/assets/stylesheets/pages/alerts_list.scss index 0d0db0ea6fe..5974f97b728 100644 --- a/app/assets/stylesheets/pages/alerts_list.scss +++ b/app/assets/stylesheets/pages/alerts_list.scss @@ -74,4 +74,14 @@ } } } + + .gl-tab-nav-item { + color: $gl-gray-600; + + > .gl-tab-counter-badge { + color: inherit; + @include gl-font-sm; + background-color: $white-normal; + } + } } |