diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-18 21:09:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-18 21:09:08 +0300 |
commit | 7ea5ca0bb5aa9792c514a22d59217dffa3800581 (patch) | |
tree | 753d90cbdb990d5b4889990fe7e8534d030480b3 /app/assets/javascripts/vue_shared | |
parent | e26bf16ed06dd7fc959961cfe16621c19f0e6acf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared')
7 files changed, 193 insertions, 18 deletions
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 7a687ea4ad0..6d5912df96b 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable vue/no-v-html */ import { groupBy } from 'lodash'; -import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { glEmojiTag } from '../../emoji'; import { __, sprintf } from '~/locale'; @@ -10,8 +10,8 @@ const NO_USER_ID = -1; export default { components: { + GlButton, GlIcon, - GlLoadingIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -64,7 +64,7 @@ export default { methods: { getAwardClassBindings(awardList) { return { - active: this.hasReactionByCurrentUser(awardList), + selected: this.hasReactionByCurrentUser(awardList), disabled: this.currentUserId === NO_USER_ID, }; }, @@ -150,40 +150,39 @@ export default { <template> <div class="awards js-awards-block"> - <button + <gl-button v-for="awardList in groupedAwards" :key="awardList.name" v-gl-tooltip.viewport + class="gl-mr-3" :class="awardList.classes" :title="awardList.title" data-testid="award-button" - class="btn award-control" - type="button" @click="handleAward(awardList.name)" > - <span data-testid="award-html" v-html="awardList.html"></span> - <span class="award-control-text js-counter">{{ awardList.list.length }}</span> - </button> + <template #emoji> + <span class="award-emoji-block" data-testid="award-html" v-html="awardList.html"></span> + </template> + <span class="js-counter">{{ awardList.list.length }}</span> + </gl-button> <div v-if="canAwardEmoji" class="award-menu-holder"> - <button + <gl-button v-gl-tooltip.viewport :class="addButtonClass" - class="award-control btn js-add-award" + class="add-reaction-button js-add-award" title="Add reaction" :aria-label="__('Add reaction')" - type="button" > - <span class="award-control-icon award-control-icon-neutral"> + <span class="reaction-control-icon reaction-control-icon-neutral"> <gl-icon aria-hidden="true" name="slight-smile" /> </span> - <span class="award-control-icon award-control-icon-positive"> + <span class="reaction-control-icon reaction-control-icon-positive"> <gl-icon aria-hidden="true" name="smiley" /> </span> - <span class="award-control-icon award-control-icon-super-positive"> - <gl-icon aria-hidden="true" name="smiley" /> + <span class="reaction-control-icon reaction-control-icon-super-positive"> + <gl-icon aria-hidden="true" name="smile" /> </span> - <gl-loading-icon size="md" color="dark" class="award-control-icon-loading" /> - </button> + </gl-button> </div> </div> </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 new file mode 100644 index 00000000000..6aeab56eea2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/constants.js @@ -0,0 +1,7 @@ +/** + * 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 new file mode 100644 index 00000000000..1e5a60c32fd --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js @@ -0,0 +1,66 @@ +import { s__, sprintf } from '~/locale'; +import { countVulnerabilities, groupedTextBuilder } from './utils'; +import { LOADING, ERROR, SUCCESS } from '~/reports/constants'; +import { TRANSLATION_IS_LOADING } from './messages'; + +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 new file mode 100644 index 00000000000..10705e04a21 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/index.js @@ -0,0 +1,16 @@ +import Vuex from 'vuex'; +import * as getters from './getters'; +import state from './state'; +import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants'; +import sast from './modules/sast'; +import secretDetection from './modules/secret_detection'; + +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 new file mode 100644 index 00000000000..c25e252a768 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/messages.js @@ -0,0 +1,4 @@ +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/state.js b/app/assets/javascripts/vue_shared/security_reports/store/state.js new file mode 100644 index 00000000000..5dc4d1ad2fb --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/state.js @@ -0,0 +1,5 @@ +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 index 6e50efae741..c5e786c92b1 100644 --- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -1,5 +1,7 @@ import pollUntilComplete from '~/lib/utils/poll_until_complete'; import axios from '~/lib/utils/axios_utils'; +import { __, n__, sprintf } from '~/locale'; +import { CRITICAL, HIGH } from '~/vulnerabilities/constants'; import { FEEDBACK_TYPE_DISMISSAL, FEEDBACK_TYPE_ISSUE, @@ -73,3 +75,79 @@ export const parseDiff = (diff, enrichData) => { 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 %{totalStart}no%{totalEnd} 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, + }; +}; |