Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 18:08:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 18:08:50 +0300
commite06d0e779673d745972863302858105aad9032e5 (patch)
tree0ff35b27a949a164f586613004b4abfe33e7d20e /app
parentf7dae0cdcb70ecb71c1d65f099e9d96b27a4548c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board.js11
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js8
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js7
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/formatter_factory.js119
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/index.js167
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue9
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue12
-rw-r--r--app/assets/stylesheets/framework/modal.scss10
-rw-r--r--app/assets/stylesheets/framework/spinner.scss3
-rw-r--r--app/assets/stylesheets/pages/issuable.scss39
-rw-r--r--app/assets/stylesheets/pages/labels.scss27
-rw-r--r--app/assets/stylesheets/pages/milestone.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss12
-rw-r--r--app/helpers/labels_helper.rb82
-rw-r--r--app/models/clusters/cluster.rb18
-rw-r--r--app/services/issuable/common_system_notes_service.rb2
-rw-r--r--app/services/resource_events/change_milestone_service.rb2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/shared/_delete_label_modal.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml13
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml15
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml8
26 files changed, 506 insertions, 96 deletions
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 = '<b>', matchSuffix = '</b>') {
- 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 = '<b>', 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 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+
export default {
name: 'ResolveDiscussionButton',
+ components: {
+ GlLoadingIcon,
+ },
props: {
isResolving: {
type: Boolean,
@@ -17,12 +22,7 @@ export default {
<template>
<button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
- <i
- v-if="isResolving"
- ref="isResolvingIcon"
- aria-hidden="true"
- class="fa fa-spinner fa-spin"
- ></i>
+ <gl-loading-icon v-if="isResolving" ref="isResolvingIcon" inline />
{{ buttonTitle }}
</button>
</template>
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index ac8437c23ca..f8c46a4495e 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -13,16 +13,14 @@
.page-title,
.modal-title {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
.modal-title-with-label span {
vertical-align: middle;
display: inline-block;
}
-
- .color-label {
- font-size: $gl-font-size;
- padding: $gl-vert-padding $label-padding-modal;
- vertical-align: middle;
- }
}
.modal-title {
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
index 5e05311041c..b7a99d421c9 100644
--- a/app/assets/stylesheets/framework/spinner.scss
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -51,7 +51,8 @@
}
.btn {
- .spinner {
+ .spinner,
+ .gl-spinner {
vertical-align: text-bottom;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 43636f65eb8..fd56f655c0a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -86,14 +86,19 @@
}
.issuable-show-labels {
- a {
+ .gl-label {
margin-bottom: 5px;
margin-right: 5px;
+ }
+
+ a {
display: inline-block;
.color-label {
padding: 4px $grid-size;
border-radius: $label-border-radius;
+ margin-right: 4px;
+ margin-bottom: 4px;
}
&:hover .color-label {
@@ -159,9 +164,25 @@
.avatar {
border-color: rgba($gray-normal, 0.2);
}
+ }
+ }
+ a.gl-label-icon {
+ color: $gray-500;
+ }
+
+ .gl-label .gl-label-link:hover {
+ text-decoration: none;
+ color: inherit;
+
+ .gl-label-text:last-of-type {
+ text-decoration: underline;
}
+ }
+ .gl-label .gl-label-icon:hover {
+ text-decoration: none;
+ color: $gray-500;
}
.btn-link {
@@ -800,11 +821,23 @@
a {
color: $gl-text-color;
+ }
- .fa {
- color: $gl-text-color-secondary;
+ .gl-label-link {
+ color: inherit;
+
+ &:hover {
+ text-decoration: none;
+
+ .gl-label-text:last-of-type {
+ text-decoration: underline;
+ }
}
}
+
+ .gl-label-icon {
+ color: $gray-500;
+ }
}
@media(max-width: map-get($grid-breakpoints, lg)-1) {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 7d5e185834b..91ac150f6e2 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -127,6 +127,11 @@
.color-label {
padding: $gl-padding-4 $grid-size;
}
+
+ .prepend-description-left {
+ vertical-align: top;
+ line-height: 24px;
+ }
}
.prioritized-labels {
@@ -305,10 +310,13 @@
width: 150px;
flex-shrink: 0;
- .badge {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
+ .scoped-label-wrapper,
+ .gl-label {
+ line-height: $gl-line-height;
+ }
+
+ .gl-label-scoped .gl-label-text:last-of-type {
+ padding-right: 22px;
}
}
@@ -445,10 +453,19 @@
}
}
+.gl-label-scoped {
+ box-shadow: 0 0 0 2px currentColor inset;
+
+ &.gl-label-sm {
+ box-shadow: 0 0 0 1px inset;
+ }
+}
+
// Label inside title of Delete Label Modal
.modal-header .page-title {
.scoped-label-wrapper {
- .scoped-label {
+ .scoped-label,
+ .gl-label-icon {
line-height: 20px;
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index b399662997c..cd1154b88a5 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -59,9 +59,19 @@ $status-box-line-height: 26px;
}
.issuable-row {
- span a {
- color: $gl-text-color;
- word-wrap: break-word;
+ span {
+ a {
+ color: $gl-text-color;
+ word-wrap: break-word;
+ }
+
+ .gl-label-link {
+ color: inherit;
+ }
+
+ .gl-label-icon {
+ color: $gray-500;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 347addcec37..aaecbd6ff00 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -283,7 +283,7 @@ $note-form-margin-left: 72px;
text-transform: lowercase;
}
- a {
+ a:not(.gl-link) {
color: $blue-600;
}
@@ -671,6 +671,16 @@ $note-form-margin-left: 72px;
a:hover {
text-decoration: underline;
}
+
+ .gl-label-link:hover,
+ .gl-label-icon:hover {
+ text-decoration: none;
+ color: inherit;
+
+ .gl-label-text:last-of-type {
+ text-decoration: underline;
+ }
+ }
}
/**
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 91127df318e..97232def91c 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -36,37 +36,42 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, type: :issue, tooltip: true, css_class: nil, &block)
+ def link_to_label(label, type: :issue, tooltip: true, small: false, &block)
link = label.filter_path(type: type)
if block_given?
- link_to link, class: css_class, &block
+ link_to link, &block
else
- render_label(label, tooltip: tooltip, link: link, css: css_class)
+ render_label(label, link: link, tooltip: tooltip, small: small)
end
end
- def render_label(label, tooltip: true, link: nil, css: nil, dataset: nil)
- # if scoped label is used then EE wraps label tag with scoped label
- # doc link
- html = render_colored_label(label, tooltip: tooltip)
- html = link_to(html, link, class: css, data: dataset) if link
+ def render_label(label, link: nil, tooltip: true, dataset: nil, small: false)
+ html = render_colored_label(label)
- html
+ if link
+ title = label_tooltip_title(label) if tooltip
+ html = render_label_link(html, link: link, title: title, dataset: dataset)
+ end
+
+ wrap_label_html(html, small: small, label: label)
end
- def render_colored_label(label, label_suffix: '', tooltip: true, title: nil)
- text_color = text_color_for_bg(label.color)
- title ||= tooltip ? label_tooltip_title(label) : label.name
+ def render_colored_label(label, suffix: '')
+ render_label_text(
+ label.name,
+ suffix: suffix,
+ css_class: text_color_class_for_bg(label.color),
+ bg_color: label.color
+ )
+ end
- # Intentionally not using content_tag here so that this method can be called
- # by LabelReferenceFilter
- span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) +
- %(data-html="true" style="background-color: #{label.color}; color: #{text_color}" ) +
- %(title="#{ERB::Util.html_escape_once(title)}" data-container="body">) +
- %(#{ERB::Util.html_escape_once(label.name)}#{label_suffix}</span>)
+ # We need the `label` argument here for EE
+ def wrap_label_html(label_html, small:, label:)
+ wrapper_classes = %w(gl-label)
+ wrapper_classes << 'gl-label-sm' if small
- span.html_safe
+ %(<span class="#{wrapper_classes.join(' ')}">#{label_html}</span>).html_safe
end
def label_tooltip_title(label)
@@ -109,6 +114,20 @@ module LabelsHelper
end
end
+ def text_color_class_for_bg(bg_color)
+ if bg_color.length == 4
+ r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
+ else
+ r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex)
+ end
+
+ if (r + g + b) > 500
+ 'gl-label-text-dark'
+ else
+ 'gl-label-text-light'
+ end
+ end
+
def text_color_for_bg(bg_color)
if bg_color.length == 4
r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
@@ -246,6 +265,31 @@ module LabelsHelper
def issuable_types
['issues', 'merge requests']
end
+
+ private
+
+ def render_label_link(label_html, link:, title:, dataset:)
+ classes = %w(gl-link gl-label-link)
+ dataset ||= {}
+
+ if title.present?
+ classes << 'has-tooltip'
+ dataset.merge!(html: true, title: title)
+ end
+
+ link_to(label_html, link, class: classes.join(' '), data: dataset)
+ end
+
+ def render_label_text(name, suffix: '', css_class: nil, bg_color: nil)
+ <<~HTML.chomp.html_safe
+ <span
+ class="gl-label-text #{css_class}"
+ data-container="body"
+ data-html="true"
+ #{"style=\"background-color: #{bg_color}\"" if bg_color}
+ >#{ERB::Util.html_escape_once(name)}#{suffix}</span>
+ HTML
+ end
end
LabelsHelper.prepend_if_ee('EE::LabelsHelper')
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7e76d324bdc..6e890de924e 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -11,15 +11,15 @@ module Clusters
self.table_name = 'clusters'
APPLICATIONS = {
- Applications::Helm.application_name => Applications::Helm,
- Applications::Ingress.application_name => Applications::Ingress,
- Applications::CertManager.application_name => Applications::CertManager,
- Applications::Crossplane.application_name => Applications::Crossplane,
- Applications::Prometheus.application_name => Applications::Prometheus,
- Applications::Runner.application_name => Applications::Runner,
- Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative,
- Applications::ElasticStack.application_name => Applications::ElasticStack
+ Clusters::Applications::Helm.application_name => Clusters::Applications::Helm,
+ Clusters::Applications::Ingress.application_name => Clusters::Applications::Ingress,
+ Clusters::Applications::CertManager.application_name => Clusters::Applications::CertManager,
+ Clusters::Applications::Crossplane.application_name => Clusters::Applications::Crossplane,
+ Clusters::Applications::Prometheus.application_name => Clusters::Applications::Prometheus,
+ Clusters::Applications::Runner.application_name => Clusters::Applications::Runner,
+ Clusters::Applications::Jupyter.application_name => Clusters::Applications::Jupyter,
+ Clusters::Applications::Knative.application_name => Clusters::Applications::Knative,
+ Clusters::Applications::ElasticStack.application_name => Clusters::Applications::ElasticStack
}.freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index c53e19c922a..67cf212691f 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -101,7 +101,7 @@ module Issuable
def create_milestone_note
if milestone_changes_tracking_enabled?
# Creates a synthetic note
- ResourceEvents::ChangeMilestoneService.new(resource: issuable, user: current_user).execute
+ ResourceEvents::ChangeMilestoneService.new(issuable, current_user).execute
else
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end
diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb
index dd637bcc765..ea196822f74 100644
--- a/app/services/resource_events/change_milestone_service.rb
+++ b/app/services/resource_events/change_milestone_service.rb
@@ -4,7 +4,7 @@ module ResourceEvents
class ChangeMilestoneService
attr_reader :resource, :user, :event_created_at, :milestone
- def initialize(resource:, user:, created_at: Time.now)
+ def initialize(resource, user, created_at: Time.now)
@resource = resource
@user = user
@event_created_at = created_at
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index c8ab47888d0..a6c6b77c9dd 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -38,7 +38,7 @@
- if issue.labels.any?
&nbsp;
- presented_labels_sorted_by_title(issue.labels, issue.project).each do |label|
- = link_to_label(label, css_class: 'label-link')
+ = link_to_label(label, small: true)
= render_if_exists "projects/issues/issue_weight", issue: issue
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 36f19ee6175..744dca1c462 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -35,7 +35,7 @@
- if merge_request.labels.any?
&nbsp;
- presented_labels_sorted_by_title(merge_request.labels, merge_request.project).each do |label|
- = link_to_label(label, type: :merge_request, css_class: 'label-link')
+ = link_to_label(label, type: :merge_request, small: true)
.issuable-meta
%ul.controls.d-flex.align-items-end
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index f37dd2cdf02..c6629cd33a5 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %h3.page-title Delete #{render_label(label, tooltip: false)} ?
+ %h3.page-title Delete label: #{label.name} ?
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 3db96db73ce..e42d8650708 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -29,11 +29,14 @@
":title" => '(list.assignee && list.assignee.username || "")' }
@{{ list.assignee.username }}
- %span.has-tooltip.badge.color-label.title.d-inline-block.mw-100.text-truncate.align-middle{ "v-if": "list.type === \"label\"",
- ":title" => '(list.label ? list.label.description : "")',
- data: { container: "body", placement: "bottom" },
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
- {{ list.title }}
+ %gl-label{ "v-if" => " list.type === \"label\"",
+ ":background-color" => "list.label.color",
+ ":title" => "list.label.title",
+ ":description" => "list.label.description",
+ "tooltipPlacement" => "bottom",
+ ":size" => '(!list.isExpanded ? "sm" : "")',
+ ":scoped" => "showScopedLabels(list.label)",
+ ":scoped-labels-documentation-link" => "helpLink" }
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index c50826a7cda..a1088dc5222 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -8,15 +8,12 @@
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
= _("None")
%span{ "v-for" => "label in issue.labels" }
- %span.d-inline-block.position-relative.scoped-label-wrapper{ "v-if" => "showScopedLabels(label)" }
- %a{ href: '#' }
- %span.badge.color-label.label{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- {{ label.title }}
- %a.label.scoped-label{ ":href" => "helpLink()" }
- %i.fa.fa-question-circle{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- %a{ href: "#", "v-else" => true }
- .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- {{ label.title }}
+ %gl-label{ ":key" => "label.id",
+ ":background-color" => "label.color",
+ ":title" => "label.title",
+ ":description" => "label.description",
+ ":scoped" => "showScopedLabels(label)",
+ ":scoped-labels-documentation-link" => "helpLink" }
- if can_admin_issue?
.selectbox
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index ae3ab2adfd0..965c72b82ba 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -21,7 +21,7 @@
%span.issuable-number= issuable.to_reference
- labels.each do |label|
- = render_label(label.present(issuable_subject: project), link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }))
+ = render_label(label.present(issuable_subject: project), link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }), small: true)
%span.assignee-icon
- assignees.each do |assignee|
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index ecab037e378..4c930b90ce7 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -3,11 +3,9 @@
- options = { milestone_title: @milestone.title, label_name: label.title }
%li.no-border
- %span.label-row
- %span.label-name
- = render_label(label, tooltip: false, link: milestones_label_path(options))
- %span.prepend-description-left
- = markdown_field(label, :description)
+ = render_label(label, tooltip: false, link: milestones_label_path(options))
+ %span.prepend-description-left
+ = markdown_field(label, :description)
.float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do