Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f321e51f46
commit
87ef501eac
86 changed files with 1040 additions and 334 deletions
|
@ -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
|
||||
|
|
|
@ -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…')"
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
@ -74,18 +75,38 @@ const mapToMetricsViewModel = (metrics, defaultLabel) =>
|
|||
...metric,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ export default {
|
|||
.then(res => res.data)
|
||||
.then(data => {
|
||||
eventHub.$emit('UpdateWidgetData', data);
|
||||
eventHub.$emit('MRWidgetUpdateRequested');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isCancellingAutoMerge = false;
|
||||
|
|
|
@ -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;
|
||||
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.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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
:private_profile,
|
||||
:include_private_contributions,
|
||||
:timezone,
|
||||
:job_title,
|
||||
status: [:emoji, :message]
|
||||
)
|
||||
end
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
7
app/models/user_detail.rb
Normal file
7
app/models/user_detail.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserDetail < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :job_title, presence: true, length: { maximum: 200 }
|
||||
end
|
|
@ -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'
|
||||
|
|
5
changelogs/unreleased/201999-define-formatter-y-axis.yml
Normal file
5
changelogs/unreleased/201999-define-formatter-y-axis.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add properties to the dashboard definition to customize y-axis format
|
||||
merge_request: 25785
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/207203-forward-deployment-ui.yml
Normal file
5
changelogs/unreleased/207203-forward-deployment-ui.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added Drop older active deployments project setting
|
||||
merge_request: 25520
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Issue Analytics: Fix svg illustration path for empty state'
|
||||
merge_request: 26219
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/add-user-job-title-column.yml
Normal file
5
changelogs/unreleased/add-user-job-title-column.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add support for user Job Title
|
||||
merge_request: 25483
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add refresh dashboard button
|
||||
merge_request: 25716
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/lm-fix-error-query.yml
Normal file
5
changelogs/unreleased/lm-fix-error-query.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix search for Sentry error list
|
||||
merge_request: 26129
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refresh widget after canceling "Merge When Pipeline Succeeds"
|
||||
merge_request: 26232
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/update_ingress_chart_version.yml
Normal file
5
changelogs/unreleased/update_ingress_chart_version.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Update Ingress chart version to 1.29.7'
|
||||
merge_request: 25949
|
||||
author:
|
||||
type: added
|
|
@ -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"
|
||||
|
|
18
db/migrate/20200227165129_create_user_details.rb
Normal file
18
db/migrate/20200227165129_create_user_details.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
8
spec/factories/user_details.rb
Normal file
8
spec/factories/user_details.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :user_detail do
|
||||
user
|
||||
job_title { 'VP of Sales' }
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
42
spec/fixtures/sentry/error_list_search_response.json
vendored
Normal file
42
spec/fixtures/sentry/error_list_search_response.json
vendored
Normal file
|
@ -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": {}
|
||||
}]
|
48
spec/fixtures/sentry/issues_sample_response.json
vendored
48
spec/fixtures/sentry/issues_sample_response.json
vendored
|
@ -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": {}
|
||||
}
|
||||
]
|
||||
|
|
155
spec/frontend/boards/components/boards_selector_spec.js
Normal file
155
spec/frontend/boards/components/boards_selector_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 }),
|
||||
);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
60
spec/frontend/monitoring/components/charts/options_spec.js
Normal file
60
spec/frontend/monitoring/components/charts/options_spec.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
14
spec/models/user_detail_spec.rb
Normal file
14
spec/models/user_detail_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
5
vendor/ingress/values.yaml
vendored
5
vendor/ingress/values.yaml
vendored
|
@ -6,3 +6,8 @@ controller:
|
|||
podAnnotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "10254"
|
||||
service:
|
||||
clusterIP: "-"
|
||||
defaultBackend:
|
||||
service:
|
||||
clusterIP: "-"
|
||||
|
|
Loading…
Reference in a new issue