diff options
author | Luke Bennett <lukeeeebennettplus@gmail.com> | 2018-07-19 09:52:54 +0100 |
---|---|---|
committer | Luke Bennett <lukeeeebennettplus@gmail.com> | 2018-07-19 09:52:54 +0100 |
commit | fb511b66d689dd4dda0dff10759344f39e81ef8a (patch) | |
tree | 612955e40636e0623e497f8036f55b8f2de27acc /app/assets | |
parent | 820c37716b6190db6c09e1be1a836df5554db518 (diff) | |
parent | 98eccfc44c597ba14939659ca3b9150197129961 (diff) | |
download | gitlab-ce-fb511b66d689dd4dda0dff10759344f39e81ef8a.tar.gz |
Merge remote-tracking branch 'origin/master' into 45443-unable-to-save-user-profile-update-with-safari
Diffstat (limited to 'app/assets')
31 files changed, 749 insertions, 118 deletions
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index fa00a3cf386..e8c59fab609 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -53,4 +53,8 @@ export default class Autosave { return window.localStorage.removeItem(this.key); } + + dispose() { + this.field.off('input'); + } } diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 8139aa69fc7..e565af800d0 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -162,8 +162,10 @@ export default class Clusters { if (type === 'password') { this.tokenField.setAttribute('type', 'text'); + this.showTokenButton.textContent = s__('ClusterIntegration|Hide'); } else { this.tokenField.setAttribute('type', 'password'); + this.showTokenButton.textContent = s__('ClusterIntegration|Show'); } } diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index ad838a32518..0fe0007057b 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -77,7 +77,8 @@ export default { diffViewType: state => state.diffs.diffViewType, diffFiles: state => state.diffs.diffFiles, }), - ...mapGetters(['isLoggedIn', 'discussionsByLineCode']), + ...mapGetters(['isLoggedIn']), + ...mapGetters('diffs', ['discussionsByLineCode']), lineHref() { return this.lineCode ? `#${this.lineCode}` : '#'; }, @@ -189,7 +190,6 @@ export default { </button> <a v-if="lineNumber" - v-once :data-linenumber="lineNumber" :href="lineHref" > diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 32f9516d332..cbe4551d06b 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -1,17 +1,17 @@ <script> -import $ from 'jquery'; import { mapState, mapGetters, mapActions } from 'vuex'; import createFlash from '~/flash'; import { s__ } from '~/locale'; import noteForm from '../../notes/components/note_form.vue'; import { getNoteFormData } from '../store/utils'; -import Autosave from '../../autosave'; -import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants'; +import autosave from '../../notes/mixins/autosave'; +import { DIFF_NOTE_TYPE } from '../constants'; export default { components: { noteForm, }, + mixins: [autosave], props: { diffFileHash: { type: String, @@ -41,28 +41,35 @@ export default { }, mounted() { if (this.isLoggedIn) { - const noteableData = this.getNoteableData; const keys = [ - NOTE_TYPE, - this.noteableType, - noteableData.id, - noteableData.diff_head_sha, + this.noteableData.diff_head_sha, DIFF_NOTE_TYPE, - noteableData.source_project_id, + this.noteableData.source_project_id, this.line.lineCode, ]; - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys); + this.initAutoSave(this.noteableData, keys); } }, methods: { ...mapActions('diffs', ['cancelCommentForm']), ...mapActions(['saveNote', 'refetchDiscussionById']), - handleCancelCommentForm() { - this.autosave.reset(); + handleCancelCommentForm(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); + + // eslint-disable-next-line no-alert + if (!window.confirm(msg)) { + return; + } + } + this.cancelCommentForm({ lineCode: this.line.lineCode, }); + this.$nextTick(() => { + this.resetAutoSave(); + }); }, handleSaveNote(note) { const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index ca265dd892c..a6f011ff31e 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -26,13 +26,16 @@ export default { ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['discussionsByLineCode']), discussions() { return this.discussionsByLineCode[this.line.lineCode] || []; }, className() { return this.discussions.length ? '' : 'js-temp-notes-holder'; }, + hasCommentForm() { + return this.diffLineCommentForms[this.line.lineCode]; + }, }, }; </script> @@ -53,7 +56,7 @@ export default { :discussions="discussions" /> <diff-line-note-form - v-if="diffLineCommentForms[line.lineCode]" + v-if="hasCommentForm" :diff-file-hash="diffFileHash" :line="line" :note-target-line="line" diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 0197a510ef1..0e306f39a9f 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -101,7 +101,6 @@ export default { class="diff-line-num new_line" /> <td - v-once :class="line.type" class="line_content" v-html="line.richText" diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 9fd19b74cd7..8e491d293e5 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -20,8 +20,7 @@ export default { }, }, computed: { - ...mapGetters('diffs', ['commitId']), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['commitId', 'discussionsByLineCode']), ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index cc5248c25d9..05e5cafc717 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -26,7 +26,7 @@ export default { ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['discussionsByLineCode']), leftLineCode() { return this.line.left.lineCode; }, diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index ee5bb4d8d05..0031cedc68f 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -119,7 +119,6 @@ export default { class="diff-line-num old_line" /> <td - v-once :id="line.left.lineCode" :class="parallelViewLeftLineType" class="line_content parallel left-side" @@ -140,7 +139,6 @@ export default { class="diff-line-num new_line" /> <td - v-once :id="line.right.lineCode" :class="line.right.type" class="line_content parallel right-side" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 32528c9e7ab..8f8d6bbc818 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -21,8 +21,7 @@ export default { }, }, computed: { - ...mapGetters('diffs', ['commitId']), - ...mapGetters(['discussionsByLineCode']), + ...mapGetters('diffs', ['commitId', 'discussionsByLineCode']), ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 855de79adf8..d3881fa1a0a 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -1,5 +1,7 @@ import _ from 'underscore'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants'; +import { getDiffRefsByLineCode } from './utils'; export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; @@ -56,6 +58,44 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash), ) || []; +/** + * Returns an Object with discussions by their diff line code + * To avoid rendering outdated discussions on the Changes tab we should do a bunch of SHA + * comparisions. `note.position.formatter` have the current version diff refs but + * `note.original_position.formatter` will have the first version's diff refs. + * If line diff refs matches with one of them, we should render it as a discussion on Changes tab. + * + * @param {Object} diff + * @returns {Array} + */ +export const discussionsByLineCode = (state, getters, rootState, rootGetters) => { + const diffRefsByLineCode = getDiffRefsByLineCode(state.diffFiles); + + return rootGetters.discussions.reduce((acc, note) => { + const isDiffDiscussion = note.diff_discussion; + const hasLineCode = note.line_code; + const isResolvable = note.resolvable; + const diffRefs = diffRefsByLineCode[note.line_code]; + + if (isDiffDiscussion && hasLineCode && isResolvable && diffRefs) { + const refs = convertObjectPropsToCamelCase(note.position.formatter); + const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter); + + if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) { + const lineCode = note.line_code; + + if (acc[lineCode]) { + acc[lineCode].push(note); + } else { + acc[lineCode] = [note]; + } + } + } + + return acc; + }, {}); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests export const getDiffFileByHash = state => fileHash => state.diffFiles.find(file => file.fileHash === fileHash); diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index d9589baa76e..82082ac508a 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -173,3 +173,24 @@ export function trimFirstCharOfLineContent(line = {}) { return parsedLine; } + +export function getDiffRefsByLineCode(diffFiles) { + return diffFiles.reduce((acc, diffFile) => { + const { baseSha, headSha, startSha } = diffFile.diffRefs; + const { newPath, oldPath } = diffFile; + + // We can only use highlightedDiffLines to create the map of diff lines because + // highlightedDiffLines will also include every parallel diff line in it. + if (diffFile.highlightedDiffLines) { + diffFile.highlightedDiffLines.forEach(line => { + const { lineCode, oldLine, newLine } = line; + + if (lineCode) { + acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine }; + } + }); + } + + return acc; + }, {}); +} diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue index 7682b34ce4d..ff114e47741 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/button.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -38,6 +38,7 @@ export default { <button :aria-label="label" type="button" + class="btn-blank" @click.stop.prevent="clicked" > <icon diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 92fe98508ad..1e6803abf3a 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -125,6 +125,7 @@ export default { :class="flagOrientation" class="prometheus-graph-flag popover" > + <div class="arrow-shadow"></div> <div class="arrow"></div> <div class="popover-title"> <h5 v-if="deploymentFlagData"> diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 748b8cb6e6e..176f7d9eef2 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -1,7 +1,10 @@ import _ from 'underscore'; function sortMetrics(metrics) { - return _.chain(metrics).sortBy('title').sortBy('weight').value(); + return _.chain(metrics) + .sortBy('title') + .sortBy('weight') + .value(); } function normalizeMetrics(metrics) { @@ -39,7 +42,9 @@ export default class MonitoringStore { } storeEnvironmentsData(environmentsData = []) { - this.environmentsData = environmentsData; + this.environmentsData = environmentsData.filter( + environment => !!environment.latest.last_deployment, + ); } getMetricsCount() { diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 26482a02e00..abcd4422d7c 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -7,7 +7,7 @@ import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; export default { - name: 'IssueNoteForm', + name: 'NoteForm', components: { issueWarning, markdownField, diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index bee635398b3..2f1a68731c7 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -6,6 +6,7 @@ import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; import systemNote from '~/vue_shared/components/notes/system_note.vue'; +import { s__ } from '~/locale'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -144,19 +145,17 @@ export default { return this.isDiffDiscussion ? '' : 'card discussion-wrapper'; }, }, - mounted() { - if (this.isReplying) { - this.initAutoSave(this.transformedDiscussion); - } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(this.transformedDiscussion); + watch: { + isReplying() { + if (this.isReplying) { + this.$nextTick(() => { + // Pass an extra key to separate reply and note edit forms + this.initAutoSave(this.transformedDiscussion, ['Reply']); + }); } else { - this.setAutoSave(); + this.disposeAutoSave(); } - } + }, }, created() { this.resolveDiscussionsSvg = resolveDiscussionsSvg; @@ -194,16 +193,18 @@ export default { showReplyForm() { this.isReplying = true; }, - cancelReplyForm(shouldConfirm) { - if (shouldConfirm && this.$refs.noteForm.isDirty) { + cancelReplyForm(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); + // eslint-disable-next-line no-alert - if (!window.confirm('Are you sure you want to cancel creating this comment?')) { + if (!window.confirm(msg)) { return; } } - this.resetAutoSave(); this.isReplying = false; + this.resetAutoSave(); }, saveReply(noteText, form, callback) { const postData = { @@ -420,7 +421,8 @@ Please check your network connection and try again.`; :is-editing="false" save-button-title="Comment" @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" /> + @cancelForm="cancelReplyForm" + /> <note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 36cc8d5d056..4f45f912479 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -4,12 +4,18 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; export default { methods: { - initAutoSave(noteable) { - this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [ + initAutoSave(noteable, extraKeys = []) { + let keys = [ 'Note', - capitalizeFirstCharacter(noteable.noteable_type), + capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType), noteable.id, - ]); + ]; + + if (extraKeys) { + keys = keys.concat(extraKeys); + } + + this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys); }, resetAutoSave() { this.autosave.reset(); @@ -17,5 +23,8 @@ export default { setAutoSave() { this.autosave.save(); }, + disposeAutoSave() { + this.autosave.dispose(); + }, }, }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5c65e1c3bb5..e9e95dd4219 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -28,18 +28,6 @@ export const notesById = state => return acc; }, {}); -export const discussionsByLineCode = state => - state.discussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code && note.resolvable) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); - export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index c42c4a1fbe7..e7ff76c8218 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,24 +1,40 @@ <script> -/* This is a re-usable vue component for rendering a svg sprite - icon - Sample configuration: - - <icon - name="retry" - :size="32" - css-classes="top" - /> - -*/ // only allow classes in images.scss e.g. s12 const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; +let iconValidator = () => true; + +/* + During development/tests we want to validate that we are just using icons that are actually defined +*/ +if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line global-require + const data = require('@gitlab-org/gitlab-svgs/dist/icons.json'); + const { icons } = data; + iconValidator = value => { + if (icons.includes(value)) { + return true; + } + // eslint-disable-next-line no-console + console.warn(`Icon '${value}' is not a known icon of @gitlab/gitlab-svg`); + return false; + }; +} +/** This is a re-usable vue component for rendering a svg sprite icon + * @example + * <icon + * name="retry" + * :size="32" + * css-classes="top" + * /> + */ export default { props: { name: { type: String, required: true, + validator: iconValidator, }, size: { @@ -83,6 +99,6 @@ export default { :x="x" :y="y" > - <use v-bind="{ 'xlink:href':spriteHref }" /> + <use v-bind="{ 'xlink:href':spriteHref }"/> </svg> </template> diff --git a/app/assets/javascripts/vue_shared/components/reports/help_popover.vue b/app/assets/javascripts/vue_shared/components/reports/help_popover.vue new file mode 100644 index 00000000000..c5faa29fd2a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/help_popover.vue @@ -0,0 +1,48 @@ +<script> +import $ from 'jquery'; +import Icon from '~/vue_shared/components/icon.vue'; +import { inserted } from '~/feature_highlight/feature_highlight_helper'; +import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover'; + +export default { + name: 'ReportsHelpPopover', + components: { + Icon, + }, + props: { + options: { + type: Object, + required: true, + }, + }, + mounted() { + const $el = $(this.$el); + + $el + .popover({ + html: true, + trigger: 'focus', + container: 'body', + placement: 'top', + template: + '<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>', + ...this.options, + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave(300)) + .on('inserted.bs.popover', inserted) + .on('show.bs.popover', () => { + window.addEventListener('scroll', togglePopover.bind($el, false), { once: true }); + }); + }, +}; +</script> +<template> + <button + type="button" + class="btn btn-blank btn-transparent btn-help" + tabindex="0" + > + <icon name="question" /> + </button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue new file mode 100644 index 00000000000..e1e03e39ee0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue @@ -0,0 +1,99 @@ +<script> +import IssuesBlock from './report_issues.vue'; + +/** + * Renders block of issues + */ + +export default { + components: { + IssuesBlock, + }, + props: { + unresolvedIssues: { + type: Array, + required: false, + default: () => [], + }, + resolvedIssues: { + type: Array, + required: false, + default: () => [], + }, + neutralIssues: { + type: Array, + required: false, + default: () => [], + }, + allIssues: { + type: Array, + required: false, + default: () => [], + }, + type: { + type: String, + required: true, + }, + }, + data() { + return { + isFullReportVisible: false, + }; + }, + computed: { + unresolvedIssuesStatus() { + return this.type === 'license' ? 'neutral' : 'failed'; + }, + }, + methods: { + openFullReport() { + this.isFullReportVisible = true; + }, + }, +}; +</script> +<template> + <div class="report-block-container"> + + <issues-block + v-if="unresolvedIssues.length" + :type="type" + :status="unresolvedIssuesStatus" + :issues="unresolvedIssues" + class="js-mr-code-new-issues" + /> + + <issues-block + v-if="isFullReportVisible" + :type="type" + :issues="allIssues" + class="js-mr-code-all-issues" + status="failed" + /> + + <issues-block + v-if="neutralIssues.length" + :type="type" + :issues="neutralIssues" + class="js-mr-code-non-issues" + status="neutral" + /> + + <issues-block + v-if="resolvedIssues.length" + :type="type" + :issues="resolvedIssues" + class="js-mr-code-resolved-issues" + status="success" + /> + + <button + v-if="allIssues.length && !isFullReportVisible" + type="button" + class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link" + @click="openFullReport" + > + {{ s__("ciReport|Show complete code vulnerabilities report") }} + </button> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue b/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue new file mode 100644 index 00000000000..4f81cee2a38 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue @@ -0,0 +1,33 @@ +<script> +import { mapActions } from 'vuex'; + +export default { + props: { + issue: { + type: Object, + required: true, + }, + // failed || success + status: { + type: String, + required: true, + }, + }, + methods: { + ...mapActions(['openModal']), + handleIssueClick() { + const { issue, status, openModal } = this; + openModal({ issue, status }); + }, + }, +}; +</script> +<template> + <button + type="button" + class="btn-link btn-blank text-left break-link vulnerability-name-button" + @click="handleIssueClick()" + > + {{ issue.title }} + </button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue new file mode 100644 index 00000000000..ecffb02a3a0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue @@ -0,0 +1,72 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + name: 'ReportIssues', + components: { + Icon, + }, + props: { + issues: { + type: Array, + required: true, + }, + type: { + type: String, + required: true, + }, + // failed || success + status: { + type: String, + required: true, + }, + }, + computed: { + iconName() { + if (this.isStatusFailed) { + return 'status_failed_borderless'; + } else if (this.isStatusSuccess) { + return 'status_success_borderless'; + } + + return 'status_created_borderless'; + }, + isStatusFailed() { + return this.status === 'failed'; + }, + isStatusSuccess() { + return this.status === 'success'; + }, + isStatusNeutral() { + return this.status === 'neutral'; + }, + }, +}; +</script> +<template> + <div> + <ul class="report-block-list"> + <li + v-for="(issue, index) in issues" + :class="{ 'is-dismissed': issue.isDismissed }" + :key="index" + class="report-block-list-issue" + > + <div + :class="{ + failed: isStatusFailed, + success: isStatusSuccess, + neutral: isStatusNeutral, + }" + class="report-block-list-icon append-right-5" + > + <icon + :name="iconName" + :size="32" + /> + </div> + + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_link.vue b/app/assets/javascripts/vue_shared/components/reports/report_link.vue new file mode 100644 index 00000000000..74d68f9f439 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/report_link.vue @@ -0,0 +1,29 @@ +<script> +export default { + name: 'ReportIssueLink', + props: { + issue: { + type: Object, + required: true, + }, + }, +}; +</script> +<template> + <div class="report-block-list-issue-description-link"> + in + + <a + v-if="issue.urlPath" + :href="issue.urlPath" + target="_blank" + rel="noopener noreferrer nofollow" + class="break-link" + > + {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template> + </a> + <template v-else> + {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template> + </template> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/vue_shared/components/reports/report_section.vue new file mode 100644 index 00000000000..d383ed99a0c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/report_section.vue @@ -0,0 +1,192 @@ +<script> +import { __ } from '~/locale'; +import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import IssuesList from './issues_list.vue'; +import Popover from './help_popover.vue'; + +const LOADING = 'LOADING'; +const ERROR = 'ERROR'; +const SUCCESS = 'SUCCESS'; + +export default { + name: 'ReportSection', + components: { + IssuesList, + StatusIcon, + Popover, + }, + props: { + alwaysOpen: { + type: Boolean, + required: false, + default: false, + }, + type: { + type: String, + required: false, + default: '', + }, + status: { + type: String, + required: true, + }, + loadingText: { + type: String, + required: false, + default: '', + }, + errorText: { + type: String, + required: false, + default: '', + }, + successText: { + type: String, + required: true, + }, + unresolvedIssues: { + type: Array, + required: false, + default: () => [], + }, + resolvedIssues: { + type: Array, + required: false, + default: () => [], + }, + neutralIssues: { + type: Array, + required: false, + default: () => [], + }, + allIssues: { + type: Array, + required: false, + default: () => [], + }, + infoText: { + type: [String, Boolean], + required: false, + default: false, + }, + hasIssues: { + type: Boolean, + required: true, + }, + popoverOptions: { + type: Object, + default: () => ({}), + required: false, + }, + }, + + data() { + return { + isCollapsed: true, + }; + }, + + computed: { + collapseText() { + return this.isCollapsed ? __('Expand') : __('Collapse'); + }, + isLoading() { + return this.status === LOADING; + }, + loadingFailed() { + return this.status === ERROR; + }, + isSuccess() { + return this.status === SUCCESS; + }, + isCollapsible() { + return !this.alwaysOpen && this.hasIssues; + }, + isExpanded() { + return this.alwaysOpen || !this.isCollapsed; + }, + statusIconName() { + if (this.isLoading) { + return 'loading'; + } + if (this.loadingFailed || this.unresolvedIssues.length || this.neutralIssues.length) { + return 'warning'; + } + return 'success'; + }, + headerText() { + if (this.isLoading) { + return this.loadingText; + } + + if (this.isSuccess) { + return this.successText; + } + + if (this.loadingFailed) { + return this.errorText; + } + + return ''; + }, + hasPopover() { + return Object.keys(this.popoverOptions).length > 0; + }, + }, + methods: { + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + }, +}; +</script> +<template> + <section class="media-section"> + <div + class="media" + > + <status-icon + :status="statusIconName" + /> + <div + class="media-body space-children d-flex" + > + <span + class="js-code-text code-text" + > + {{ headerText }} + + <popover + v-if="hasPopover" + :options="popoverOptions" + class="prepend-left-5" + /> + </span> + + <button + v-if="isCollapsible" + type="button" + class="js-collapse-btn btn bt-default float-right btn-sm" + @click="toggleCollapsed" + > + {{ collapseText }} + </button> + </div> + </div> + + <div + v-if="hasIssues" + v-show="isExpanded" + class="js-report-section-container" + > + <slot name="body"> + <issues-list + :unresolved-issues="unresolvedIssues" + :resolved-issues="resolvedIssues" + :all-issues="allIssues" + :type="type" + /> + </slot> + </div> + </section> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue new file mode 100644 index 00000000000..997bad960e2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue @@ -0,0 +1,66 @@ +<script> +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import Popover from './help_popover.vue'; + +/** + * Renders the summary row for each report + * + * Used both in MR widget and Pipeline's view for: + * - Unit tests reports + * - Security reports + */ + +export default { + name: 'ReportSummaryRow', + components: { + CiIcon, + LoadingIcon, + Popover, + }, + props: { + summary: { + type: String, + required: true, + }, + statusIcon: { + type: String, + required: true, + }, + popoverOptions: { + type: Object, + required: true, + }, + }, + computed: { + iconStatus() { + return { + group: this.statusIcon, + icon: `status_${this.statusIcon}`, + }; + }, + }, +}; +</script> +<template> + <div class="report-block-list-issue report-block-list-issue-parent"> + <div class="report-block-list-icon append-right-10 prepend-left-5"> + <loading-icon + v-if="statusIcon === 'loading'" + css-class="report-block-list-loading-icon" + /> + <ci-icon + v-else + :status="iconStatus" + /> + </div> + + <div class="report-block-list-issue-description"> + <div class="report-block-list-issue-description-text"> + {{ summary }} + </div> + + <popover :options="popoverOptions" /> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index a8e28104a94..5ca4d944d73 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -47,7 +47,6 @@ .card-body { padding: $gl-padding; - background-color: $white-light; .form-actions { margin: -$gl-padding; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 6cfa09b56a7..6c2fdbe0608 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -297,7 +297,6 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; -$gcp-signup-offer-icon-max-width: 125px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 56beb7718a4..0f22fe21143 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -32,49 +32,23 @@ } .gcp-signup-offer { - background-color: $blue-50; - border: 1px solid $blue-300; - border-radius: $border-radius-default; + border-left-color: $blue-500; - // TODO: To be superceded by cssLab - &.alert { - padding: 24px 16px; - - &-dismissable { - padding-right: 32px; - - .close { - top: -8px; - right: -16px; - color: $blue-500; - opacity: 1; - } - } - } - - .gcp-logo { - margin-bottom: $gl-padding; - text-align: center; - } - - img { - max-width: $gcp-signup-offer-icon-max-width; + svg { + fill: $blue-500; + vertical-align: middle; } - a:not(.btn) { - color: $gl-link-color; - font-weight: normal; - text-decoration: none; - } + .gcp-signup-offer--content { + display: flex; - @include media-breakpoint-up(sm) { - > div { - display: flex; - align-items: center; + h4 { + font-size: 16px; + line-height: 24px; } - .gcp-logo { - margin: 0; + .gcp-signup-offer--icon { + align-self: flex-start; } } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3144dcc4dc0..8915b323b3c 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -293,6 +293,8 @@ .prometheus-graph-flag { display: block; min-width: 160px; + border: 0; + box-shadow: 0 1px 4px 0 $black-transparent; h5 { padding: 0; @@ -312,7 +314,6 @@ &.popover { padding: 0; - border: 1px solid $border-color; &.left { left: auto; @@ -320,12 +321,19 @@ margin-right: 10px; > .arrow { - right: -16px; + right: -14px; border-left-color: $border-color; } > .arrow::after { - border-left-color: $theme-gray-50; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 4px solid $theme-gray-50; + } + + .arrow-shadow { + right: -3px; + box-shadow: 1px 0 9px 0 $black-transparent; } } @@ -335,19 +343,35 @@ margin-left: 10px; > .arrow { - left: -16px; + left: -7px; border-right-color: $border-color; } > .arrow::after { - border-right-color: $theme-gray-50; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 4px solid $theme-gray-50; + } + + .arrow-shadow { + left: -3px; + box-shadow: 1px 0 8px 0 $black-transparent; } } > .arrow { - top: 16px; - margin-top: -8px; - border-width: 8px; + top: 10px; + margin: 0; + } + + .arrow-shadow { + content: ""; + position: absolute; + width: 7px; + height: 7px; + background-color: transparent; + transform: rotate(45deg); + top: 13px; } > .popover-title, @@ -355,10 +379,12 @@ padding: 8px; font-size: 12px; white-space: nowrap; + position: relative; } > .popover-title { background-color: $theme-gray-50; + border-radius: $border-radius-default $border-radius-default 0 0; } } |