diff options
86 files changed, 1041 insertions, 335 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index 89d32efec6d..093d993c3ad 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -15,7 +15,6 @@ import { GlDropdownDivider, } from '@gitlab/ui'; import { __, sprintf, n__ } from '~/locale'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import Stacktrace from './stacktrace.vue'; @@ -28,7 +27,6 @@ import query from '../queries/details.query.graphql'; export default { components: { - LoadingButton, GlButton, GlFormInput, GlLink, @@ -234,19 +232,21 @@ export default { </div> <div class="error-details-actions"> <div class="d-inline-flex bv-d-sm-down-none"> - <loading-button - :label="ignoreBtnLabel" + <gl-button :loading="updatingIgnoreStatus" data-qa-selector="update_ignore_status_button" @click="onIgnoreStatusUpdate" - /> - <loading-button + > + {{ ignoreBtnLabel }} + </gl-button> + <gl-button class="btn-outline-info ml-2" - :label="resolveBtnLabel" :loading="updatingResolveStatus" data-qa-selector="update_resolve_status_button" @click="onResolveStatusUpdate" - /> + > + {{ resolveBtnLabel }} + </gl-button> <gl-button v-if="error.gitlabIssuePath" class="ml-2" @@ -270,14 +270,15 @@ export default { name="issue[sentry_issue_attributes][sentry_issue_identifier]" /> <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" /> - <loading-button + <gl-button v-if="!error.gitlabIssuePath" class="btn-success" - :label="__('Create issue')" :loading="issueCreationInProgress" data-qa-selector="create_issue_button" @click="createIssue" - /> + > + {{ __('Create issue') }} + </gl-button> </form> </div> <gl-dropdown diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 70f257180c6..552e8cac3a7 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -236,6 +236,7 @@ export default { </gl-dropdown> <div class="filtered-search-input-container flex-fill"> <gl-form-input + v-model="errorSearchQuery" class="pl-2 filtered-search" :disabled="loading" :placeholder="__('Search or filter results…')" diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js new file mode 100644 index 00000000000..d9f49bd81f5 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/options.js @@ -0,0 +1,78 @@ +import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; +import { s__ } from '~/locale'; + +const yAxisBoundaryGap = [0.1, 0.1]; +/** + * Max string length of formatted axis tick + */ +const maxDataAxisTickLength = 8; + +// Defaults +const defaultFormat = SUPPORTED_FORMATS.number; + +const defaultYAxisFormat = defaultFormat; +const defaultYAxisPrecision = 2; + +const defaultTooltipFormat = defaultFormat; +const defaultTooltipPrecision = 3; + +// Give enough space for y-axis with units and name. +const chartGridLeft = 75; + +// Axis options + +/** + * Converts .yml parameters to echarts axis options for data axis + * @param {Object} param - Dashboard .yml definition options + */ +const getDataAxisOptions = ({ format, precision, name }) => { + const formatter = getFormatter(format); + + return { + name, + nameLocation: 'center', // same as gitlab-ui's default + scale: true, + axisLabel: { + formatter: val => formatter(val, precision, maxDataAxisTickLength), + }, + }; +}; + +/** + * Converts .yml parameters to echarts y-axis options + * @param {Object} param - Dashboard .yml definition options + */ +export const getYAxisOptions = ({ + name = s__('Metrics|Values'), + format = defaultYAxisFormat, + precision = defaultYAxisPrecision, +} = {}) => { + return { + nameGap: 63, // larger gap than gitlab-ui's default to fit with formatted numbers + scale: true, + boundaryGap: yAxisBoundaryGap, + + ...getDataAxisOptions({ + name, + format, + precision, + }), + }; +}; + +// Chart grid + +/** + * Grid with enough room to display chart. + */ +export const getChartGrid = ({ left = chartGridLeft } = {}) => ({ left }); + +// Tooltip options + +export const getTooltipFormatter = ({ + format = defaultTooltipFormat, + precision = defaultTooltipPrecision, +} = {}) => { + const formatter = getFormatter(format); + return num => formatter(num, precision); +}; diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 1c39fb072d9..cba0a6da6a9 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -4,7 +4,6 @@ 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 { getFormatter } from '~/lib/utils/unit_format'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import Icon from '~/vue_shared/components/icon.vue'; import { @@ -16,6 +15,7 @@ import { dateFormats, chartColorValues, } from '../../constants'; +import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options'; import { makeDataSeries } from '~/helpers/monitor_helper'; import { graphDataValidatorForValues } from '../../utils'; @@ -30,15 +30,13 @@ const deploymentYAxisCoords = { max: 100, }; -const THROTTLED_DATAZOOM_WAIT = 1000; // miliseconds +const THROTTLED_DATAZOOM_WAIT = 1000; // milliseconds const timestampToISODate = timestamp => new Date(timestamp).toISOString(); const events = { datazoom: 'datazoom', }; -const yValFormatter = getFormatter('number'); - export default { components: { GlAreaChart, @@ -167,14 +165,7 @@ export default { const option = omit(this.option, ['series', 'yAxis', 'xAxis']); const dataYAxis = { - name: this.yAxisLabel, - nameGap: 50, // same as gitlab-ui's default - nameLocation: 'center', // same as gitlab-ui's default - boundaryGap: [0.1, 0.1], - scale: true, - axisLabel: { - formatter: num => yValFormatter(num, 3), - }, + ...getYAxisOptions(this.graphData.yAxis), ...yAxis, }; @@ -204,6 +195,7 @@ export default { series: this.chartOptionSeries, xAxis: timeXAxis, yAxis: [dataYAxis, deploymentsYAxis], + grid: getChartGrid(), dataZoom: [this.dataZoomConfig], ...option, }; @@ -282,8 +274,9 @@ export default { }, }; }, - yAxisLabel() { - return `${this.graphData.y_label}`; + tooltipYFormatter() { + // Use same format as y-axis + return getTooltipFormatter({ format: this.graphData.yAxis?.format }); }, }, created() { @@ -315,12 +308,11 @@ export default { this.tooltip.commitUrl = deploy.commitUrl; } else { const { seriesName, color, dataIndex } = dataPoint; - const value = yValFormatter(yVal, 3); this.tooltip.content.push({ name: seriesName, dataIndex, - value, + value: this.tooltipYFormatter(yVal), color, }); } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index a4073133028..a0bd45bef5e 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -19,7 +19,7 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import { s__ } from '~/locale'; import createFlash from '~/flash'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; +import { mergeUrlParams, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import Icon from '~/vue_shared/components/icon.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; @@ -351,6 +351,10 @@ export default { }; redirectTo(mergeUrlParams(params, window.location.href)); }, + + refreshDashboard() { + refreshCurrentPage(); + }, }, addMetric: { title: s__('Metrics|Add metric'), @@ -438,7 +442,7 @@ export default { :label="s__('Metrics|Show last')" label-size="sm" label-for="monitor-time-window-dropdown" - class="col-sm-6 col-md-6 col-lg-4" + class="col-sm-auto col-md-auto col-lg-auto" > <date-time-picker ref="dateTimePicker" @@ -449,6 +453,18 @@ export default { /> </gl-form-group> + <gl-form-group class="col-sm-2 col-md-2 col-lg-1 refresh-dashboard-button"> + <gl-button + ref="refreshDashboardBtn" + v-gl-tooltip + variant="default" + :title="s__('Metrics|Reload this page')" + @click="refreshDashboard" + > + <icon name="repeat" /> + </gl-button> + </gl-form-group> + <gl-form-group v-if="hasHeaderButtons" label-for="prometheus-graphs-dropdown-buttons" diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 82deaa7ccfd..0e97d50f317 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -1,5 +1,6 @@ import { slugify } from '~/lib/utils/text_utility'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; +import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; export const gqClient = createGqClient( @@ -75,17 +76,37 @@ const mapToMetricsViewModel = (metrics, defaultLabel) => })); /** + * Maps an axis view model + * + * Defaults to a 2 digit precision and `number` format. It only allows + * formats in the SUPPORTED_FORMATS array. + * + * @param {Object} axis + */ +const mapToAxisViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, precision = 2 }) => { + return { + name, + format: SUPPORTED_FORMATS[format] || SUPPORTED_FORMATS.number, + precision, + }; +}; + +/** * Maps a metrics panel to its view model * * @param {Object} panel - Metrics panel * @returns {Object} */ -const mapToPanelViewModel = ({ title = '', type, y_label, metrics = [] }) => { +const mapToPanelViewModel = ({ title = '', type, y_label, y_axis = {}, metrics = [] }) => { + // Both `y_axis.name` and `y_label` are supported for now + // https://gitlab.com/gitlab-org/gitlab/issues/208385 + const yAxis = mapToAxisViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase return { title, type, - y_label, - metrics: mapToMetricsViewModel(metrics, y_label), + y_label: yAxis.name, // Changing y_label to yLabel is pending https://gitlab.com/gitlab-org/gitlab/issues/207198 + yAxis, + metrics: mapToMetricsViewModel(metrics, yAxis.name), }; }; diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index 9c7c10d9864..f4e546e4d4e 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { throttle } from 'lodash'; import $ from 'jquery'; import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; @@ -85,7 +85,7 @@ export default class GLTerminal { addScrollListener(onScrollLimit) { const viewport = this.container.querySelector('.xterm-viewport'); - const listener = _.throttle(() => { + const listener = throttle(() => { onScrollLimit({ canScrollUp: canScrollUp(viewport, SCROLL_MARGIN), canScrollDown: canScrollDown(viewport, SCROLL_MARGIN), diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index abfc81e681e..6244df1180e 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; +import { template as lodashTemplate, omit } from 'lodash'; import importU2FLibrary from './util'; import U2FError from './error'; @@ -37,7 +37,7 @@ export default class U2FAuthenticate { // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. // This can be removed once we upgrade. // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 - this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge')); + this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge')); this.templates = { setup: '#js-authenticate-u2f-setup', @@ -74,7 +74,7 @@ export default class U2FAuthenticate { renderTemplate(name, params) { const templateString = $(this.templates[name]).html(); - const template = _.template(templateString); + const template = lodashTemplate(templateString); return this.container.html(template(params)); } diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 43c814c8070..f5a422727ad 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; +import { template as lodashTemplate } from 'lodash'; import importU2FLibrary from './util'; import U2FError from './error'; @@ -59,7 +59,7 @@ export default class U2FRegister { renderTemplate(name, params) { const templateString = $(this.templates[name]).html(); - const template = _.template(templateString); + const template = lodashTemplate(templateString); return this.container.html(template(params)); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 8e8e67228ed..ad80a51c5f9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -53,6 +53,7 @@ export default { .then(res => res.data) .then(data => { eventHub.$emit('UpdateWidgetData', data); + eventHub.$emit('MRWidgetUpdateRequested'); }) .catch(() => { this.isCancellingAutoMerge = false; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index ea83c61e275..91ac23f427d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -123,13 +123,15 @@ export default class MergeRequestStore { const currentUser = data.current_user; - this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path; - this.revertInForkPath = currentUser.revert_in_fork_path; - - this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; - this.canCreateIssue = currentUser.can_create_issue || false; - this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; - this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false; + if (currentUser) { + this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path; + this.revertInForkPath = currentUser.revert_in_fork_path; + + this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; + this.canCreateIssue = currentUser.can_create_issue || false; + this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; + this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false; + } this.setState(data); } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3892d9dbd07..1c9bfe962f6 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -98,6 +98,14 @@ } } +.refresh-dashboard-button { + margin-top: 22px; + + @media(max-width: map-get($grid-breakpoints, sm)) { + margin-top: 0; + } +} + .metric-area { opacity: 0.25; } diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 2b7571e42b7..c9f46eb72c5 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -117,6 +117,7 @@ class ProfilesController < Profiles::ApplicationController :private_profile, :include_private_contributions, :timezone, + :job_title, status: [:emoji, :message] ) end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index c4cc1adcd4e..aac6ecb07e4 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -66,7 +66,7 @@ module Projects [ :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_human_readable, :build_coverage_regex, :public_builds, - :auto_cancel_pending_pipelines, :ci_config_path, + :auto_cancel_pending_pipelines, :forward_deployment_enabled, :ci_config_path, auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy], ci_cd_settings_attributes: [:default_git_depth] ].tap do |list| diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 1104b676bc4..9da4dfd43b5 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -38,7 +38,7 @@ class Appearance < ApplicationRecord def single_appearance_row if self.class.any? - errors.add(:single_appearance_row, 'Only 1 appearances row can exist') + errors.add(:base, _('Only 1 appearances row can exist')) end end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index a9856203cc0..98b8981754f 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -389,7 +389,7 @@ module ApplicationSettingImplementation def terms_exist return unless enforce_terms? - errors.add(:terms, "You need to set terms to be enforced") unless terms.present? + errors.add(:base, _('You need to set terms to be enforced')) unless terms.present? end def expire_performance_bar_allowed_user_ids_cache diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 8defe742ec4..f9a5f713814 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -148,7 +148,7 @@ module Ci def valid_file_format? unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym - errors.add(:file_format, 'Invalid file format with specified file type') + errors.add(:base, _('Invalid file format with specified file type')) end end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index bdd7ad90fba..233920f4fe2 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Ingress < ApplicationRecord - VERSION = '1.29.3' + VERSION = '1.29.7' MODSECURITY_LOG_CONTAINER_NAME = 'modsecurity-log' self.table_name = 'clusters_applications_ingress' diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 6e890de924e..7f155a8d435 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -306,7 +306,7 @@ module Clusters .where.not(id: id) if duplicate_management_clusters.any? - errors.add(:environment_scope, "cannot add duplicated environment scope") + errors.add(:environment_scope, 'cannot add duplicated environment scope') end end @@ -380,7 +380,7 @@ module Clusters def restrict_modification if provider&.on_creation? - errors.add(:base, "cannot modify during creation") + errors.add(:base, _('Cannot modify provider during creation')) return false end diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index d04a6408a21..0887236e65e 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -19,7 +19,7 @@ module HasRepository def valid_repo? repository.exists? rescue - errors.add(:path, _('Invalid repository path')) + errors.add(:base, _('Invalid repository path')) false end diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index 7df6981a129..3ffb32f94fc 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -37,7 +37,7 @@ module Milestoneable private def milestone_is_valid - errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available? + errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available? end end diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index f61a0bbc65b..dddf96837b7 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -77,7 +77,7 @@ module TimeTrackable return if time_spent.nil? || time_spent == :reset if time_spent < 0 && (time_spent.abs > original_total_time_spent) - errors.add(:time_spent, 'Time to subtract exceeds the total time spent') + errors.add(:base, _('Time to subtract exceeds the total time spent')) end end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 31c813edb67..a9844f627b7 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -105,7 +105,7 @@ class DeployToken < ApplicationRecord end def ensure_at_least_one_scope - errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry + errors.add(:base, _("Scopes can't be blank")) unless read_repository || read_registry end def default_username diff --git a/app/models/description_version.rb b/app/models/description_version.rb index 05362a2f90b..f69564f4893 100644 --- a/app/models/description_version.rb +++ b/app/models/description_version.rb @@ -19,7 +19,13 @@ class DescriptionVersion < ApplicationRecord def exactly_one_issuable issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] } - errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required") if issuable_count != 1 + if issuable_count != 1 + errors.add( + :base, + _("Exactly one of %{attributes} is required") % + { attributes: self.class.issuable_attrs.join(', ') } + ) + end end end diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb index 65ae8d95500..9c6d05f773a 100644 --- a/app/models/external_pull_request.rb +++ b/app/models/external_pull_request.rb @@ -78,7 +78,7 @@ class ExternalPullRequest < ApplicationRecord def not_from_fork if from_fork? - errors.add(:base, 'Pull requests from fork are not supported') + errors.add(:base, _('Pull requests from fork are not supported')) end end diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb index 713c8ef7b94..0a6165c8254 100644 --- a/app/models/milestone_release.rb +++ b/app/models/milestone_release.rb @@ -11,6 +11,6 @@ class MilestoneRelease < ApplicationRecord def same_project_between_milestone_and_release return if milestone&.project_id == release&.project_id - errors.add(:base, 'does not have the same project as the milestone') + errors.add(:base, _('Release does not have the same project as the milestone')) end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index fbc010c6b7c..631bd930e2f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -376,7 +376,7 @@ class Namespace < ApplicationRecord def nesting_level_allowed if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED - errors.add(:parent_id, "has too deep level of nesting") + errors.add(:parent_id, 'has too deep level of nesting') end end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index b26a3025b61..39e177e8bd8 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -31,7 +31,7 @@ class ProjectCiCdSetting < ApplicationRecord end def forward_deployment_enabled? - super && ::Feature.enabled?(:forward_deployment_enabled, project) + super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true) end private diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 7cdbb124dee..2bf14a6ed25 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -168,7 +168,7 @@ class IssueTrackerService < Service return if project.blank? if project.services.external_issue_trackers.where.not(id: id).any? - errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time') + errors.add(:base, _('Another issue tracker is already in use. Only one issue tracker service can be active at a time')) end end end diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb index 1014231102f..1dc7dc73e31 100644 --- a/app/models/prometheus_alert.rb +++ b/app/models/prometheus_alert.rb @@ -69,13 +69,13 @@ class PrometheusAlert < ApplicationRecord def require_valid_environment_project! return if project == environment&.project - errors.add(:environment, "invalid project") + errors.add(:environment, 'invalid project') end def require_valid_metric_project! return if prometheus_metric&.common? return if project == prometheus_metric&.project - errors.add(:prometheus_metric, "invalid project") + errors.add(:prometheus_metric, 'invalid project') end end diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb index 9b3a211ad43..2c0052b0be3 100644 --- a/app/models/resource_event.rb +++ b/app/models/resource_event.rb @@ -37,6 +37,9 @@ class ResourceEvent < ApplicationRecord return true if issuable_count == 1 end - errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required") + errors.add( + :base, _("Exactly one of %{attributes} is required") % + { attributes: self.class.issuable_attrs.join(', ') } + ) end end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 0427d5b9ca7..f3a9293376f 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -111,7 +111,10 @@ class SentNotification < ApplicationRecord note = create_reply('Test', dryrun: true) unless note.valid? - self.errors.add(:base, "Note parameters are invalid: #{note.errors.full_messages.to_sentence}") + self.errors.add( + :base, _("Note parameters are invalid: %{errors}") % + { errors: note.errors.full_messages.to_sentence } + ) end end diff --git a/app/models/timelog.rb b/app/models/timelog.rb index 4ddaf6bcb86..f52dd74d4c9 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -28,9 +28,9 @@ class Timelog < ApplicationRecord def issuable_id_is_present if issue_id && merge_request_id - errors.add(:base, 'Only Issue ID or Merge Request ID is required') + errors.add(:base, _('Only Issue ID or Merge Request ID is required')) elsif issuable.nil? - errors.add(:base, 'Issue or Merge Request ID is required') + errors.add(:base, _('Issue or Merge Request ID is required')) end end diff --git a/app/models/user.rb b/app/models/user.rb index f3db0522edc..81cabc67c3b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -162,6 +162,7 @@ class User < ApplicationRecord has_one :status, class_name: 'UserStatus' has_one :user_preference + has_one :user_detail # # Validations @@ -259,8 +260,10 @@ class User < ApplicationRecord delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference delegate :setup_for_company, :setup_for_company=, to: :user_preference delegate :render_whitespace_in_code, :render_whitespace_in_code=, to: :user_preference + delegate :job_title, :job_title=, to: :user_detail, allow_nil: true accepts_nested_attributes_for :user_preference, update_only: true + accepts_nested_attributes_for :user_detail, update_only: true state_machine :state, initial: :active do event :block do @@ -1619,6 +1622,10 @@ class User < ApplicationRecord super.presence || build_user_preference end + def user_detail + super.presence || build_user_detail + end + def todos_limited_to(ids) todos.where(id: ids) end diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb new file mode 100644 index 00000000000..1621f336111 --- /dev/null +++ b/app/models/user_detail.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class UserDetail < ApplicationRecord + belongs_to :user + + validates :job_title, presence: true, length: { maximum: 200 } +end diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index a72179f40ad..e6be364b48f 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -88,6 +88,15 @@ = _("New pipelines will cancel older, pending pipelines on the same branch") = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank' + .form-group + .form-check + = f.check_box :forward_deployment_enabled, { class: 'form-check-input' } + = f.label :forward_deployment_enabled, class: 'form-check-label' do + %strong= _("Skip older, pending deployment jobs") + .form-text.text-muted + = _("When a deployment job is successful, skip older deployment jobs that are still pending") + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'skip-older-pending-deployment-jobs'), target: '_blank' + %hr .form-group = f.label :build_coverage_regex, _("Test coverage parsing"), class: 'label-bold' diff --git a/changelogs/unreleased/201999-define-formatter-y-axis.yml b/changelogs/unreleased/201999-define-formatter-y-axis.yml new file mode 100644 index 00000000000..3a6039cd1bf --- /dev/null +++ b/changelogs/unreleased/201999-define-formatter-y-axis.yml @@ -0,0 +1,5 @@ +--- +title: Add properties to the dashboard definition to customize y-axis format +merge_request: 25785 +author: +type: added diff --git a/changelogs/unreleased/207203-forward-deployment-ui.yml b/changelogs/unreleased/207203-forward-deployment-ui.yml new file mode 100644 index 00000000000..bd2165beb30 --- /dev/null +++ b/changelogs/unreleased/207203-forward-deployment-ui.yml @@ -0,0 +1,5 @@ +--- +title: Added Drop older active deployments project setting +merge_request: 25520 +author: +type: added diff --git a/changelogs/unreleased/208479-requests-for-svgs-returning-404-in-issues-analytics-feature.yml b/changelogs/unreleased/208479-requests-for-svgs-returning-404-in-issues-analytics-feature.yml new file mode 100644 index 00000000000..913569985b8 --- /dev/null +++ b/changelogs/unreleased/208479-requests-for-svgs-returning-404-in-issues-analytics-feature.yml @@ -0,0 +1,5 @@ +--- +title: 'Issue Analytics: Fix svg illustration path for empty state' +merge_request: 26219 +author: +type: fixed diff --git a/changelogs/unreleased/add-user-job-title-column.yml b/changelogs/unreleased/add-user-job-title-column.yml new file mode 100644 index 00000000000..5090f6f4eb1 --- /dev/null +++ b/changelogs/unreleased/add-user-job-title-column.yml @@ -0,0 +1,5 @@ +--- +title: Add support for user Job Title +merge_request: 25483 +author: +type: added diff --git a/changelogs/unreleased/jivavnvl-add-refresh-button-monitoring-dashboard.yml b/changelogs/unreleased/jivavnvl-add-refresh-button-monitoring-dashboard.yml new file mode 100644 index 00000000000..75052ff337f --- /dev/null +++ b/changelogs/unreleased/jivavnvl-add-refresh-button-monitoring-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add refresh dashboard button +merge_request: 25716 +author: +type: changed diff --git a/changelogs/unreleased/lm-fix-error-query.yml b/changelogs/unreleased/lm-fix-error-query.yml new file mode 100644 index 00000000000..2baa316dd5a --- /dev/null +++ b/changelogs/unreleased/lm-fix-error-query.yml @@ -0,0 +1,5 @@ +--- +title: Fix search for Sentry error list +merge_request: 26129 +author: +type: fixed diff --git a/changelogs/unreleased/sh-refresh-mr-widget-upon-cancel.yml b/changelogs/unreleased/sh-refresh-mr-widget-upon-cancel.yml new file mode 100644 index 00000000000..66e8ebf1992 --- /dev/null +++ b/changelogs/unreleased/sh-refresh-mr-widget-upon-cancel.yml @@ -0,0 +1,5 @@ +--- +title: Refresh widget after canceling "Merge When Pipeline Succeeds" +merge_request: 26232 +author: +type: fixed diff --git a/changelogs/unreleased/update_ingress_chart_version.yml b/changelogs/unreleased/update_ingress_chart_version.yml new file mode 100644 index 00000000000..9a3c85c5637 --- /dev/null +++ b/changelogs/unreleased/update_ingress_chart_version.yml @@ -0,0 +1,5 @@ +--- +title: 'Update Ingress chart version to 1.29.7' +merge_request: 25949 +author: +type: added diff --git a/config/locales/en.yml b/config/locales/en.yml index dabcefba169..c95232ae540 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -20,6 +20,8 @@ en: token: "Grafana HTTP API Token" grafana_url: "Grafana API URL" grafana_enabled: "Grafana integration enabled" + user/user_detail: + job_title: 'Job title' views: pagination: previous: "Prev" diff --git a/db/migrate/20200227165129_create_user_details.rb b/db/migrate/20200227165129_create_user_details.rb new file mode 100644 index 00000000000..14403824abc --- /dev/null +++ b/db/migrate/20200227165129_create_user_details.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateUserDetails < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + with_lock_retries do + create_table :user_details, id: false do |t| + t.references :user, index: false, foreign_key: { on_delete: :cascade }, null: false, primary_key: true + t.string :job_title, limit: 200, default: "", null: false + end + end + + add_index :user_details, :user_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index fa251f65741..355b36e1483 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_02_26_162723) do +ActiveRecord::Schema.define(version: 2020_02_27_165129) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -4170,6 +4170,11 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do t.index ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true end + create_table "user_details", primary_key: "user_id", force: :cascade do |t| + t.string "job_title", limit: 200, default: "", null: false + t.index ["user_id"], name: "index_user_details_on_user_id", unique: true + end + create_table "user_interacted_projects", id: false, force: :cascade do |t| t.integer "user_id", null: false t.integer "project_id", null: false @@ -5028,6 +5033,7 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do add_foreign_key "u2f_registrations", "users" add_foreign_key "user_callouts", "users", on_delete: :cascade add_foreign_key "user_custom_attributes", "users", on_delete: :cascade + add_foreign_key "user_details", "users", on_delete: :cascade add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade add_foreign_key "user_preferences", "users", on_delete: :cascade diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md index e7c1b2793c7..9ee215c60c5 100644 --- a/doc/administration/geo/replication/datatypes.md +++ b/doc/administration/geo/replication/datatypes.md @@ -75,7 +75,7 @@ GitLab stores files and blobs such as Issue attachments or LFS objects into eith - The filesystem in a specific location. - An Object Storage solution. Object Storage solutions can be: - Cloud based like Amazon S3 Google Cloud Storage. - - Self hosted (like MinIO). + - Hosted by you (like MinIO). - A Storage Appliance that exposes an Object Storage-compatible API. When using the filesystem store instead of Object Storage, you need to use network mounted filesystems diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 9c64453dadd..7beb1193459 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -72,10 +72,10 @@ If a new pipeline would cause the total number of jobs to exceed the limit, the will fail with a `job_activity_limit_exceeded` error. - On GitLab.com different [limits are defined per plan](../user/gitlab_com/index.md#gitlab-cicd) and they affect all projects under that plan. -- On [GitLab Starter](https://about.gitlab.com/pricing/#self-managed) tier or higher self-hosted installations, this limit is defined for the `default` plan that affects all projects. +- On [GitLab Starter](https://about.gitlab.com/pricing/#self-managed) tier or higher self-managed installations, this limit is defined for the `default` plan that affects all projects. This limit is disabled by default. -To set this limit on a self-hosted installation, run the following in the +To set this limit on a self-managed installation, run the following in the [GitLab Rails console](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session): ```ruby @@ -113,9 +113,9 @@ text field exceeds this limit then the text will be truncated to this number of characters and the rest will not be indexed and hence will not be searchable. - On GitLab.com this is limited to 20000 characters -- For self-hosted installations it is unlimited by default +- For self-managed installations it is unlimited by default -This limit can be configured for self hosted installations when [enabling +This limit can be configured for self-managed installations when [enabling Elasticsearch](../integration/elasticsearch.md#enabling-elasticsearch). NOTE: **Note:** Set the limit to `0` to disable it. diff --git a/doc/administration/libravatar.md b/doc/administration/libravatar.md index 43a6b8f0d34..c28e701dc25 100644 --- a/doc/administration/libravatar.md +++ b/doc/administration/libravatar.md @@ -9,7 +9,7 @@ GitLab by default supports the [Gravatar](https://gravatar.com) avatar service. Libravatar is another service that delivers your avatar (profile picture) to other websites. The Libravatar API is [heavily based on gravatar](https://wiki.libravatar.org/api/), so you can -easily switch to the Libravatar avatar service or even a self-hosted Libravatar +easily switch to the Libravatar avatar service or even your own Libravatar server. ## Configuration @@ -35,7 +35,7 @@ the configuration options as follows: ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" ``` -### Self-hosted Libravatar server +### Your own Libravatar server If you are [running your own libravatar service](https://wiki.libravatar.org/running_your_own/), the URL will be different in the configuration, but you must provide the same diff --git a/doc/api/users.md b/doc/api/users.md index 929ad1248be..49bd090f294 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -95,6 +95,7 @@ GET /users "twitter": "", "website_url": "", "organization": "", + "job_title": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, @@ -132,6 +133,7 @@ GET /users "twitter": "", "website_url": "", "organization": "", + "job_title": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", "theme_id": 1, @@ -247,7 +249,8 @@ Parameters: "linkedin": "", "twitter": "", "website_url": "", - "organization": "" + "organization": "", + "job_title": "Operations Specialist" } ``` @@ -282,6 +285,7 @@ Example Responses: "twitter": "", "website_url": "", "organization": "", + "job_title": "Operations Specialist", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, @@ -545,6 +549,7 @@ GET /user "twitter": "", "website_url": "", "organization": "", + "job_title": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 777174eb280..32faa128e6b 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -693,7 +693,7 @@ To configure credentials store, follow these steps: } ``` - - Or, if you are running self-hosted Runners, add the above JSON to + - Or, if you are running self-managed Runners, add the above JSON to `${GITLAB_RUNNER_HOME}/.docker/config.json`. GitLab Runner will read this config file and will use the needed helper for this specific repository. @@ -726,7 +726,7 @@ To configure access for `aws_account_id.dkr.ecr.region.amazonaws.com`, follow th } ``` - - Or, if you are running self-hosted Runners, + - Or, if you are running self-managed Runners, add the above JSON to `${GITLAB_RUNNER_HOME}/.docker/config.json`. GitLab Runner will read this config file and will use the needed helper for this specific repository. diff --git a/doc/development/README.md b/doc/development/README.md index 6121ddf3ed6..d73b83e53fc 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -44,7 +44,7 @@ Complementary reads: - [Guidelines for implementing Enterprise Edition features](ee_features.md) - [Danger bot](dangerbot.md) - [Generate a changelog entry with `bin/changelog`](changelog.md) -- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLabbers) +- [Requesting access to Chatops on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLab team members) ## UX and Frontend guides diff --git a/doc/development/deleting_migrations.md b/doc/development/deleting_migrations.md index 46f4b840134..3ac039a1692 100644 --- a/doc/development/deleting_migrations.md +++ b/doc/development/deleting_migrations.md @@ -1,7 +1,7 @@ # Delete existing migrations When removing existing migrations from the GitLab project, you have to take into account -the possibility of the migration already been included in past releases or in the current release, and thus already executed on GitLab.com and/or in self-hosted instances. +the possibility of the migration already been included in past releases or in the current release, and thus already executed on GitLab.com and/or in self-managed instances. Because of it, it's not possible to delete existing migrations, as that could lead to: diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md index 922995cb915..a9a75791db7 100644 --- a/doc/development/feature_flags/controls.md +++ b/doc/development/feature_flags/controls.md @@ -147,7 +147,7 @@ is always on or off to the users. ## Cleaning up Once the change is deemed stable, submit a new merge request to remove the -feature flag. This ensures the change is available to all users and self-hosted +feature flag. This ensures the change is available to all users and self-managed instances. Make sure to add the ~"feature flag" label to this merge request so release managers are aware the changes are hidden behind a feature flag. If the merge request has to be picked into a stable branch, make sure to also add the diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index b7c667d034b..b7ea56be873 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -50,7 +50,7 @@ The reason we spread this out across three releases is that dropping a column is a destructive operation that can't be rolled back easily. Following this procedure helps us to make sure there are no deployments to GitLab.com -and upgrade processes for self-hosted installations that lump together any of these steps. +and upgrade processes for self-managed installations that lump together any of these steps. ### Step 1: Ignoring the column (release M) diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 0c74c39a067..2089a9dfaf8 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -121,7 +121,7 @@ With the [Customers Portal](https://customers.gitlab.com/) you can: To change billing information: -1. Log in to [Customers Portal](https://customers.gitlab.com/customers/sign_in). +1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in). 1. Go to the **My Account** page. 1. Make the required changes to the **Account Details** information. 1. Click **Update Account**. @@ -143,7 +143,7 @@ account: 1. On the Customers Portal page, click [**My Account**](https://customers.gitlab.com/customers/edit) in the top menu. 1. Under **Your GitLab.com account**, click **Change linked account** button. -1. Log in to the [GitLab.com](https://gitlab.com) account you want to link to Customers Portal. +1. Log in to the [GitLab.com](https://gitlab.com) account you want to link to the Customers Portal. ### Change the associated namespace @@ -195,9 +195,9 @@ The [Customers Portal](https://customers.gitlab.com/customers/sign_in) is your t TIP: **Tip:** Contact our [support team](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293) if you need assistance accessing the Customers Portal or if you need to change the contact person who manages your subscription. -Check who is accessing your system. Are there user accounts which are no longer active? It's important to regularly review your GitLab user accounts because: +It's important to regularly review your user accounts, because: -- A GitLab subscription is based on the number of users. Renewing a subscription for too many users results in you paying more than you should. Attempting to renew a subscription for too few users will result in the renewal failing. +- A GitLab subscription is based on the number of users. You will pay more than you should if you renew for too many users, while the renewal will fail if you attempt to renew a subscription for too few users. - Stale user accounts can be a security risk. A regular review helps reduce this risk. #### Users over License @@ -219,7 +219,7 @@ Self-managed instances can add users to a subscription any time during the subsc To add users to a subscription: -1. Log in to [Customers Portal](https://customers.gitlab.com/). +1. Log in to the [Customers Portal](https://customers.gitlab.com/). 1. Select **Manage Purchases**. 1. Select **Add more seats**. 1. Enter the number of additional users. @@ -234,7 +234,7 @@ The following will be emailed to you: ### Renew or change a GitLab.com subscription -To renew for more users than are currently active in your GitLab.com system, contact our sales team via `renewals@gitlab.com` for assistance as this can't be done in Customers Portal. +To renew for more users than are currently active in your GitLab.com system, contact our sales team via `renewals@gitlab.com` for assistance as this can't be done in the Customers Portal. To change the [GitLab tier](https://about.gitlab.com/pricing/), select **Upgrade** under your subscription on the [My Account](https://customers.gitlab.com/subscriptions) page. @@ -259,13 +259,13 @@ We recommend following these steps during renewal: 1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and select the **Renew** button beneath your existing subscription. TIP: **Tip:** - If you need to change your [GitLab tier](https://about.gitlab.com/pricing/), contact our sales team via `renewals@gitlab.com` for assistance as this can't be done in Customers Portal. + If you need to change your [GitLab tier](https://about.gitlab.com/pricing/), contact our sales team via `renewals@gitlab.com` for assistance as this can't be done in the Customers Portal. 1. In the first box, enter the total number of user licenses you’ll need for the upcoming year. Be sure this number is at least **equal to, or greater than** the number of active users in the system at the time of performing the renewal. 1. Enter the number of [users over license](#users-over-license) in the second box for the user overage incurred in your previous subscription term. TIP: **Tip:** - You can find the _users over license_ in your instance's **Admin** dashboard by clicking on {**admin**} (**Admin Area**) in the top bar, or going to `/admin`. + You can find the _users over license_ in your instance's **Admin** dashboard by clicking on **{admin}** (**Admin Area**) in the top bar, or going to `/admin`. 1. Review your renewal details and complete the payment process. 1. A license for the renewal term will be available on the [Manage Purchases](https://customers.gitlab.com/subscriptions) page beneath your new subscription details. @@ -294,13 +294,11 @@ CI pipeline minutes are the execution time for your [pipelines](../ci/pipelines. Quotas apply to: -- Groups, where the minutes are shared across all members of the group, its subgroups, and nested projects. To view the group's usage, navigate to the group's page, then **Settings > Usage Quotas**. -- Your personal account, where the minutes are available for your personal projects. To view and buy personal minutes, click your avatar, then **Settings > Pipeline quota**. +- Groups, where the minutes are shared across all members of the group, its subgroups, and nested projects. To view the group's usage, navigate to the group, then **{settings}** **Settings > Usage Quotas**. +- Your personal account, where the minutes are available for your personal projects. To view and buy personal minutes, click your avatar, then **{settings}** **Settings > Pipeline quota**. Only pipeline minutes for GitLab shared runners are restricted. If you have a specific runner set up for your projects, there is no limit to your build time on GitLab.com. -The minutes limit does not apply to public projects. - The available quota is reset on the first of each calendar month at midnight UTC. When the CI minutes are depleted, an email is sent automatically to notify the owner(s) @@ -317,10 +315,10 @@ main quota. Additional minutes: To purchase additional minutes for your group on GitLab.com: -1. From your group, go to **Settings > Pipeline quota**. +1. From your group, go to **{settings}** **Settings > Usage Quotas**. 1. Locate the subscription card that's linked to your group on GitLab.com, click **Buy more CI minutes**, and complete the details about the transaction. 1. Once we have processed your payment, the extra CI minutes will be synced to your group. -1. To confirm the available CI minutes, go to **Group > Settings > Pipelines quota**. +1. To confirm the available CI minutes, go to your group, then **{settings}** **Settings > Usage Quotas**. The **Additional minutes** displayed now includes the purchased additional CI minutes, plus any minutes rolled over from last month. To purchase additional minutes for your personal namespace: diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index d4e755dab6b..9ca9b3c0358 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -191,6 +191,25 @@ you can enable this in the project settings: 1. Check the **Auto-cancel redundant, pending pipelines** checkbox. 1. Click **Save changes**. +## Skip older, pending deployment jobs + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25276) in GitLab 12.9. + +Your project may have multiple concurrent deployment jobs that are +scheduled to run within the same time frame. + +This can lead to a situation where an older deployment job runs after a +newer one, which may not be what you want. + +To avoid this scenario: + +1. Go to **{settings}** **Settings > CI / CD**. +1. Expand **General pipelines**. +1. Check the **Skip older, pending deployment jobs** checkbox. +1. Click **Save changes**. + +The pending deployment jobs will be skipped. + ## Pipeline Badges In the pipelines settings page you can find pipeline status and test coverage diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb index 15e4619cdb8..4a1f570c3f0 100644 --- a/lib/api/entities/user.rb +++ b/lib/api/entities/user.rb @@ -4,7 +4,7 @@ module API module Entities class User < UserBasic expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } - expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization + expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 42302cf8c0b..62a28181e7d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1281,6 +1281,9 @@ msgstr "" msgid "Admin notes" msgstr "" +msgid "AdminArea|Included Free in license" +msgstr "" + msgid "AdminArea|Stop all jobs" msgstr "" @@ -2012,6 +2015,9 @@ msgstr "" msgid "Anonymous" msgstr "" +msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time" +msgstr "" + msgid "Anti-spam verification" msgstr "" @@ -3248,6 +3254,9 @@ msgstr "" msgid "Cannot modify managed Kubernetes cluster" msgstr "" +msgid "Cannot modify provider during creation" +msgstr "" + msgid "Cannot refer to a group milestone by an internal id!" msgstr "" @@ -5817,6 +5826,9 @@ msgstr "" msgid "Current node" msgstr "" +msgid "Current node must be the primary node or you will be locking yourself out" +msgstr "" + msgid "Current password" msgstr "" @@ -8018,6 +8030,9 @@ msgstr "" msgid "Evidence collection" msgstr "" +msgid "Exactly one of %{attributes} is required" +msgstr "" + msgid "Example: @sub\\.company\\.com$" msgstr "" @@ -9776,6 +9791,9 @@ msgstr "" msgid "Group pipeline minutes were successfully reset." msgstr "" +msgid "Group requires separate account" +msgstr "" + msgid "Group variables (inherited)" msgstr "" @@ -10121,6 +10139,9 @@ msgstr "" msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}" msgstr "" +msgid "Hashed Storage must be enabled to use Geo" +msgstr "" + msgid "Hashed repository storage paths" msgstr "" @@ -10755,6 +10776,9 @@ msgstr "" msgid "Invalid field" msgstr "" +msgid "Invalid file format with specified file type" +msgstr "" + msgid "Invalid file." msgstr "" @@ -10848,6 +10872,9 @@ msgstr "" msgid "Issue events" msgstr "" +msgid "Issue or Merge Request ID is required" +msgstr "" + msgid "Issue template (optional)" msgstr "" @@ -12387,6 +12414,9 @@ msgstr "" msgid "Metrics|Prometheus Query Documentation" msgstr "" +msgid "Metrics|Reload this page" +msgstr "" + msgid "Metrics|Show last" msgstr "" @@ -12432,6 +12462,9 @@ msgstr "" msgid "Metrics|Validating query" msgstr "" +msgid "Metrics|Values" +msgstr "" + msgid "Metrics|View logs" msgstr "" @@ -13141,6 +13174,9 @@ msgstr "" msgid "Note" msgstr "" +msgid "Note parameters are invalid: %{errors}" +msgstr "" + msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}." msgstr "" @@ -13374,6 +13410,12 @@ msgstr "" msgid "Only 'Reporter' roles and above on tiers Premium / Silver and above can see Value Stream Analytics." msgstr "" +msgid "Only 1 appearances row can exist" +msgstr "" + +msgid "Only Issue ID or Merge Request ID is required" +msgstr "" + msgid "Only Project Members" msgstr "" @@ -13548,12 +13590,24 @@ msgstr "" msgid "Package Registry" msgstr "" +msgid "Package already exists" +msgstr "" + msgid "Package deleted successfully" msgstr "" msgid "Package information" msgstr "" +msgid "Package recipe already exists" +msgstr "" + +msgid "Package type must be Conan" +msgstr "" + +msgid "Package type must be Maven" +msgstr "" + msgid "Package was removed" msgstr "" @@ -15753,6 +15807,9 @@ msgstr "" msgid "Pull" msgstr "" +msgid "Pull requests from fork are not supported" +msgstr "" + msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue." msgstr "" @@ -16015,6 +16072,9 @@ msgid_plural "Releases" msgstr[0] "" msgstr[1] "" +msgid "Release does not have the same project as the milestone" +msgstr "" + msgid "Release notes" msgstr "" @@ -16834,6 +16894,9 @@ msgstr "" msgid "Scopes" msgstr "" +msgid "Scopes can't be blank" +msgstr "" + msgid "Scroll down" msgstr "" @@ -17908,6 +17971,9 @@ msgstr "" msgid "Size settings for static websites" msgstr "" +msgid "Skip older, pending deployment jobs" +msgstr "" + msgid "Skip this for now" msgstr "" @@ -19349,6 +19415,9 @@ msgstr "" msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "" +msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc." +msgstr "" + msgid "The license was removed. GitLab has fallen back on the previous license." msgstr "" @@ -20003,6 +20072,9 @@ msgstr "" msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action." msgstr "" +msgid "This license has already expired." +msgstr "" + msgid "This may expose confidential information as the selected fork is in another namespace that can have other members." msgstr "" @@ -20114,6 +20186,9 @@ msgstr "" msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here." msgstr "" +msgid "Thread to reply to cannot be found" +msgstr "" + msgid "Threat Monitoring" msgstr "" @@ -20210,6 +20285,9 @@ msgstr "" msgid "Time to merge" msgstr "" +msgid "Time to subtract exceeds the total time spent" +msgstr "" + msgid "Time tracking" msgstr "" @@ -21288,6 +21366,9 @@ msgstr "" msgid "User identity was successfully updated." msgstr "" +msgid "User is not allowed to resolve thread" +msgstr "" + msgid "User key was successfully removed." msgstr "" @@ -22043,6 +22124,9 @@ msgstr "" msgid "What are you searching for?" msgstr "" +msgid "When a deployment job is successful, skip older deployment jobs that are still pending" +msgstr "" + msgid "When a runner is locked, it cannot be assigned to other projects" msgstr "" @@ -22600,6 +22684,9 @@ msgstr "" msgid "You need to register a two-factor authentication app before you can set up a U2F device." msgstr "" +msgid "You need to set terms to be enforced" +msgstr "" + msgid "You need to specify both an Access Token and a Host URL." msgstr "" diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index 91f3bfcfa40..f0d83bb6bbd 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -89,6 +89,16 @@ describe ProfilesController, :request_store do expect(user.reload.status.message).to eq('Working hard!') expect(response).to have_gitlab_http_status(:found) end + + it 'allows updating user specified job title' do + title = 'Marketing Executive' + sign_in(user) + + put :update, params: { user: { job_title: title } } + + expect(user.reload.job_title).to eq(title) + expect(response).to have_gitlab_http_status(:found) + end end describe 'PUT update_username' do diff --git a/spec/factories/user_details.rb b/spec/factories/user_details.rb new file mode 100644 index 00000000000..3442f057c44 --- /dev/null +++ b/spec/factories/user_details.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user_detail do + user + job_title { 'VP of Sales' } + end +end diff --git a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb new file mode 100644 index 00000000000..690c60a3c3f --- /dev/null +++ b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do + include_context 'sentry error tracking context feature' + + let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') } + let_it_be(:error_search_response_body) { fixture_file('sentry/error_list_search_response.json') } + let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" } + let(:issues_api_url_search) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved%20NotFound" } + + before do + stub_request(:get, issues_api_url).with( + headers: { 'Authorization' => 'Bearer access_token_123' } + ).to_return(status: 200, body: issues_response_body, headers: { 'Content-Type' => 'application/json' }) + + stub_request(:get, issues_api_url_search).with( + headers: { 'Authorization' => 'Bearer access_token_123', 'Content-Type' => 'application/json' } + ).to_return(status: 200, body: error_search_response_body, headers: { 'Content-Type' => 'application/json' }) + end + + it 'displays the results' do + sign_in(project.owner) + visit project_error_tracking_index_path(project) + + page.within(find('.gl-table')) do + results = page.all('.table-row') + expect(results.count).to be(2) + end + + find('.gl-form-input').set('NotFound').native.send_keys(:return) + + page.within(find('.gl-table')) do + results = page.all('.table-row') + expect(results.count).to be(1) + expect(results.first).to have_content('NotFound') + end + end +end diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index a37fc120b86..5cc61333bb4 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -64,6 +64,10 @@ describe 'Merge request > User merges when pipeline succeeds', :js do before do click_button "Merge when pipeline succeeds" click_link "Cancel automatic merge" + + wait_for_requests + + expect(page).to have_content 'Merge when pipeline succeeds', wait: 0 end it_behaves_like 'Merge when pipeline succeeds activator' diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 23358d5cd67..87e467571e6 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -61,6 +61,28 @@ describe "Projects > Settings > Pipelines settings" do expect(checkbox).to be_checked end + it 'updates forward_deployment_enabled' do + visit project_settings_ci_cd_path(project) + + checkbox = find_field('project_forward_deployment_enabled') + expect(checkbox).to be_checked + + checkbox.set(false) + + page.within '#js-general-pipeline-settings' do + click_on 'Save changes' + end + + expect(page.status_code).to eq(200) + + page.within '#js-general-pipeline-settings' do + expect(page).to have_button('Save changes', disabled: false) + end + + checkbox = find_field('project_forward_deployment_enabled') + expect(checkbox).not_to be_checked + end + describe 'Auto DevOps' do context 'when auto devops is turned on instance-wide' do before do diff --git a/spec/fixtures/sentry/error_list_search_response.json b/spec/fixtures/sentry/error_list_search_response.json new file mode 100644 index 00000000000..e77c837b48c --- /dev/null +++ b/spec/fixtures/sentry/error_list_search_response.json @@ -0,0 +1,42 @@ +[{ + "lastSeen": "2018-12-31T12:00:11Z", + "numComments": 0, + "userCount": 0, + "stats": { + "24h": [ + [ + 1546437600, + 0 + ] + ] + }, + "culprit": "sentry.tasks.reports.deliver_organization_user_report", + "title": "NotFound desc = GetRepoPath: not a git repository", + "id": "13", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { + "type": "gaierror", + "value": "[Errno -2] Name or service not known" + }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": false, + "shortId": "INTERNAL-4", + "shareId": null, + "firstSeen": "2018-12-17T12:00:14Z", + "count": "17283712", + "permalink": "35.228.54.90/sentry/internal/issues/13/", + "level": "error", + "isSubscribed": true, + "isBookmarked": false, + "project": { + "slug": "internal", + "id": "1", + "name": "Internal" + }, + "statusDetails": {} +}] diff --git a/spec/fixtures/sentry/issues_sample_response.json b/spec/fixtures/sentry/issues_sample_response.json index ed22499cfa1..495562ac960 100644 --- a/spec/fixtures/sentry/issues_sample_response.json +++ b/spec/fixtures/sentry/issues_sample_response.json @@ -1,4 +1,5 @@ -[{ +[ + { "lastSeen": "2018-12-31T12:00:11Z", "numComments": 0, "userCount": 0, @@ -39,4 +40,47 @@ "name": "Internal" }, "statusDetails": {} - }] + }, + { + "lastSeen": "2018-12-31T12:00:11Z", + "numComments": 0, + "userCount": 0, + "stats": { + "24h": [ + [ + 1546437600, + 0 + ] + ] + }, + "culprit": "sentry.tasks.reports.deliver_organization_user_report", + "title": "NotFound desc = GetRepoPath: not a git repository", + "id": "13", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { + "type": "gaierror", + "value": "GetRepoPath: not a git repository" + }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": false, + "shortId": "INTERNAL-4", + "shareId": null, + "firstSeen": "2018-12-17T12:00:14Z", + "count": "17283712", + "permalink": "35.228.54.90/sentry/internal/issues/13/", + "level": "error", + "isSubscribed": true, + "isBookmarked": false, + "project": { + "slug": "internal", + "id": "1", + "name": "Internal" + }, + "statusDetails": {} + } +] diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js new file mode 100644 index 00000000000..7723af07d8c --- /dev/null +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -0,0 +1,155 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlDropdown } from '@gitlab/ui'; +import { TEST_HOST } from 'spec/test_constants'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; +import boardsStore from '~/boards/stores/boards_store'; + +const throttleDuration = 1; + +function boardGenerator(n) { + return new Array(n).fill().map((board, id) => { + const name = `board${id}`; + + return { + id, + name, + }; + }); +} + +describe('BoardsSelector', () => { + let wrapper; + let allBoardsResponse; + let recentBoardsResponse; + const boards = boardGenerator(20); + const recentBoards = boardGenerator(5); + + const fillSearchBox = filterTerm => { + const searchBox = wrapper.find({ ref: 'searchBox' }); + const searchBoxInput = searchBox.find('input'); + searchBoxInput.setValue(filterTerm); + searchBoxInput.trigger('input'); + }; + + const getDropdownItems = () => wrapper.findAll('.js-dropdown-item'); + const getDropdownHeaders = () => wrapper.findAll('.dropdown-bold-header'); + + beforeEach(() => { + boardsStore.setEndpoints({ + boardsEndpoint: '', + recentBoardsEndpoint: '', + listsEndpoint: '', + bulkUpdatePath: '', + boardId: '', + }); + + allBoardsResponse = Promise.resolve({ + data: boards, + }); + recentBoardsResponse = Promise.resolve({ + data: recentBoards, + }); + + boardsStore.allBoards = jest.fn(() => allBoardsResponse); + boardsStore.recentBoards = jest.fn(() => recentBoardsResponse); + + const Component = Vue.extend(BoardsSelector); + wrapper = mount(Component, { + propsData: { + throttleDuration, + currentBoard: { + id: 1, + name: 'Development', + milestone_id: null, + weight: null, + assignee_id: null, + labels: [], + }, + milestonePath: `${TEST_HOST}/milestone/path`, + boardBaseUrl: `${TEST_HOST}/board/base/url`, + hasMissingBoards: false, + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + labelsPath: `${TEST_HOST}/labels/path`, + projectId: 42, + groupId: 19, + scopedIssueBoardFeatureEnabled: true, + weights: [], + }, + attachToDocument: true, + }); + + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + wrapper.find(GlDropdown).vm.$emit('show'); + + return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => Vue.nextTick()); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('filtering', () => { + it('shows all boards without filtering', () => { + expect(getDropdownItems().length).toBe(boards.length + recentBoards.length); + }); + + it('shows only matching boards when filtering', () => { + const filterTerm = 'board1'; + const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; + + fillSearchBox(filterTerm); + + return Vue.nextTick().then(() => { + expect(getDropdownItems().length).toBe(expectedCount); + }); + }); + + it('shows message if there are no matching boards', () => { + fillSearchBox('does not exist'); + + return Vue.nextTick().then(() => { + expect(getDropdownItems().length).toBe(0); + expect(wrapper.text().includes('No matching boards found')).toBe(true); + }); + }); + }); + + describe('recent boards section', () => { + it('shows only when boards are greater than 10', () => { + const expectedCount = 2; // Recent + All + + expect(getDropdownHeaders().length).toBe(expectedCount); + }); + + it('does not show when boards are less than 10', () => { + wrapper.setData({ + boards: boards.slice(0, 5), + }); + + return Vue.nextTick().then(() => { + expect(getDropdownHeaders().length).toBe(0); + }); + }); + + it('does not show when recentBoards api returns empty array', () => { + wrapper.setData({ + recentBoards: [], + }); + + return Vue.nextTick().then(() => { + expect(getDropdownHeaders().length).toBe(0); + }); + }); + + it('does not show when search is active', () => { + fillSearchBox('Random string'); + + return Vue.nextTick().then(() => { + expect(getDropdownHeaders().length).toBe(0); + }); + }); + }); +}); diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index 94bf0189c91..e43f9569ffc 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -1,8 +1,15 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import { __ } from '~/locale'; -import { GlLoadingIcon, GlLink, GlBadge, GlFormInput, GlAlert, GlSprintf } from '@gitlab/ui'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { + GlButton, + GlLoadingIcon, + GlLink, + GlBadge, + GlFormInput, + GlAlert, + GlSprintf, +} from '@gitlab/ui'; import Stacktrace from '~/error_tracking/components/stacktrace.vue'; import ErrorDetails from '~/error_tracking/components/error_details.vue'; import { @@ -28,7 +35,7 @@ describe('ErrorDetails', () => { function mountComponent() { wrapper = shallowMount(ErrorDetails, { - stubs: { LoadingButton, GlSprintf }, + stubs: { GlButton, GlSprintf }, localVue, store, mocks, @@ -127,7 +134,7 @@ describe('ErrorDetails', () => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(Stacktrace).exists()).toBe(false); expect(wrapper.find(GlBadge).exists()).toBe(false); - expect(wrapper.findAll('button').length).toBe(3); + expect(wrapper.findAll(GlButton).length).toBe(3); }); describe('Badges', () => { @@ -226,7 +233,7 @@ describe('ErrorDetails', () => { it('should submit the form', () => { window.HTMLFormElement.prototype.submit = () => {}; const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit'); - wrapper.find('[data-qa-selector="create_issue_button"]').trigger('click'); + wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click'); expect(submitSpy).toHaveBeenCalled(); submitSpy.mockRestore(); }); @@ -255,14 +262,14 @@ describe('ErrorDetails', () => { }); it('marks error as ignored when ignore button is clicked', () => { - findUpdateIgnoreStatusButton().trigger('click'); + findUpdateIgnoreStatusButton().vm.$emit('click'); expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.IGNORED }), ); }); it('marks error as resolved when resolve button is clicked', () => { - findUpdateResolveStatusButton().trigger('click'); + findUpdateResolveStatusButton().vm.$emit('click'); expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.RESOLVED }), ); @@ -281,14 +288,14 @@ describe('ErrorDetails', () => { }); it('marks error as unresolved when ignore button is clicked', () => { - findUpdateIgnoreStatusButton().trigger('click'); + findUpdateIgnoreStatusButton().vm.$emit('click'); expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.UNRESOLVED }), ); }); it('marks error as resolved when resolve button is clicked', () => { - findUpdateResolveStatusButton().trigger('click'); + findUpdateResolveStatusButton().vm.$emit('click'); expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.RESOLVED }), ); @@ -307,14 +314,14 @@ describe('ErrorDetails', () => { }); it('marks error as ignored when ignore button is clicked', () => { - findUpdateIgnoreStatusButton().trigger('click'); + findUpdateIgnoreStatusButton().vm.$emit('click'); expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.IGNORED }), ); }); it('marks error as unresolved when unresolve button is clicked', () => { - findUpdateResolveStatusButton().trigger('click'); + findUpdateResolveStatusButton().vm.$emit('click'); expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual( expect.objectContaining({ status: errorStatus.UNRESOLVED }), ); diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index b632b461eb9..f852a3091aa 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -42,9 +42,6 @@ describe('ErrorTrackingList', () => { ...stubChildren(ErrorTrackingList), ...stubs, }, - data() { - return { errorSearchQuery: 'search' }; - }, }); } @@ -164,8 +161,9 @@ describe('ErrorTrackingList', () => { }); it('it searches by query', () => { + findSearchBox().vm.$emit('input', 'search'); findSearchBox().trigger('keyup.enter'); - expect(actions.searchByQuery.mock.calls[0][1]).toEqual(wrapper.vm.errorSearchQuery); + expect(actions.searchByQuery.mock.calls[0][1]).toBe('search'); }); it('it sorts by fields', () => { diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index c705270343b..77f7f2e0609 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -72,7 +72,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` </gl-form-group-stub> <gl-form-group-stub - class="col-sm-6 col-md-6 col-lg-4" + class="col-sm-auto col-md-auto col-lg-auto" label="Show last" label-for="monitor-time-window-dropdown" label-size="sm" @@ -83,6 +83,21 @@ exports[`Dashboard template matches the default snapshot 1`] = ` /> </gl-form-group-stub> + <gl-form-group-stub + class="col-sm-2 col-md-2 col-lg-1 refresh-dashboard-button" + > + <gl-button-stub + size="md" + title="Reload this page" + variant="default" + > + <icon-stub + name="repeat" + size="16" + /> + </gl-button-stub> + </gl-form-group-stub> + <!----> </div> </div> diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js new file mode 100644 index 00000000000..d219a6627bf --- /dev/null +++ b/spec/frontend/monitoring/components/charts/options_spec.js @@ -0,0 +1,60 @@ +import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; +import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options'; + +describe('options spec', () => { + describe('getYAxisOptions', () => { + it('default options', () => { + const options = getYAxisOptions(); + + expect(options).toMatchObject({ + name: expect.any(String), + axisLabel: { + formatter: expect.any(Function), + }, + scale: true, + boundaryGap: [expect.any(Number), expect.any(Number)], + }); + + expect(options.name).not.toHaveLength(0); + }); + + it('name options', () => { + const yAxisName = 'My axis values'; + const options = getYAxisOptions({ + name: yAxisName, + }); + + expect(options).toMatchObject({ + name: yAxisName, + nameLocation: 'center', + nameGap: expect.any(Number), + }); + }); + + it('formatter options', () => { + const options = getYAxisOptions({ + format: SUPPORTED_FORMATS.bytes, + }); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(1)).toBe('1.00B'); + }); + }); + + describe('getTooltipFormatter', () => { + it('default format', () => { + const formatter = getTooltipFormatter(); + + expect(formatter).toEqual(expect.any(Function)); + expect(formatter(1)).toBe('1.000'); + }); + + it('defined format', () => { + const formatter = getTooltipFormatter({ + format: SUPPORTED_FORMATS.bytes, + }); + + expect(formatter(1)).toBe('1.000B'); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index e9322d6b5a9..8dcb54e3fd9 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -190,7 +190,8 @@ describe('Time series component', () => { it('formats tooltip content', () => { const name = 'Total'; - const value = '5.556'; + const value = '5.556MB'; + const dataIndex = 0; const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); @@ -348,9 +349,9 @@ describe('Time series component', () => { }); }); - it('additional y axis data', () => { + it('additional y-axis data', () => { const mockCustomYAxisOption = { - name: 'Custom y axis label', + name: 'Custom y-axis label', axisLabel: { formatter: jest.fn(), }, @@ -397,8 +398,8 @@ describe('Time series component', () => { deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; }); - it('rounds to 3 decimal places', () => { - expect(dataFormatter(0.88888)).toBe('0.889'); + it('formats and rounds to 2 decimal places', () => { + expect(dataFormatter(0.88888)).toBe('0.89MB'); }); it('deployment formatter is set as is required to display a tooltip', () => { @@ -421,7 +422,7 @@ describe('Time series component', () => { }); describe('yAxisLabel', () => { - it('y axis is configured correctly', () => { + it('y-axis is configured correctly', () => { const { yAxis } = getChartOptions(); expect(yAxis).toHaveLength(2); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 6f05207204e..bec22b28a5c 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -214,6 +214,19 @@ describe('Dashboard', () => { }); }); + it('renders the refresh dashboard button', () => { + createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); + + setupComponentStore(wrapper); + + return wrapper.vm.$nextTick().then(() => { + const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' }); + + expect(refreshBtn).toHaveLength(1); + expect(refreshBtn.is(GlButton)).toBe(true); + }); + }); + describe('when one of the metrics is missing', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 32daf990ad3..60b1510973d 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -393,13 +393,16 @@ export const metricsDashboardPayload = { type: 'area-chart', y_label: 'Total Memory Used', weight: 4, + y_axis: { + format: 'megabytes', + }, metrics: [ { id: 'system_metrics_kubernetes_container_memory_total', query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1000/1000', label: 'Total', - unit: 'GB', + unit: 'MB', metric_id: 12, prometheus_endpoint_path: 'http://test', }, diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 57418e90470..2bd8af9b7d5 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -1,3 +1,4 @@ +import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { uniqMetricsId, parseEnvironmentsResponse, @@ -44,6 +45,11 @@ describe('mapToDashboardViewModel', () => { title: 'Title A', type: 'chart-type', y_label: 'Y Label A', + yAxis: { + name: 'Y Label A', + format: 'number', + precision: 2, + }, metrics: [], }, ], @@ -90,6 +96,98 @@ describe('mapToDashboardViewModel', () => { }); }); + describe('panel mapping', () => { + const panelTitle = 'Panel Title'; + const yAxisName = 'Y Axis Name'; + + let dashboard; + + const setupWithPanel = panel => { + dashboard = { + panel_groups: [ + { + panels: [panel], + }, + ], + }; + }; + + const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0]; + + it('group y_axis defaults', () => { + setupWithPanel({ + title: panelTitle, + }); + + expect(getMappedPanel()).toEqual({ + title: panelTitle, + y_label: '', + yAxis: { + name: '', + format: SUPPORTED_FORMATS.number, + precision: 2, + }, + metrics: [], + }); + }); + + it('panel with y_axis.name', () => { + setupWithPanel({ + y_axis: { + name: yAxisName, + }, + }); + + expect(getMappedPanel().y_label).toBe(yAxisName); + expect(getMappedPanel().yAxis.name).toBe(yAxisName); + }); + + it('panel with y_axis.name and y_label, displays y_axis.name', () => { + setupWithPanel({ + y_label: 'Ignored Y Label', + y_axis: { + name: yAxisName, + }, + }); + + expect(getMappedPanel().y_label).toBe(yAxisName); + expect(getMappedPanel().yAxis.name).toBe(yAxisName); + }); + + it('group y_label', () => { + setupWithPanel({ + y_label: yAxisName, + }); + + expect(getMappedPanel().y_label).toBe(yAxisName); + expect(getMappedPanel().yAxis.name).toBe(yAxisName); + }); + + it('group y_axis format and precision', () => { + setupWithPanel({ + title: panelTitle, + y_axis: { + precision: 0, + format: SUPPORTED_FORMATS.bytes, + }, + }); + + expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.bytes); + expect(getMappedPanel().yAxis.precision).toBe(0); + }); + + it('group y_axis unsupported format defaults to number', () => { + setupWithPanel({ + title: panelTitle, + y_axis: { + format: 'invalid_format', + }, + }); + + expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.number); + }); + }); + describe('metrics mapping', () => { const defaultLabel = 'Panel Label'; const dashboardWithMetric = (metric, label = defaultLabel) => ({ diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js deleted file mode 100644 index 16ec3b801cd..00000000000 --- a/spec/javascripts/boards/components/boards_selector_spec.js +++ /dev/null @@ -1,203 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { TEST_HOST } from 'spec/test_constants'; -import BoardsSelector from '~/boards/components/boards_selector.vue'; -import boardsStore from '~/boards/stores/boards_store'; - -const throttleDuration = 1; - -function boardGenerator(n) { - return new Array(n).fill().map((board, id) => { - const name = `board${id}`; - - return { - id, - name, - }; - }); -} - -describe('BoardsSelector', () => { - let vm; - let allBoardsResponse; - let recentBoardsResponse; - let fillSearchBox; - const boards = boardGenerator(20); - const recentBoards = boardGenerator(5); - - beforeEach(done => { - setFixtures('<div class="js-boards-selector"></div>'); - window.gl = window.gl || {}; - - boardsStore.setEndpoints({ - boardsEndpoint: '', - recentBoardsEndpoint: '', - listsEndpoint: '', - bulkUpdatePath: '', - boardId: '', - }); - - allBoardsResponse = Promise.resolve({ - data: boards, - }); - recentBoardsResponse = Promise.resolve({ - data: recentBoards, - }); - - spyOn(boardsStore, 'allBoards').and.returnValue(allBoardsResponse); - spyOn(boardsStore, 'recentBoards').and.returnValue(recentBoardsResponse); - - const Component = Vue.extend(BoardsSelector); - vm = mountComponent( - Component, - { - throttleDuration, - currentBoard: { - id: 1, - name: 'Development', - milestone_id: null, - weight: null, - assignee_id: null, - labels: [], - }, - milestonePath: `${TEST_HOST}/milestone/path`, - boardBaseUrl: `${TEST_HOST}/board/base/url`, - hasMissingBoards: false, - canAdminBoard: true, - multipleIssueBoardsAvailable: true, - labelsPath: `${TEST_HOST}/labels/path`, - projectId: 42, - groupId: 19, - scopedIssueBoardFeatureEnabled: true, - weights: [], - }, - document.querySelector('.js-boards-selector'), - ); - - // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time - vm.$children[0].$emit('show'); - - Promise.all([allBoardsResponse, recentBoardsResponse]) - .then(() => vm.$nextTick()) - .then(done) - .catch(done.fail); - - fillSearchBox = filterTerm => { - const { searchBox } = vm.$refs; - const searchBoxInput = searchBox.$el.querySelector('input'); - searchBoxInput.value = filterTerm; - searchBoxInput.dispatchEvent(new Event('input')); - }; - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('filtering', () => { - it('shows all boards without filtering', done => { - vm.$nextTick() - .then(() => { - const dropdownItem = vm.$el.querySelectorAll('.js-dropdown-item'); - - expect(dropdownItem.length).toBe(boards.length + recentBoards.length); - }) - .then(done) - .catch(done.fail); - }); - - it('shows only matching boards when filtering', done => { - const filterTerm = 'board1'; - const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length; - - fillSearchBox(filterTerm); - - vm.$nextTick() - .then(() => { - const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); - - expect(dropdownItems.length).toBe(expectedCount); - }) - .then(done) - .catch(done.fail); - }); - - it('shows message if there are no matching boards', done => { - fillSearchBox('does not exist'); - - vm.$nextTick() - .then(() => { - const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item'); - - expect(dropdownItems.length).toBe(0); - expect(vm.$el).toContainText('No matching boards found'); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('recent boards section', () => { - it('shows only when boards are greater than 10', done => { - vm.$nextTick() - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - - const expectedCount = 2; // Recent + All - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not show when boards are less than 10', done => { - spyOn(vm, 'initScrollFade'); - spyOn(vm, 'setScrollFade'); - - vm.$nextTick() - .then(() => { - vm.boards = vm.boards.slice(0, 5); - }) - .then(vm.$nextTick) - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - const expectedCount = 0; - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not show when recentBoards api returns empty array', done => { - vm.$nextTick() - .then(() => { - vm.recentBoards = []; - }) - .then(vm.$nextTick) - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - const expectedCount = 0; - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not show when search is active', done => { - fillSearchBox('Random string'); - - vm.$nextTick() - .then(() => { - const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header'); - const expectedCount = 0; - - expect(expectedCount).toBe(headerEls.length); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb index 2762c5b5cb9..d35e4b83d7f 100644 --- a/spec/lib/sentry/client/issue_spec.rb +++ b/spec/lib/sentry/client/issue_spec.rb @@ -49,7 +49,7 @@ describe Sentry::Client::Issue do it_behaves_like 'calls sentry api' it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error - it_behaves_like 'issues have correct length', 1 + it_behaves_like 'issues have correct length', 2 shared_examples 'has correct external_url' do context 'external_url' do @@ -184,7 +184,7 @@ describe Sentry::Client::Issue do it_behaves_like 'calls sentry api' it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error - it_behaves_like 'issues have correct length', 1 + it_behaves_like 'issues have correct length', 2 end context 'when cursor is present' do @@ -194,7 +194,7 @@ describe Sentry::Client::Issue do it_behaves_like 'calls sentry api' it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error - it_behaves_like 'issues have correct length', 1 + it_behaves_like 'issues have correct length', 2 end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index c086ab23058..754e26dcec8 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -102,7 +102,7 @@ describe Clusters::Applications::Ingress do it 'is initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') - expect(subject.version).to eq('1.29.3') + expect(subject.version).to eq('1.29.7') expect(subject).to be_rbac expect(subject.files).to eq(ingress.files) end @@ -119,7 +119,7 @@ describe Clusters::Applications::Ingress do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } it 'is initialized with the locked version' do - expect(subject.version).to eq('1.29.3') + expect(subject.version).to eq('1.29.7') end end end @@ -135,6 +135,7 @@ describe Clusters::Applications::Ingress do expect(values).to include('repository') expect(values).to include('stats') expect(values).to include('podAnnotations') + expect(values).to include('clusterIP') end end diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb new file mode 100644 index 00000000000..3b6de5bb390 --- /dev/null +++ b/spec/models/user_detail_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UserDetail do + it { is_expected.to belong_to(:user) } + + describe 'validations' do + describe 'job_title' do + it { is_expected.to validate_presence_of(:job_title) } + it { is_expected.to validate_length_of(:job_title).is_at_most(200) } + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 68cf41ce8a4..b8abe698639 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -29,6 +29,7 @@ describe User, :do_not_mock_admin_mode do it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:status) } it { is_expected.to have_one(:max_access_level_membership) } + it { is_expected.to have_one(:user_detail) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } it { is_expected.to have_many(:project_members) } @@ -4318,4 +4319,19 @@ describe User, :do_not_mock_admin_mode do expect(user.hook_attrs).to eq(user_attributes) end end + + describe 'user detail' do + context 'when user is initialized' do + let(:user) { build(:user) } + + it { expect(user.user_detail).to be_present } + it { expect(user.user_detail).not_to be_persisted } + end + + context 'when user detail exists' do + let(:user) { create(:user, job_title: 'Engineer') } + + it { expect(user.user_detail).to be_persisted } + end + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5a302f0528e..73bdd3b5d96 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -330,6 +330,21 @@ describe API::Users, :do_not_mock_admin_mode do expect(json_response.keys).not_to include 'last_sign_in_ip' end + context 'when job title is present' do + let(:job_title) { 'Fullstack Engineer' } + + before do + create(:user_detail, user: user, job_title: job_title) + end + + it 'returns job title of a user' do + get api("/users/#{user.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response['job_title']).to eq(job_title) + end + end + context 'when authenticated as admin' do it 'includes the `is_admin` field' do get api("/users/#{user.id}", admin) diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb index fdbed4fa5d8..d487edd8850 100644 --- a/spec/services/clusters/update_service_spec.rb +++ b/spec/services/clusters/update_service_spec.rb @@ -86,7 +86,7 @@ describe Clusters::UpdateService do it 'rejects changes' do is_expected.to eq(false) - expect(cluster.errors.full_messages).to include('cannot modify during creation') + expect(cluster.errors.full_messages).to include('Cannot modify provider during creation') end end end diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index 5cd6283ca96..24738a79045 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -64,6 +64,13 @@ describe Users::UpdateService do end.not_to change { user.name } end + it 'updates user detail with provided attributes' do + result = update_user(user, job_title: 'Backend Engineer') + + expect(result).to eq(status: :success) + expect(user.job_title).to eq('Backend Engineer') + end + def update_user(user, opts) described_class.new(user, opts.merge(user: user)).execute end diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index 30ba8d9b436..53183ac89f8 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -109,7 +109,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| end expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) + expect(json_response['message']['base'].first).to eq(_('Time to subtract exceeds the total time spent')) end end end diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml index 0baaccedc4e..681d10d938a 100644 --- a/vendor/ingress/values.yaml +++ b/vendor/ingress/values.yaml @@ -6,3 +6,8 @@ controller: podAnnotations: prometheus.io/scrape: "true" prometheus.io/port: "10254" + service: + clusterIP: "-" +defaultBackend: + service: + clusterIP: "-" |