From e06d0e779673d745972863302858105aad9032e5 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 25 Feb 2020 15:08:50 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- app/assets/javascripts/boards/components/board.js | 11 +- .../javascripts/boards/components/board_sidebar.js | 8 +- .../javascripts/lib/utils/datetime_utility.js | 10 +- app/assets/javascripts/lib/utils/highlight.js | 7 +- app/assets/javascripts/lib/utils/text_utility.js | 4 +- .../lib/utils/unit_format/formatter_factory.js | 119 ++++++++++++ .../javascripts/lib/utils/unit_format/index.js | 167 +++++++++++++++++ .../monitoring/components/charts/time_series.vue | 9 +- .../notes/components/discussion_resolve_button.vue | 12 +- app/assets/stylesheets/framework/modal.scss | 10 +- app/assets/stylesheets/framework/spinner.scss | 3 +- app/assets/stylesheets/pages/issuable.scss | 39 +++- app/assets/stylesheets/pages/labels.scss | 27 ++- app/assets/stylesheets/pages/milestone.scss | 16 +- app/assets/stylesheets/pages/notes.scss | 12 +- app/helpers/labels_helper.rb | 82 +++++++-- app/models/clusters/cluster.rb | 18 +- .../issuable/common_system_notes_service.rb | 2 +- .../resource_events/change_milestone_service.rb | 2 +- app/views/projects/issues/_issue.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- app/views/shared/_delete_label_modal.html.haml | 2 +- .../shared/boards/components/_board.html.haml | 13 +- .../boards/components/sidebar/_labels.html.haml | 15 +- app/views/shared/milestones/_issuable.html.haml | 2 +- app/views/shared/milestones/_labels_tab.html.haml | 8 +- .../unreleased/196648-replace-_-with-lodash.yml | 5 + ...4-replace-labels-in-haml-with-gitlab-ui-css.yml | 5 + ...p-assets-javascripts-notes-components-discu.yml | 5 + lib/banzai/filter/label_reference_filter.rb | 17 +- lib/banzai/filter/reference_filter.rb | 3 +- lib/gitlab/markdown_cache.rb | 2 +- locale/gitlab.pot | 12 +- spec/features/boards/sidebar_spec.rb | 10 +- spec/features/container_registry_spec.rb | 75 -------- spec/features/groups/container_registry_spec.rb | 93 ++++++++++ spec/features/issuables/issuable_list_spec.rb | 8 +- .../issues/filtered_search/filter_issues_spec.rb | 2 +- spec/features/labels_hierarchy_spec.rb | 6 +- spec/features/projects/container_registry_spec.rb | 161 +++++++++++++++++ .../utils/unit_format/formatter_factory_spec.js | 200 +++++++++++++++++++++ spec/frontend/lib/utils/unit_format/index_spec.js | 117 ++++++++++++ spec/helpers/labels_helper_spec.rb | 12 +- spec/helpers/markup_helper_spec.rb | 4 +- .../banzai/filter/label_reference_filter_spec.rb | 26 +-- .../change_milestone_service_spec.rb | 62 +------ .../change_milestone_service_shared_examples.rb | 46 +++++ 47 files changed, 1199 insertions(+), 274 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/unit_format/formatter_factory.js create mode 100644 app/assets/javascripts/lib/utils/unit_format/index.js create mode 100644 changelogs/unreleased/196648-replace-_-with-lodash.yml create mode 100644 changelogs/unreleased/38144-replace-labels-in-haml-with-gitlab-ui-css.yml create mode 100644 changelogs/unreleased/Resolve-Migrate--fa-spinner-app-assets-javascripts-notes-components-discu.yml delete mode 100644 spec/features/container_registry_spec.rb create mode 100644 spec/features/groups/container_registry_spec.rb create mode 100644 spec/features/projects/container_registry_spec.rb create mode 100644 spec/frontend/lib/utils/unit_format/formatter_factory_spec.js create mode 100644 spec/frontend/lib/utils/unit_format/index_spec.js create mode 100644 spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index a6deb656b37..67046715e9b 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import Sortable from 'sortablejs'; import Vue from 'vue'; -import { GlButtonGroup, GlButton, GlTooltip } from '@gitlab/ui'; +import { GlButtonGroup, GlButton, GlLabel, GlTooltip } from '@gitlab/ui'; import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import { s__, __, sprintf } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -14,6 +14,7 @@ import IssueCount from './issue_count.vue'; import boardsStore from '../stores/boards_store'; import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; import { ListType } from '../constants'; +import { isScopedLabel } from '~/lib/utils/common_utils'; export default Vue.extend({ components: { @@ -24,6 +25,7 @@ export default Vue.extend({ GlButtonGroup, IssueCount, GlButton, + GlLabel, GlTooltip, }, directives: { @@ -95,6 +97,9 @@ export default Vue.extend({ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; }, + helpLink() { + return boardsStore.scopedLabels.helpLink; + }, }, watch: { filter: { @@ -145,6 +150,10 @@ export default Vue.extend({ } }, methods: { + showScopedLabels(label) { + return boardsStore.scopedLabels.enabled && isScopedLabel(label); + }, + showNewIssueForm() { this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; }, diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index ba1fe9202fc..9b67126bee2 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Vue from 'vue'; +import { GlLabel } from '@gitlab/ui'; import Flash from '~/flash'; import { sprintf, __ } from '~/locale'; import Sidebar from '~/right_sidebar'; @@ -22,6 +23,7 @@ export default Vue.extend({ components: { AssigneeTitle, Assignees, + GlLabel, SidebarEpicsSelect: () => import('ee_component/sidebar/components/sidebar_item_epics_select.vue'), RemoveBtn, @@ -67,6 +69,9 @@ export default Vue.extend({ selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; }, + helpLink() { + return boardsStore.scopedLabels.helpLink; + }, }, watch: { detail: { @@ -147,8 +152,5 @@ export default Vue.extend({ showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, - helpLink() { - return boardsStore.scopedLabels.helpLink; - }, }, }); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index ad8095e1ae3..a70bab013c6 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; +import { isString, mapValues, isNumber, reduce } from 'lodash'; import * as timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { languageCode, s__, __, n__ } from '../../locale'; @@ -79,7 +79,7 @@ export const getDayName = date => * @returns {String} */ export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => { - if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { + if (isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { throw new Error(__('Invalid date')); } return dateFormat(datetime, format); @@ -497,7 +497,7 @@ export const parseSeconds = ( let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE); - return _.mapObject(timePeriodConstraints, minutesPerPeriod => { + return mapValues(timePeriodConstraints, minutesPerPeriod => { if (minutesPerPeriod === 0) { return 0; } @@ -516,7 +516,7 @@ export const parseSeconds = ( * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days' */ export const stringifyTime = (timeObject, fullNameFormat = false) => { - const reducedTime = _.reduce( + const reducedTime = reduce( timeObject, (memo, unitValue, unitName) => { const isNonZero = Boolean(unitValue); @@ -642,7 +642,7 @@ export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + * @return {String} approximated time */ export const approximateDuration = (seconds = 0) => { - if (!_.isNumber(seconds) || seconds < 0) { + if (!isNumber(seconds) || seconds < 0) { return ''; } diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js index 8f0afa3467d..b1dd562f63a 100644 --- a/app/assets/javascripts/lib/utils/highlight.js +++ b/app/assets/javascripts/lib/utils/highlight.js @@ -1,5 +1,4 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import _ from 'underscore'; import sanitize from 'sanitize-html'; /** @@ -17,11 +16,11 @@ import sanitize from 'sanitize-html'; * @param {String} matchSuffix The string to insert at the end of a match */ export default function highlight(string, match = '', matchPrefix = '', matchSuffix = '') { - if (_.isUndefined(string) || _.isNull(string)) { + if (!string) { return ''; } - if (_.isUndefined(match) || _.isNull(match) || match === '') { + if (!match) { return string; } @@ -34,7 +33,7 @@ export default function highlight(string, match = '', matchPrefix = '', match return sanitizedValue .split('') .map((character, i) => { - if (_.contains(occurrences, i)) { + if (occurrences.includes(i)) { return `${matchPrefix}${character}${matchSuffix}`; } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index a03fedcd7e7..9ed286826cc 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { isString } from 'lodash'; /** * Adds a , to a string composed by numbers, at every 3 chars. @@ -199,7 +199,7 @@ export const splitCamelCase = string => * i.e. "My Group / My Subgroup / My Project" */ export const truncateNamespace = (string = '') => { - if (_.isNull(string) || !_.isString(string)) { + if (string === null || !isString(string)) { return ''; } diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js new file mode 100644 index 00000000000..432a9254558 --- /dev/null +++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js @@ -0,0 +1,119 @@ +/** + * Formats a number as string using `toLocaleString`. + * + * @param {Number} number to be converted + * @param {params} Parameters + * @param {params.fractionDigits} Number of decimal digits + * to display, defaults to using `toLocaleString` defaults. + * @param {params.maxLength} Max output char lenght at the + * expense of precision, if the output is longer than this, + * the formatter switches to using exponential notation. + * @param {params.factor} Value is multiplied by this factor, + * useful for value normalization. + * @returns Formatted value + */ +function formatNumber( + value, + { fractionDigits = undefined, valueFactor = 1, style = undefined, maxLength = undefined }, +) { + if (value === null) { + return ''; + } + + const num = value * valueFactor; + const formatted = num.toLocaleString(undefined, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + style, + }); + + if (maxLength !== undefined && formatted.length > maxLength) { + // 123456 becomes 1.23e+8 + return num.toExponential(2); + } + return formatted; +} + +/** + * Formats a number as a string scaling it up according to units. + * + * While the number is scaled down, the units are scaled up. + * + * @param {Array} List of units of the scale + * @param {Number} unitFactor - Factor of the scale for each + * unit after which the next unit is used scaled. + */ +const scaledFormatter = (units, unitFactor = 1000) => { + if (unitFactor === 0) { + return new RangeError(`unitFactor cannot have the value 0.`); + } + + return (value, fractionDigits) => { + if (value === null) { + return ''; + } + if ( + value === Number.NEGATIVE_INFINITY || + value === Number.POSITIVE_INFINITY || + Number.isNaN(value) + ) { + return value.toLocaleString(undefined); + } + + let num = value; + let scale = 0; + const limit = units.length; + + while (Math.abs(num) >= unitFactor) { + scale += 1; + num /= unitFactor; + + if (scale >= limit) { + return 'NA'; + } + } + + const unit = units[scale]; + + return `${formatNumber(num, { fractionDigits })}${unit}`; + }; +}; + +/** + * Returns a function that formats a number as a string. + */ +export const numberFormatter = (style = 'decimal', valueFactor = 1) => { + return (value, fractionDigits, maxLength) => { + return `${formatNumber(value, { fractionDigits, maxLength, valueFactor, style })}`; + }; +}; + +/** + * Returns a function that formats a number as a string with a suffix. + */ +export const suffixFormatter = (unit = '', valueFactor = 1) => { + return (value, fractionDigits, maxLength) => { + const length = maxLength !== undefined ? maxLength - unit.length : undefined; + return `${formatNumber(value, { fractionDigits, maxLength: length, valueFactor })}${unit}`; + }; +}; + +/** + * Returns a function that formats a number scaled using SI units notation. + */ +export const scaledSIFormatter = (unit = '', prefixOffset = 0) => { + const fractional = ['y', 'z', 'a', 'f', 'p', 'n', 'ยต', 'm']; + const multiplicative = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + const symbols = [...fractional, '', ...multiplicative]; + + const units = symbols.slice(fractional.length + prefixOffset).map(prefix => { + return `${prefix}${unit}`; + }); + + if (!units.length) { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + throw new RangeError('The unit cannot be converted, please try a different scale'); + } + + return scaledFormatter(units); +}; diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js new file mode 100644 index 00000000000..daf70ebb5d7 --- /dev/null +++ b/app/assets/javascripts/lib/utils/unit_format/index.js @@ -0,0 +1,167 @@ +import { s__ } from '~/locale'; + +import { suffixFormatter, scaledSIFormatter, numberFormatter } from './formatter_factory'; + +/** + * Supported formats + */ +export const SUPPORTED_FORMATS = { + // Number + number: 'number', + percent: 'percent', + percentHundred: 'percentHundred', + + // Duration + seconds: 'seconds', + miliseconds: 'miliseconds', + + // Digital + bytes: 'bytes', + kilobytes: 'kilobytes', + megabytes: 'megabytes', + gigabytes: 'gigabytes', + terabytes: 'terabytes', + petabytes: 'petabytes', +}; + +/** + * Returns a function that formats number to different units + * @param {String} format - Format to use, must be one of the SUPPORTED_FORMATS. Defaults to number. + * + * + */ +export const getFormatter = (format = SUPPORTED_FORMATS.number) => { + // Number + if (format === SUPPORTED_FORMATS.number) { + /** + * Formats a number + * + * @function + * @param {Number} value - Number to format + * @param {Number} fractionDigits - precision decimals + * @param {Number} maxLength - Max lenght of formatted number + * if lenght is exceeded, exponential format is used. + */ + return numberFormatter(); + } + if (format === SUPPORTED_FORMATS.percent) { + /** + * Formats a percentge (0 - 1) + * + * @function + * @param {Number} value - Number to format, `1` is rendered as `100%` + * @param {Number} fractionDigits - number of precision decimals + * @param {Number} maxLength - Max lenght of formatted number + * if lenght is exceeded, exponential format is used. + */ + return numberFormatter('percent'); + } + if (format === SUPPORTED_FORMATS.percentHundred) { + /** + * Formats a percentge (0 to 100) + * + * @function + * @param {Number} value - Number to format, `100` is rendered as `100%` + * @param {Number} fractionDigits - number of precision decimals + * @param {Number} maxLength - Max lenght of formatted number + * if lenght is exceeded, exponential format is used. + */ + return numberFormatter('percent', 1 / 100); + } + + // Durations + if (format === SUPPORTED_FORMATS.seconds) { + /** + * Formats a number of seconds + * + * @function + * @param {Number} value - Number to format, `1` is rendered as `1s` + * @param {Number} fractionDigits - number of precision decimals + * @param {Number} maxLength - Max lenght of formatted number + * if lenght is exceeded, exponential format is used. + */ + return suffixFormatter(s__('Units|s')); + } + if (format === SUPPORTED_FORMATS.miliseconds) { + /** + * Formats a number of miliseconds with ms as units + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1ms` + * @param {Number} fractionDigits - number of precision decimals + * @param {Number} maxLength - Max lenght of formatted number + * if lenght is exceeded, exponential format is used. + */ + return suffixFormatter(s__('Units|ms')); + } + + // Digital + if (format === SUPPORTED_FORMATS.bytes) { + /** + * Formats a number of bytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1B` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledSIFormatter('B'); + } + if (format === SUPPORTED_FORMATS.kilobytes) { + /** + * Formats a number of kilobytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1kB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledSIFormatter('B', 1); + } + if (format === SUPPORTED_FORMATS.megabytes) { + /** + * Formats a number of megabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1MB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledSIFormatter('B', 2); + } + if (format === SUPPORTED_FORMATS.gigabytes) { + /** + * Formats a number of gigabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1GB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledSIFormatter('B', 3); + } + if (format === SUPPORTED_FORMATS.terabytes) { + /** + * Formats a number of terabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1GB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledSIFormatter('B', 4); + } + if (format === SUPPORTED_FORMATS.petabytes) { + /** + * Formats a number of petabytes scaled up to larger digital + * units for larger numbers. + * + * @function + * @param {Number} value - Number to format, `1` is formatted as `1PB` + * @param {Number} fractionDigits - number of precision decimals + */ + return scaledSIFormatter('B', 5); + } + // Fail so client library addresses issue + throw TypeError(`${format} is not a valid number format`); +}; diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 8abb16f58ca..1c39fb072d9 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -4,7 +4,7 @@ import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { s__, __ } from '~/locale'; -import { roundOffFloat } from '~/lib/utils/common_utils'; +import { getFormatter } from '~/lib/utils/unit_format'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import Icon from '~/vue_shared/components/icon.vue'; import { @@ -37,6 +37,8 @@ const events = { datazoom: 'datazoom', }; +const yValFormatter = getFormatter('number'); + export default { components: { GlAreaChart, @@ -171,7 +173,7 @@ export default { boundaryGap: [0.1, 0.1], scale: true, axisLabel: { - formatter: num => roundOffFloat(num, 3).toString(), + formatter: num => yValFormatter(num, 3), }, ...yAxis, }; @@ -313,7 +315,8 @@ export default { this.tooltip.commitUrl = deploy.commitUrl; } else { const { seriesName, color, dataIndex } = dataPoint; - const value = yVal.toFixed(3); + const value = yValFormatter(yVal, 3); + this.tooltip.content.push({ name: seriesName, dataIndex, diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue index 2b29d710236..77f6f1e51c5 100644 --- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue +++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue @@ -1,6 +1,11 @@