Commit 874ead9c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 2e4c4055
......@@ -28,7 +28,7 @@ export default {
{{ s__('Badges|Your badges') }}
<span v-show="!isLoading" class="badge badge-pill">{{ badges.length }}</span>
</div>
<gl-loading-icon v-show="isLoading" :size="2" class="card-body" />
<gl-loading-icon v-show="isLoading" size="lg" class="card-body" />
<div v-if="hasNoBadges" class="card-body">
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
......
......@@ -197,7 +197,7 @@ export default {
<template>
<div>
<div v-if="loading" class="contributors-loader text-center">
<gl-loading-icon :inline="true" :size="4" />
<gl-loading-icon :inline="true" size="xl" />
</div>
<div v-else-if="showChart" class="contributors-charts">
......
......@@ -119,7 +119,7 @@ export default {
<gl-loading-icon
v-if="isLoading && !hasKeys"
:label="s__('DeployKeys|Loading deploy keys')"
:size="2"
size="lg"
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
......
......@@ -58,12 +58,6 @@ export default {
required: true,
},
shouldShowAutoStopDate: {
type: Boolean,
required: false,
default: false,
},
tableData: {
type: Object,
required: true,
......@@ -638,12 +632,7 @@ export default {
</span>
</div>
<div
v-if="!isFolder && shouldShowAutoStopDate"
class="table-section"
:class="tableData.autoStop.spacing"
role="gridcell"
>
<div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
<span
v-if="canShowAutoStopDate"
......@@ -662,10 +651,7 @@ export default {
role="gridcell"
>
<div class="btn-group table-action-buttons" role="group">
<pin-component
v-if="canShowAutoStopDate && shouldShowAutoStopDate"
:auto-stop-url="autoStopUrl"
/>
<pin-component v-if="canShowAutoStopDate" :auto-stop-url="autoStopUrl" />
<external-url-component
v-if="externalURL && canReadEnvironment"
......
......@@ -6,7 +6,6 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentItem from './environment_item.vue';
export default {
......@@ -17,7 +16,7 @@ export default {
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
},
mixins: [environmentTableMixin, glFeatureFlagsMixin()],
mixins: [environmentTableMixin],
props: {
environments: {
type: Array,
......@@ -43,9 +42,6 @@ export default {
: env,
);
},
shouldShowAutoStopDate() {
return this.glFeatures.autoStopEnvironments;
},
tableData() {
return {
// percent spacing for cols, should add up to 100
......@@ -74,7 +70,7 @@ export default {
spacing: 'section-5',
},
actions: {
spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30',
spacing: 'section-25',
},
};
},
......@@ -131,12 +127,7 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }}
</div>
<div
v-if="shouldShowAutoStopDate"
class="table-section"
:class="tableData.autoStop.spacing"
role="columnheader"
>
<div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
{{ tableData.autoStop.title }}
</div>
</div>
......@@ -146,7 +137,6 @@ export default {
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
:should-show-auto-stop-date="shouldShowAutoStopDate"
:table-data="tableData"
/>
......
......@@ -225,7 +225,7 @@ export default {
<template>
<div>
<div v-if="errorLoading" class="py-3">
<gl-loading-icon :size="3" />
<gl-loading-icon size="lg" />
</div>
<div v-else-if="error" class="error-details">
<gl-alert v-if="isAlertVisible" @dismiss="isAlertVisible = false">
......@@ -405,7 +405,7 @@ export default {
</ul>
<div v-if="loadingStacktrace" class="py-3">
<gl-loading-icon :size="3" />
<gl-loading-icon size="lg" />
</div>
<template v-else-if="showStacktrace">
......
......@@ -107,7 +107,7 @@ export default {
<gl-loading-icon
v-if="isLoadingItems"
:label="translations.loadingMessage"
:size="2"
size="lg"
class="loading-animation prepend-top-20"
/>
<div v-if="!isLoadingItems && !hasSearchQuery" class="section-header">
......
......@@ -72,7 +72,7 @@ export default {
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
v-if="isLoading"
:size="2"
size="lg"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
/>
<ul v-else class="mb-0 w-100">
......
......@@ -88,7 +88,7 @@ export default {
<i aria-hidden="true" class="fa fa-search dropdown-input-search"></i>
</div>
<div class="dropdown-content">
<gl-loading-icon v-if="showLoading" :size="2" />
<gl-loading-icon v-if="showLoading" size="lg" />
<ul v-else>
<li v-for="(item, index) in outputData" :key="index">
<button type="button" @click="clickItem(item)">{{ item.name }}</button>
......
......@@ -26,7 +26,7 @@ export default {
<template>
<div>
<gl-loading-icon v-if="loading && !stages.length" :size="2" class="prepend-top-default" />
<gl-loading-icon v-if="loading && !stages.length" size="lg" class="prepend-top-default" />
<template v-else>
<stage
v-for="stage in stages"
......
......@@ -90,7 +90,7 @@ export default {
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
v-if="isLoading"
:size="2"
size="lg"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
/>
<template v-else>
......
......@@ -56,7 +56,7 @@ export default {
<template>
<div class="ide-pipeline">
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
<gl-loading-icon v-if="showLoadingIcon" size="lg" class="prepend-top-default" />
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
......
......@@ -176,6 +176,6 @@ export default {
{{ s__('IDE|Get started with Live Preview') }}
</a>
</div>
<gl-loading-icon v-else :size="2" class="align-self-center mt-auto mb-auto" />
<gl-loading-icon v-else size="lg" class="align-self-center mt-auto mb-auto" />
</div>
</template>
......@@ -910,3 +910,18 @@ export const setCookie = (name, value) => Cookies.set(name, value, { expires: 36
export const getCookie = name => Cookies.get(name);
export const removeCookie = name => Cookies.remove(name);
/**
* Returns the status of a feature flag.
* Currently, there is no way to access feature
* flags in Vuex other than directly tapping into
* window.gon.
*
* This should only be used on Vuex. If feature flags
* need to be accessed in Vue components consider
* using the Vue feature flag mixin.
*
* @param {String} flag Feature flag
* @returns {Boolean} on/off
*/
export const isFeatureFlagEnabled = flag => window.gon.features?.[flag];
......@@ -55,6 +55,11 @@ export default {
required: false,
default: () => [],
},
annotations: {
type: Array,
required: false,
default: () => [],
},
projectPath: {
type: String,
required: false,
......@@ -143,6 +148,7 @@ export default {
return (this.option.series || []).concat(
generateAnnotationsSeries({
deployments: this.recentDeployments,
annotations: this.annotations,
}),
);
},
......
......@@ -213,7 +213,6 @@ export default {
'dashboard',
'emptyState',
'showEmptyState',
'deploymentData',
'useDashboardEndpoint',
'allDashboards',
'additionalPanelTypesEnabled',
......
......@@ -89,6 +89,9 @@ export default {
deploymentData(state) {
return state[this.namespace].deploymentData;
},
annotations(state) {
return state[this.namespace].annotations;
},
projectPath(state) {
return state[this.namespace].projectPath;
},
......@@ -310,6 +313,7 @@ export default {
ref="timeChart"
:graph-data="graphData"
:deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
......
query getAnnotations($projectPath: ID!) {
environment(name: $environmentName) {
metricDashboard(id: $dashboardId) {
annotations: nodes {
id
description
from
to
panelId
}
}
}
}
......@@ -6,8 +6,13 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import statusCodes from '../../lib/utils/http_status';
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import {
backOff,
convertObjectPropsToCamelCase,
isFeatureFlagEnabled,
} from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
......@@ -80,6 +85,14 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData');
dispatch('fetchDashboard');
/**
* Annotations data is not yet fetched. This will be
* ready after the BE piece is implemented.
* https://gitlab.com/gitlab-org/gitlab/-/issues/211330
*/
if (isFeatureFlagEnabled('metrics_dashboard_annotations')) {
dispatch('fetchAnnotations');
}
};
// Metrics dashboard
......@@ -269,6 +282,40 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
};
export const fetchAnnotations = ({ state, dispatch }) => {
dispatch('requestAnnotations');
return gqClient
.mutate({
mutation: getAnnotations,
variables: {
projectPath: removeLeadingSlash(state.projectPath),
dashboardId: state.currentDashboard,
environmentName: state.currentEnvironmentName,
},
})
.then(resp => resp.data?.project?.environment?.metricDashboard?.annotations)
.then(annotations => {
if (!annotations) {
createFlash(s__('Metrics|There was an error fetching annotations. Please try again.'));
}
dispatch('receiveAnnotationsSuccess', annotations);
})
.catch(err => {
Sentry.captureException(err);
dispatch('receiveAnnotationsFailure');
createFlash(s__('Metrics|There was an error getting annotations information.'));
});
};
// While this commit does not update the state it will
// eventually be useful to show a loading state
export const requestAnnotations = ({ commit }) => commit(types.REQUEST_ANNOTATIONS);
export const receiveAnnotationsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_ANNOTATIONS_SUCCESS, data);
export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_ANNOTATIONS_FAILURE);
// Dashboard manipulation
/**
......
......@@ -3,6 +3,11 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
// Annotations
export const REQUEST_ANNOTATIONS = 'REQUEST_ANNOTATIONS';
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
// Git project deployments
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
......
......@@ -92,6 +92,16 @@ export default {
state.environments = [];
},
/**
* Annotations
*/
[types.RECEIVE_ANNOTATIONS_SUCCESS](state, annotations) {
state.annotations = annotations;
},
[types.RECEIVE_ANNOTATIONS_FAILURE](state) {
state.annotations = [];
},
/**
* Individual panel/metric results
*/
......
......@@ -20,6 +20,7 @@ export default () => ({
allDashboards: [],
// Other project data
annotations: [],
deploymentData: [],
environments: [],
environmentsSearchTerm: '',
......
......@@ -135,7 +135,7 @@ export default {
paddingRight: `${graphRightPadding}px`,
}"
>
<gl-loading-icon v-if="isLoading" class="m-auto" :size="3" />
<gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
<pipeline-graph
v-if="pipelineTypeUpstream"
......
......@@ -108,7 +108,7 @@ export default {
/>
</ci-header>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" />
<gl-loading-icon v-if="isLoading" size="lg" class="prepend-top-default append-bottom-default" />
<gl-modal
:modal-id="$options.DELETE_MODAL_ID"
......
......@@ -271,7 +271,7 @@ export default {
<gl-loading-icon
v-if="stateToRender === $options.stateMap.loading"
:label="s__('Pipelines|Loading Pipelines')"
:size="3"
size="lg"
class="prepend-top-20"
/>
......
......@@ -94,7 +94,7 @@ export default {
</script>
<template>
<div class="ci-status-link">
<gl-loading-icon v-if="isLoading" :size="3" label="Loading pipeline status" />
<gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
<ci-icon
v-tooltip
......
......@@ -36,7 +36,7 @@ export default {
</div>
</div>
<div v-if="loadingStacktrace" class="card">
<gl-loading-icon class="py-2" label="Fetching stack trace" :size="1" />
<gl-loading-icon class="py-2" label="Fetching stack trace" size="sm" />
</div>
<stacktrace v-else :entries="stacktrace" />
</div>
......
......@@ -77,7 +77,7 @@ export default {
<section id="serverless-functions" class="flex-grow">
<gl-loading-icon
v-if="checkingInstalled"
:size="2"
size="lg"
class="prepend-top-default append-bottom-default"
/>
......@@ -97,7 +97,7 @@ export default {
</template>
<gl-loading-icon
v-if="isLoading"
:size="2"
size="lg"
class="prepend-top-default append-bottom-default js-functions-loader"
/>
</div>
......
......@@ -33,7 +33,7 @@ export default class SmartInterval {
this.state = {
intervalId: null,
currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible',
pagevisibile: true,
};
this.initInterval();
......@@ -91,8 +91,10 @@ export default class SmartInterval {
}
destroy() {
document.removeEventListener('visibilitychange', this.onVisibilityChange);
window.removeEventListener('blur', this.onWindowVisibilityChange);
window.removeEventListener('focus', this.onWindowVisibilityChange);
this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document)
.off('visibilitychange')
.off('beforeunload');
......@@ -124,9 +126,21 @@ export default class SmartInterval {
});
}
onWindowVisibilityChange(e) {
this.state.pagevisibile = e.type === 'focus';
this.handleVisibilityChange();
}
onVisibilityChange(e) {
this.state.pagevisibile = e.target.visibilityState === 'visible';
this.handleVisibilityChange();
}
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
// cancel interval when tab or window is no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this));
window.addEventListener('blur', this.onWindowVisibilityChange.bind(this));
window.addEventListener('focus', this.onWindowVisibilityChange.bind(this));
}
initPageUnloadHandling() {
......@@ -135,8 +149,7 @@ export default class SmartInterval {
$(document).on('beforeunload', () => this.cancel());
}
handleVisibilityChange(e) {
this.state.pageVisibility = e.target.visibilityState;
handleVisibilityChange() {
const intervalAction = this.isPageVisible()
? this.onVisibilityVisible
: this.onVisibilityHidden;
......@@ -166,7 +179,7 @@ export default class SmartInterval {
}
isPageVisible() {
return this.state.pageVisibility === 'visible';
return this.state.pagevisibile;
}
stopTimer() {
......
<script>
import GetSnippetQuery from '../queries/snippet.query.graphql';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { getSnippetMixin } from '../mixins/snippets';
export default {
components: {
SnippetHeader,
......@@ -12,33 +13,7 @@ export default {
GlLoadingIcon,
SnippetBlob,
},
apollo: {
snippet: {
query: GetSnippetQuery,
variables() {
return {
ids: this.snippetGid,
};
},
update: data => data.snippets.edges[0].node,
},
},
props: {
snippetGid: {
type: String,
required: true,
},
},
data() {
return {
snippet: {},
};
},
computed: {
isLoading() {
return this.$apollo.queries.snippet.loading;
},
},
mixins: [getSnippetMixin],
};
</script>
<template>
......@@ -46,7 +21,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
:label="__('Loading snippet')"
:size="2"
size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<template v-else>
......
......@@ -37,7 +37,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
:label="__('Loading snippet')"
:size="2"
size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<blob-content-edit
......
import GetSnippetQuery from '../queries/snippet.query.graphql';
export const getSnippetMixin = {
apollo: {
snippet: {
query: GetSnippetQuery,
variables() {
return {
ids: this.snippetGid,
};
},
update: data => data.snippets.edges[0]?.node,
result(res) {
if (this.onSnippetFetch) {
this.onSnippetFetch(res);
}
},
},
},
props: {
snippetGid: {
type: String,
required: true,
},
},
data() {
return {
snippet: {},
newSnippet: false,
};
},
computed: {
isLoading() {
return this.$apollo.queries.snippet.loading;
},
},
};
export default () => {};
......@@ -12,8 +12,8 @@ export default {
Toolbar,
},
computed: {
...mapState(['content', 'isLoadingContent', 'isSavingChanges']),
...mapGetters(['isContentLoaded', 'contentChanged']),
...mapState(['content', 'isLoadingContent', 'isSavingChanges', 'isContentLoaded']),
...mapGetters(['contentChanged']),
},
mounted() {
this.loadContent();
......
export const isContentLoaded = ({ originalContent }) => Boolean(originalContent);
// eslint-disable-next-line import/prefer-default-export
export const contentChanged = ({ originalContent, content }) => originalContent !== content;
......@@ -6,6 +6,7 @@ export default {
},
[types.RECEIVE_CONTENT_SUCCESS](state, { title, content }) {
state.isLoadingContent = false;
state.isContentLoaded = true;
state.title = title;
state.content = content;
state.originalContent = content;
......
......@@ -6,6 +6,8 @@ const createState = (initialState = {}) => ({
isLoadingContent: false,
isSavingChanges: false,
isContentLoaded: false,
originalContent: '',
content: '',
title: '',
......
......@@ -214,8 +214,6 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
if (document.visibilityState !== 'visible') return Promise.resolve();
return this.service
.checkStatus()
.then(({ data }) => {
......@@ -238,10 +236,10 @@ export default {
initPolling() {
this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
startingInterval: 10000,
maxInterval: 30000,
hiddenInterval: 120000,
incrementByFactorOf: 5000,
startingInterval: 10 * 1000,
maxInterval: 240 * 1000,
hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
incrementByFactorOf: 2,
});
},
initDeploymentsPolling() {
......@@ -253,10 +251,9 @@ export default {
deploymentsPoll(callback) {
return new SmartInterval({
callback,
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
startingInterval: 30 * 1000,
maxInterval: 240 * 1000,
incrementByFactorOf: 4,
immediateExecution: true,
});
},
......
......@@ -80,7 +80,7 @@ export default {
@input="onInput"
/>
<div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" :size="1" class="py-2 px-4" />
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll
:max-list-height="402"
:fetched-items="projectSearchResults.length"
......
......@@ -219,6 +219,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:domain_blacklist_file,
:raw_blob_request_limit,
:namespace_storage_size_limit,
:issues_create_limit,
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
......
......@@ -14,9 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts)
end
before_action do
push_frontend_feature_flag(:auto_stop_environments, default_enabled: true)
push_frontend_feature_flag(:metrics_dashboard_annotations)
end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
......
......@@ -42,6 +42,9 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_import_issues!, only: [:import_csv]
before_action :authorize_download_code!, only: [:related_branches]
# Limit the amount of issues created per minute
before_action :create_rate_limit, only: [:create]
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true)
......@@ -296,6 +299,22 @@ class Projects::IssuesController < Projects::ApplicationController
# 3. https://gitlab.com/gitlab-org/gitlab-foss/issues/42426
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42422')
end
private
def create_rate_limit
key = :issues_create
if rate_limiter.throttled?(key, scope: [@project, @current_user])
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
end
Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController')
......@@ -24,6 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
end
before_action do
......
......@@ -41,5 +41,16 @@ module Emails
subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'")
)
end
def pages_domain_auto_ssl_failed_email(domain, recipient)
@domain = domain
@project = domain.project
subject_text = _("ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'") % { domain: domain.domain }
mail(
to: recipient.notification_email_for(@project.group),
subject: subject(subject_text)
)
end
end
end
......@@ -79,6 +79,7 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
issues_create_limit: 300,
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
......
......@@ -73,12 +73,14 @@ module Ci
validates :file_format, presence: true, unless: :trace?, on: :create
validate :valid_file_format?, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
update_project_statistics project_statistics_name: :build_artifacts_size
before_save :set_size, if: :file_changed?
before_save :set_file_store, if: ->(job_artifact) { job_artifact.file_store.nil? }
after_save :update_file_store, if: :saved_change_to_file?
update_project_statistics project_statistics_name: :build_artifacts_size
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
......@@ -226,6 +228,15 @@ module Ci
self.size = file.size
end
def set_file_store
self.file_store =
if JobArtifactUploader.object_store_enabled? && JobArtifactUploader.direct_upload_enabled?
JobArtifactUploader::Store::REMOTE
else
file.object_store
end
end
def project_destroyed?
# Use job.project to avoid extra DB query for project
job.project.pending_delete?
......
# frozen_string_literal: true
class DiffNotePosition < ApplicationRecord
belongs_to :note
enum diff_content_type: {
text: 0,
image: 1
}
enum diff_type: {
head: 0
}
def position
Gitlab::Diff::Position.new(
old_path: old_path,
new_path: new_path,
old_line: old_line,
new_line: new_line,
position_type: diff_content_type,
diff_refs: Gitlab::Diff::DiffRefs.new(
base_sha: base_sha,
start_sha: start_sha,
head_sha: head_sha
)
)
end
def position=(position)
position_attrs = position.to_h
position_attrs[:diff_content_type] = position_attrs.delete(:position_type)
assign_attributes(position_attrs)
end
end
......@@ -17,6 +17,8 @@ class LfsObject < ApplicationRecord
mount_uploader :file, LfsObjectUploader
before_save :set_file_store, if: ->(lfs_object) { lfs_object.file_store.nil? }
after_save :update_file_store, if: :saved_change_to_file?
def self.not_linked_to_project(project)
......@@ -55,6 +57,17 @@ class LfsObject < ApplicationRecord
def self.calculate_oid(path)
self.hexdigest(path)
end
private
def set_file_store
self.file_store =
if LfsObjectUploader.object_store_enabled? && LfsObjectUploader.direct_upload_enabled?
LfsObjectUploader::Store::REMOTE
else
file.object_store
end
end
end
LfsObject.prepend_if_ee('EE::LfsObject')
......@@ -23,6 +23,8 @@ module Clusters
cluster.errors.add(:base, _('Instance does not support multiple Kubernetes clusters'))
end
validate_management_project_permissions(cluster)
return cluster if cluster.errors.present?
cluster.tap do |cluster|
......@@ -57,6 +59,11 @@ module Clusters
def can_create_cluster?
clusterable.clusters.empty?
end
def validate_management_project_permissions(cluster)
Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
.execute(cluster, params[:management_project_id])
end
end
end
......
# frozen_string_literal: true
module Clusters
module Management
class ValidateManagementProjectPermissionsService
attr_reader :current_user
def initialize(user = nil)
@current_user = user
end
def execute(cluster, management_project_id)
if management_project_id.present?
management_project = management_project_scope(cluster).find_by_id(management_project_id)
unless management_project && can_admin_pipeline_for_project?(management_project)
cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
return false
end
end
true
end
private
def can_admin_pipeline_for_project?(project)
Ability.allowed?(current_user, :admin_pipeline, project)
end
def management_project_scope(cluster)
return ::Project.all if cluster.instance_type?
group =
if cluster.group_type?
cluster.first_group
elsif cluster.project_type?
cluster.first_project&.namespace
end
# Prevent users from selecting nested projects until
# https://gitlab.com/gitlab-org/gitlab/issues/34650 is resolved
include_subgroups = cluster.group_type?
::GroupProjectsFinder.new(
group: group,
current_user: current_user,
options: { only_owned: true, include_subgroups: include_subgroups }
).execute
end
end
end
end
......@@ -18,46 +18,9 @@ module Clusters
private
def can_admin_pipeline_for_project?(project)
Ability.allowed?(current_user, :admin_pipeline, project)
end
def validate_params(cluster)
if params[:management_project_id].present?
management_project = management_project_scope(cluster).find_by_id(params[:management_project_id])
unless management_project
cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
return false
end
unless can_admin_pipeline_for_project?(management_project)
# Use same message as not found to prevent enumeration
cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
return false
end
end
true
end
def management_project_scope(cluster)
return ::Project.all if cluster.instance_type?
group =
if cluster.group_type?
cluster.first_group
elsif cluster.project_type?
cluster.first_project&.namespace
end
# Prevent users from selecting nested projects until
# https://gitlab.com/gitlab-org/gitlab/issues/34650 is resolved
include_subgroups = cluster.group_type?
::GroupProjectsFinder.new(group: group, current_user: current_user, options: { only_owned: true, include_subgroups: include_subgroups }).execute
::Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
.execute(cluster, params[:management_project_id])
end
end
end
......@@ -30,7 +30,7 @@ module Environments
def stop_in_batch
environments = Environment.auto_stoppable(BATCH_SIZE)
return false unless environments.exists? && Feature.enabled?(:auto_stop_environments, default_enabled: true)
return false unless environments.exists?
Ci::StopEnvironmentsService.execute_in_batch(environments)
end
......
......@@ -489,6 +489,12 @@ class NotificationService
end
end
def pages_domain_auto_ssl_failed(domain)
project_maintainers_recipients(domain, action: 'disabled').each do |recipient|
mailer.pages_domain_auto_ssl_failed_email(domain, recipient.user).deliver_later
end
end
def issue_due(issue)
recipients = NotificationRecipients::BuildService.build_recipients(
issue,
......
......@@ -57,6 +57,8 @@ module PagesDomains
pages_domain.save!(validate: false)
acme_order.destroy!
NotificationService.new.pages_domain_auto_ssl_failed(pages_domain)
end
def log_error(api_order)
......
......@@ -56,10 +56,31 @@ module RecordsUploads
size: file.size,
path: upload_path,
model: model,
mount_point: mounted_as
mount_point: mounted_as,
store: initial_store
)
end
def initial_store
if immediately_remote_stored?
::ObjectStorage::Store::REMOTE
else
::ObjectStorage::Store::LOCAL
end
end
def immediately_remote_stored?
object_storage_available? && direct_upload_enabled?
end
def object_storage_available?
self.class.ancestors.include?(ObjectStorage::Concern)
end
def direct_upload_enabled?
self.class.object_store_enabled? && self.class.direct_upload_enabled?
end
# Before removing an attachment, destroy any Upload records at the same path
#
# Called `before :remove`
......
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-issue-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :issues_create_limit, 'Max requests per second per user', class: 'label-bold'
= f.number_field :issues_create_limit, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' }
......@@ -46,4 +46,15 @@
.settings-content
= render 'protected_paths'
%section.settings.as-issue-limits.no-animate#js-issue-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Issues Rate Limits')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure limit for issues created per minute by web and API requests.')
.settings-content
= render 'issue_limits'
= render_if_exists 'admin/application_settings/ee_network_settings'
- page_title _('Deploy Keys')
%h3.page-title.deploy-keys-title
= _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.count }
= _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
.float-right
= link_to _('New deploy key'), new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted'
......
%p
= _("Something went wrong while obtaining the Let's Encrypt certificate.")
%p
#{_('Project')}: #{link_to @project.human_name, project_url(@project)}
%p
#{_('Domain')}: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
%p
- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_url }
- link_end = '</a>'.html_safe
= _("Please follow the %{link_start}Let\'s Encrypt troubleshooting instructions%{link_end} to re-obtain your Let's Encrypt certificate.").html_safe % { link_start: link_start, link_end: link_end }
= _("Something went wrong while obtaining the Let's Encrypt certificate.").html_safe
#{_('Project')}: #{project_url(@project)}
#{_('Domain')}: #{project_pages_domain_url(@project, @domain)}
- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
= _("Please follow the Let\'s Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}.").html_safe % { docs_url: docs_url }
......@@ -18,7 +18,7 @@
= f.submit _('Add email address'), class: 'btn btn-success', data: { qa_selector: 'add_email_address_button' }
%hr
%h4.prepend-top-0
= _('Linked emails (%{email_count})') % { email_count: @emails.count + 1 }
= _('Linked emails (%{email_count})') % { email_count: @emails.load.size + 1 }
.account-well.append-bottom-default
%ul
%li
......
- if @related_branches.any?
%h2.related-branches-title
= pluralize(@related_branches.count, 'Related Branch')
= pluralize(@related_branches.size, 'Related Branch')
%ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch|
%li
......
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- if can?(current_user, :update_pages, @project) && @domains.any?
- if can?(current_user, :update_pages, @project) && @domains.load.any?
.card
.card-header
Domains (#{@domains.count})
Domains (#{@domains.size})
%ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
- domain = Gitlab::View::Presenter::Factory.new(domain, current_user: current_user).fabricate!
......
......@@ -8,8 +8,6 @@ module Environments
feature_category :continuous_delivery
def perform
return unless Feature.enabled?(:auto_stop_environments, default_enabled: true)
AutoStopService.new.execute
end
end
......
---
title: Add management_project_id to group and project cluster creation, clarifies
docs.
merge_request: 28289
author:
type: fixed
---
title: 'fix: Publish toolbar dissappears when submitting empty content'
merge_request: 29410
author:
type: fixed
---
title: Add autostop check to folder table
merge_request: 28937
author:
type: fixed
---
title: Use NOT VALID to enforce a NOT NULL constraint on file_store to ci_job_artifacts,
lfs_objects and uploads tables
merge_request: 28946
author:
type: fixed
---
title: Optimize projects with repositories enabled usage data
merge_request: 29117
author:
type: performance
---
title: Introduce rate limit for creating issues via web UI
merge_request: 28129
author:
type: performance
---
title: Avoid scheduling duplicate sidekiq jobs
merge_request: 29116
author:
type: performance
---
title: Increase the timing of polling for the merge request widget
merge_request:
author:
type: changed
---
title: Replace deprecated GlLoadingIcon sizes
merge_request: 29417
author:
type: fixed
# frozen_string_literal: true
class AddIssuesCreateLimitToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :issues_create_limit, :integer, default: 300, null: false
end
end
# frozen_string_literal: true
class CreateDiffNotePositions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
create_table :diff_note_positions do |t|
t.references :note, foreign_key: { on_delete: :cascade }, null: false, index: false
t.integer :old_line
t.integer :new_line
t.integer :diff_content_type, limit: 2, null: false
t.integer :diff_type, limit: 2, null: false
t.string :line_code, limit: 255, null: false
t.binary :base_sha, null: false
t.binary :start_sha, null: false
t.binary :head_sha, null: false
t.text :old_path, null: false
t.text :new_path, null: false
t.index [:note_id, :diff_type], unique: true
end
end
end
def down
drop_table :diff_note_positions
end
end
# frozen_string_literal: true
class AddNotNullConstraintOnFileStoreToLfsObjects < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
CONSTRAINT_NAME = 'lfs_objects_file_store_not_null'
DOWNTIME = false
def up
with_lock_retries do
execute <<~SQL
ALTER TABLE lfs_objects ADD CONSTRAINT #{CONSTRAINT_NAME} CHECK (file_store IS NOT NULL) NOT VALID;
SQL
end
end
def down
with_lock_retries do
execute <<~SQL
ALTER TABLE lfs_objects DROP CONSTRAINT IF EXISTS #{CONSTRAINT_NAME};
SQL
end
end
end
# frozen_string_literal: true
class AddNotNullConstraintOnFileStoreToCiJobArtifacts < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
CONSTRAINT_NAME = 'ci_job_artifacts_file_store_not_null'
DOWNTIME = false
def up
with_lock_retries do
execute <<~SQL
ALTER TABLE ci_job_artifacts ADD CONSTRAINT #{CONSTRAINT_NAME} CHECK (file_store IS NOT NULL) NOT VALID;
SQL
end
end
def down
with_lock_retries do
execute <<~SQL
ALTER TABLE ci_job_artifacts DROP CONSTRAINT IF EXISTS #{CONSTRAINT_NAME};
SQL
end
end
end
# frozen_string_literal: true
class AddNotNullConstraintOnFileStoreToUploads < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
CONSTRAINT_NAME = 'uploads_store_not_null'
DOWNTIME = false
def up
with_lock_retries do
execute <<~SQL
ALTER TABLE uploads ADD CONSTRAINT #{CONSTRAINT_NAME} CHECK (store IS NOT NULL) NOT VALID;
SQL
end
end
def down
with_lock_retries do
execute <<~SQL
ALTER TABLE uploads DROP CONSTRAINT IF EXISTS #{CONSTRAINT_NAME};
SQL
end
end
end
# frozen_string_literal: true
class AddIndexOnCreatorIdAndIdOnProjects < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :projects, [:creator_id, :id]
end
def down
remove_concurrent_index :projects, [:creator_id, :id]
end
end
......@@ -397,6 +397,7 @@ CREATE TABLE public.application_settings (
email_restrictions text,
npm_package_requests_forwarding boolean DEFAULT true NOT NULL,
namespace_storage_size_limit bigint DEFAULT 0 NOT NULL,
issues_create_limit integer DEFAULT 300 NOT NULL,
seat_link_enabled boolean DEFAULT true NOT NULL,
container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL
);
......@@ -2138,6 +2139,30 @@ CREATE SEQUENCE public.design_user_mentions_id_seq
ALTER SEQUENCE public.design_user_mentions_id_seq OWNED BY public.design_user_mentions.id;
CREATE TABLE public.diff_note_positions (
id bigint NOT NULL,
note_id bigint NOT NULL,
old_line integer,
new_line integer,
diff_content_type smallint NOT NULL,
diff_type smallint NOT NULL,
line_code character varying(255) NOT NULL,
base_sha bytea NOT NULL,
start_sha bytea NOT NULL,
head_sha bytea NOT NULL,
old_path text NOT NULL,
new_path text NOT NULL
);
CREATE SEQUENCE public.diff_note_positions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.diff_note_positions_id_seq OWNED BY public.diff_note_positions.id;
CREATE TABLE public.draft_notes (
id bigint NOT NULL,
merge_request_id integer NOT NULL,
......@@ -7124,6 +7149,8 @@ ALTER TABLE ONLY public.design_management_versions ALTER COLUMN id SET DEFAULT n
ALTER TABLE ONLY public.design_user_mentions ALTER COLUMN id SET DEFAULT nextval('public.design_user_mentions_id_seq'::regclass);
ALTER TABLE ONLY public.diff_note_positions ALTER COLUMN id SET DEFAULT nextval('public.diff_note_positions_id_seq'::regclass);
ALTER TABLE ONLY public.draft_notes ALTER COLUMN id SET DEFAULT nextval('public.draft_notes_id_seq'::regclass);
ALTER TABLE ONLY public.emails ALTER COLUMN id SET DEFAULT nextval('public.emails_id_seq'::regclass);
......@@ -7670,6 +7697,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.ci_group_variables
ADD CONSTRAINT ci_group_variables_pkey PRIMARY KEY (id);
ALTER TABLE public.ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_file_store_not_null CHECK ((file_store IS NOT NULL)) NOT VALID;
ALTER TABLE ONLY public.ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id);
......@@ -7829,6 +7859,9 @@ ALTER TABLE ONLY public.design_management_versions
ALTER TABLE ONLY public.design_user_mentions
ADD CONSTRAINT design_user_mentions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.diff_note_positions
ADD CONSTRAINT diff_note_positions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.draft_notes
ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id);
......@@ -8024,6 +8057,9 @@ ALTER TABLE ONLY public.ldap_group_links
ALTER TABLE ONLY public.lfs_file_locks
ADD CONSTRAINT lfs_file_locks_pkey PRIMARY KEY (id);
ALTER TABLE public.lfs_objects
ADD CONSTRAINT lfs_objects_file_store_not_null CHECK ((file_store IS NOT NULL)) NOT VALID;
ALTER TABLE ONLY public.lfs_objects
ADD CONSTRAINT lfs_objects_pkey PRIMARY KEY (id);
......@@ -8417,6 +8453,9 @@ ALTER TABLE ONLY public.u2f_registrations
ALTER TABLE ONLY public.uploads
ADD CONSTRAINT uploads_pkey PRIMARY KEY (id);
ALTER TABLE public.uploads
ADD CONSTRAINT uploads_store_not_null CHECK ((store IS NOT NULL)) NOT VALID;
ALTER TABLE ONLY public.user_agent_details
ADD CONSTRAINT user_agent_details_pkey PRIMARY KEY (id);
......@@ -9086,6 +9125,8 @@ CREATE UNIQUE INDEX index_design_management_versions_on_sha_and_issue_id ON publ
CREATE UNIQUE INDEX index_design_user_mentions_on_note_id ON public.design_user_mentions USING btree (note_id);
CREATE UNIQUE INDEX index_diff_note_positions_on_note_id_and_diff_type ON public.diff_note_positions USING btree (note_id, diff_type);
CREATE INDEX index_draft_notes_on_author_id ON public.draft_notes USING btree (author_id);
CREATE INDEX index_draft_notes_on_discussion_id ON public.draft_notes USING btree (discussion_id);
......@@ -9886,6 +9927,8 @@ CREATE INDEX index_projects_on_creator_id_and_created_at ON public.projects USIN
CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON public.projects USING btree (creator_id, created_at, id);
CREATE INDEX index_projects_on_creator_id_and_id ON public.projects USING btree (creator_id, id);
CREATE INDEX index_projects_on_description_trigram ON public.projects USING gin (description public.gin_trgm_ops);
CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON public.projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false));
......@@ -11068,6 +11111,9 @@ ALTER TABLE ONLY public.project_statistics
ALTER TABLE ONLY public.user_details
ADD CONSTRAINT fk_rails_12e0b3043d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.diff_note_positions
ADD CONSTRAINT fk_rails_13c7212859 FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.users_security_dashboard_projects
ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -13064,10 +13110,12 @@ COPY "schema_migrations" (version) FROM STDIN;
20200323134519
20200324093258
20200324115359
20200325111432
20200325152327
20200325160952
20200325183636
20200326114443
20200326122700
20200326124443
20200326134443
20200326135443
......@@ -13090,10 +13138,14 @@ COPY "schema_migrations" (version) FROM STDIN;
20200403185127
20200403185422
20200406135648
20200406165950
20200406171857
20200406172135
20200406192059
20200407094005
20200407094923
20200408110856
20200408153842
20200408175424
\.
......@@ -224,6 +224,7 @@ Parameters:
| `cluster_id` | integer | yes | The ID of the cluster |
| `name` | string | no | The name of the cluster |
| `domain` | string | no | The [base domain](../user/group/clusters/index.md#base-domain) of the cluster |
| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster |
| `platform_kubernetes_attributes[api_url]` | string | no | The URL to access the Kubernetes API |
| `platform_kubernetes_attributes[token]` | string | no | The token to authenticate against Kubernetes |
| `platform_kubernetes_attributes[ca_cert]` | string | no | TLS certificate. Required if API is using a self-signed TLS certificate. |
......
......@@ -179,6 +179,7 @@ Parameters:
| `id` | integer | yes | The ID of the project owned by the authenticated user |
| `name` | string | yes | The name of the cluster |
| `domain` | string | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster |
| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster |
| `enabled` | boolean | no | Determines if cluster is active or not, defaults to true |
| `managed` | boolean | no | Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true |
| `platform_kubernetes_attributes[api_url]` | string | yes | The URL to access the Kubernetes API |
......
......@@ -54,6 +54,9 @@ When you are ready to send your code back to the upstream project,
[create a merge request](../merge_requests/creating_merge_requests.md). For **Source branch**,
choose your forked project's branch. For **Target branch**, choose the original project's branch.
NOTE: **Note:**
When creating a merge request, if the forked project's visibility is more restrictive than the parent project (for example the fork is private, parent is public), the target branch will default to the forked project's default branch. This prevents potentially exposing private code of the forked project.
![Selecting branches](img/forking_workflow_branch_select.png)
Then you can add labels, a milestone, and assign the merge request to someone who can review
......
......@@ -53,6 +53,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
......
......@@ -56,6 +56,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
......
......@@ -19,8 +19,9 @@ module Gitlab
# and only do that when it's needed.
def rate_limits
{
project_export: { threshold: 1, interval: 5.minutes },
project_download_export: { threshold: 10, interval: 10.minutes },
issues_create: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.issues_create_limit }, interval: 1.minute },
project_export: { threshold: 1, interval: 5.minutes },
project_download_export: { threshold: 10, interval: 10.minutes },
project_repositories_archive: { threshold: 5, interval: 1.minute },
project_generate_new_export: { threshold: 1, interval: 5.minutes },
project_import: { threshold: 30, interval: 5.minutes },
......
# frozen_string_literal: true
require 'digest'
module Gitlab
module SidekiqMiddleware
module DuplicateJobs
DROPPABLE_QUEUES = Set.new([
Namespaces::RootStatisticsWorker.queue,
Namespaces::ScheduleAggregationWorker.queue
]).freeze
def self.drop_duplicates?(queue_name)
Feature.enabled?(:drop_duplicate_sidekiq_jobs) ||
drop_duplicates_for_queue?(queue_name)
end
private_class_method def self.drop_duplicates_for_queue?(queue_name)
DROPPABLE_QUEUES.include?(queue_name) &&
Feature.enabled?(:drop_duplicate_sidekiq_jobs_for_queue)
end
end
end
end
......@@ -67,7 +67,7 @@ module Gitlab
end
def droppable?
idempotent? && duplicate? && DuplicateJobs.drop_duplicates?(queue_name)
idempotent? && duplicate?
end
private
......
......@@ -894,6 +894,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
msgid "API Help"
msgstr ""
......@@ -5334,6 +5337,9 @@ msgstr ""
msgid "Configure existing installation"
msgstr ""
msgid "Configure limit for issues created per minute by web and API requests."
msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
......@@ -11385,6 +11391,9 @@ msgstr ""
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
......@@ -12941,9 +12950,15 @@ msgstr ""
msgid "Metrics|There was an error creating the dashboard. %{error}"
msgstr ""
msgid "Metrics|There was an error fetching annotations. Please try again."
msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr ""
msgid "Metrics|There was an error getting annotations information."
msgstr ""
msgid "Metrics|There was an error getting deployment information."
msgstr ""
......@@ -14900,6 +14915,12 @@ msgstr ""
msgid "Please fill in a descriptive name for your group."
msgstr ""
msgid "Please follow the %{link_start}Let's Encrypt troubleshooting instructions%{link_end} to re-obtain your Let's Encrypt certificate."
msgstr ""
msgid "Please follow the Let's Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}."
msgstr ""
msgid "Please migrate all existing projects to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
msgstr ""
......@@ -18939,6 +18960,9 @@ msgstr ""
msgid "Something went wrong while moving issues."
msgstr ""
msgid "Something went wrong while obtaining the Let's Encrypt certificate."
msgstr ""
msgid "Something went wrong while performing the action."
msgstr ""
......
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
class ARCountEach < RuboCop::Cop::Cop
def message(ivar)
"If #{ivar} is AR relation, avoid `#{ivar}.count ...; #{ivar}.each... `, this will trigger two queries. " \
"Use `#{ivar}.load.size ...; #{ivar}.each... ` instead. If #{ivar} is an array, try to use #{ivar}.size."
end
def_node_matcher :count_match, <<~PATTERN
(send (ivar $_) :count)
PATTERN
def_node_matcher :each_match, <<~PATTERN
(send (ivar $_) :each)
PATTERN
def file_name(node)
node.location.expression.source_buffer.name
end
def in_haml_file?(node)
file_name(node).end_with?('.haml.rb')
end
def on_send(node)
return unless in_haml_file?(node)
ivar_count = count_match(node)
return unless ivar_count
node.each_ancestor(:begin) do |begin_node|
begin_node.each_descendant do |n|
ivar_each = each_match(n)
add_offense(node, location: :expression, message: message(ivar_count)) if ivar_each == ivar_count
end
end
end
end
end
end
end
......@@ -1085,6 +1085,48 @@ describe Projects::IssuesController do
expect { subject }.to change(SentryIssue, :count)
end
end
context 'when the endpoint receives requests above the limit' do
before do
stub_application_setting(issues_create_limit: 5)
end
it 'prevents from creating more issues', :request_store do
5.times { post_new_issue }
expect { post_new_issue }
.to change { Gitlab::GitalyClient.get_request_count }.by(1) # creates 1 projects and 0 issues
post_new_issue
expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
expect(response).to have_gitlab_http_status(:too_many_requests)
end
it 'logs the event on auth.log' do
attributes = {
message: 'Application_Rate_Limiter_Request',
env: :issues_create_request_limit,
remote_ip: '0.0.0.0',
request_method: 'POST',
path: "/#{project.full_path}/-/issues",
user_id: user.id,
username: user.username
}
expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
project.add_developer(user)
sign_in(user)
6.times do
post :create, params: {
namespace_id: project.namespace.to_param,
project_id: project,
issue: { title: 'Title', description: 'Description' }
}
end
end
end
end
describe 'POST #mark_as_spam' do
......
......@@ -13,7 +13,7 @@ FactoryBot.define do
end
trait :remote_store do
file_store { JobArtifactUploader::Store::REMOTE}
file_store { JobArtifactUploader::Store::REMOTE }
end
after :build do |artifact|
......
# frozen_string_literal: true
FactoryBot.define do
factory :diff_note_position do
association :note, factory: :diff_note_on_merge_request
line_code { note.line_code }
position { note.position }
diff_type { :head }
end
end
......@@ -399,10 +399,12 @@ describe 'Environments page', :js do
describe 'environments folders' do
before do
create(:environment, project: project,
create(:environment, :will_auto_stop,
project: project,
name: 'staging/review-1',
state: :available)
create(:environment, project: project,
create(:environment, :will_auto_stop,
project: project,
name: 'staging/review-2',
state: :available)
end
......@@ -420,6 +422,14 @@ describe 'Environments page', :js do
expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2'
within('.ci-table') do
within('.gl-responsive-table-row:nth-child(3)') do
expect(find('.js-auto-stop').text).not_to be_empty
end
within('.gl-responsive-table-row:nth-child(4)') do
expect(find('.js-auto-stop').text).not_to be_empty
end
end
end
end
......
......@@ -50,6 +50,7 @@ describe('Time series component', () => {
propsData: {
graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations,
projectPath: `${mockHost}${mockProjectDir}`,
},
store,
......
......@@ -16,6 +16,7 @@ import {
fetchDeploymentsData,
fetchEnvironmentsData,
fetchDashboardData,
fetchAnnotations,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
......@@ -24,10 +25,12 @@ import {
} from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
import storeState from '~/monitoring/stores/state';
import {
deploymentData,
environmentData,
annotationsData,
metricsDashboardResponse,
metricsDashboardViewModel,
dashboardGitResponse,
......@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => {
});
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
project: {
data: {
environments: [],
},
jest.spyOn(gqClient, 'mutate').mockReturnValue({
data: {
project: {
data: {
environments: [],
},
},
}),
);
},
});
return testAction(
filterEnvironments,
......@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveEnvironmentsDataSuccess on success', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
project: {
data: {
environments: environmentData,
},
jest.spyOn(gqClient, 'mutate').mockResolvedValue({
data: {
project: {
data: {
environments: environmentData,
},
},
}),
);
},
});
return testAction(
fetchEnvironmentsData,
......@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveEnvironmentsDataFailure on error', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject());
jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
return testAction(
fetchEnvironmentsData,
......@@ -220,6 +219,80 @@ describe('Monitoring store actions', () => {
});
});
describe('fetchAnnotations', () => {
const { state } = store;
state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
afterEach(() => {
resetStore(store);
});
it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
const mockMutate = jest.spyOn(gqClient, 'mutate');
const mutationVariables = {
mutation: getAnnotations,
variables: {
projectPath: state.projectPath,
environmentName: state.currentEnvironmentName,
dashboardId: state.currentDashboard,
},
};
mockMutate.mockResolvedValue({
data: {
project: {
environment: {
metricDashboard: {
annotations: annotationsData,
},
},
},
},
});
return testAction(
fetchAnnotations,
null,
state,
[],
[
{ type: 'requestAnnotations' },
{ type: 'receiveAnnotationsSuccess', payload: annotationsData },
],
() => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
},
);
});
it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
const mockMutate = jest.spyOn(gqClient, 'mutate');
const mutationVariables = {
mutation: getAnnotations,
variables: {
projectPath: state.projectPath,
environmentName: state.currentEnvironmentName,
dashboardId: state.currentDashboard,
},
};
mockMutate.mockRejectedValue({});
return testAction(
fetchAnnotations,
null,
state,
[],
[{ type: 'requestAnnotations' }, { type: 'receiveAnnotationsFailure' }],
() => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
},
);
});
});
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {
......
import $ from 'jquery';
import { assignIn } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises';
import SmartInterval from '~/smart_interval';
jest.useFakeTimers();
let interval;
describe('SmartInterval', () => {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_INCREMENT_FACTOR = 2;
function createDefaultSmartInterval(config) {
const defaultParams = {
callback: () => Promise.resolve(),
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
lazyStart: false,
immediateExecution: false,
hiddenInterval: null,
};
if (config) {
assignIn(defaultParams, config);
}
return new SmartInterval(defaultParams);
}
afterEach(() => {
interval.destroy();
});
describe('Increment Interval', () => {
it('should increment the interval delay', () => {
interval = createDefaultSmartInterval();
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
const intervalConfig = interval.cfg;
const iterationCount = 4;
const maxIntervalAfterIterations =
intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
const currentInterval = interval.getCurrentInterval();
// Provide some flexibility for performance of testing environment
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
});
});
it('should not increment past maxInterval', () => {
interval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
const currentInterval = interval.getCurrentInterval();
expect(currentInterval).toBe(interval.cfg.maxInterval);
});
});
it('does not increment while waiting for callback', () => {
interval = createDefaultSmartInterval({
callback: () => new Promise($.noop),
});
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
const oneInterval = interval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
expect(interval.getCurrentInterval()).toEqual(oneInterval);
});
});
});
describe('Public methods', () => {
beforeEach(() => {
interval = createDefaultSmartInterval();
});
it('should cancel an interval', () => {
jest.runOnlyPendingTimers();
interval.cancel();
return waitForPromises().then(() => {
const { intervalId } = interval.state;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
expect(intervalId).toBeUndefined();
expect(currentInterval).toBe(intervalLowerLimit);
});
});
it('should resume an interval', () => {
jest.runOnlyPendingTimers();
interval.cancel();
interval.resume();
return waitForPromises().then(() => {
const { intervalId } = interval.state;
expect(intervalId).toBeTruthy();
});
});
});
describe('DOM Events', () => {
beforeEach(() => {
// This ensures DOM and DOM events are initialized for these specs.
setFixtures('<div></div>');
interval = createDefaultSmartInterval();
});
it('should pause when page is not visible', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
});
});
it('should change to the hidden interval when page is not visible', () => {
interval.destroy();
const HIDDEN_INTERVAL = 1500;
interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
expect(interval.state.intervalId).toBeTruthy();
expect(
interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
).toBeTruthy();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
});
});
it('should resume when page is becomes visible at the previous interval', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'visible' } });
expect(interval.state.intervalId).toBeTruthy();
});
});
it('should cancel on page unload', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
$(document).triggerHandler('beforeunload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
});
});
it('should execute callback before first interval', () => {
interval = createDefaultSmartInterval({ immediateExecution: true });
expect(interval.cfg.immediateExecution).toBeFalsy();
});
});
});
......@@ -30,7 +30,6 @@ describe('StaticSiteEditor', () => {
store = new Vuex.Store({
state: createState(initialState),
getters: {
isContentLoaded: () => false,
contentChanged: () => false,
...getters,
},
......@@ -43,9 +42,11 @@ describe('StaticSiteEditor', () => {
};
const buildContentLoadedStore = ({ initialState, getters } = {}) => {
buildStore({
initialState,
initialState: {
isContentLoaded: true,
...initialState,
},
getters: {
isContentLoaded: () => true,
...getters,
},
});
......@@ -85,7 +86,7 @@ describe('StaticSiteEditor', () => {
const content = 'edit area content';
beforeEach(() => {
buildStore({ initialState: { content }, getters: { isContentLoaded: () => true } });
buildContentLoadedStore({ initialState: { content } });
buildWrapper();
});
......
import createState from '~/static_site_editor/store/state';
import { isContentLoaded, contentChanged } from '~/static_site_editor/store/getters';
import { contentChanged } from '~/static_site_editor/store/getters';
import { sourceContent as content } from '../mock_data';
describe('Static Site Editor Store getters', () => {
describe('isContentLoaded', () => {
it('returns true when originalContent is not empty', () => {
expect(isContentLoaded(createState({ originalContent: content }))).toBe(true);
});
it('returns false when originalContent is empty', () => {
expect(isContentLoaded(createState({ originalContent: '' }))).toBe(false);
});
});
describe('contentChanged', () => {
it('returns true when content and originalContent are different', () => {
const state = createState({ content, originalContent: 'something else' });
......
......@@ -19,6 +19,7 @@ describe('Static Site Editor Store mutations', () => {
mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment