diff options
Diffstat (limited to 'app')
63 files changed, 437 insertions, 154 deletions
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue index d2767dd6c64..04c2d4a7493 100644 --- a/app/assets/javascripts/badges/components/badge_list.vue +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -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> diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue index 19516a13d15..3de1b2f0707 100644 --- a/app/assets/javascripts/contributors/components/contributors.vue +++ b/app/assets/javascripts/contributors/components/contributors.vue @@ -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"> diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index 048f3a2485c..5505704f430 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -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"> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 305d860a692..335c668474e 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -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" diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 01a00e03814..89e40faa23e 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -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" /> diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index a8103c80da0..148edfe3a51 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -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"> diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 2ffecce0a56..1f1776a5487 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -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"> diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue index 76821bcd986..dd2d726d525 100644 --- a/app/assets/javascripts/ide/components/branches/search_list.vue +++ b/app/assets/javascripts/ide/components/branches/search_list.vue @@ -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"> diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index 35e5f9bcf69..d80662f6ae1 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -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> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue index 2cb5050c3f0..b97b7289886 100644 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -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" diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index 15c08988977..bf2a33be653 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -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> diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 343b0b6e90c..d3e5add2e83 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -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" /> diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index 86a773499bc..3852f2fdfa4 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -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> diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 9b0ee40a30a..4a48852159a 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -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]; diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 24aa8480ce4..9041b01088c 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -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, }), ); }, diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 15b17f01daf..4586ce70ad6 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -213,7 +213,6 @@ export default { 'dashboard', 'emptyState', 'showEmptyState', - 'deploymentData', 'useDashboardEndpoint', 'allDashboards', 'additionalPanelTypesEnabled', diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index d1394bca447..676fc0cca64 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -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" diff --git a/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql new file mode 100644 index 00000000000..e2edaa707b2 --- /dev/null +++ b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql @@ -0,0 +1,13 @@ +query getAnnotations($projectPath: ID!) { + environment(name: $environmentName) { + metricDashboard(id: $dashboardId) { + annotations: nodes { + id + description + from + to + panelId + } + } + } +} diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 06b99f572e7..5b2bd1f1493 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -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 /** diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 9a3489d53d7..2f9955da1b1 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 38c1524d904..aa31b6642d7 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -93,6 +93,16 @@ export default { }, /** + * Annotations + */ + [types.RECEIVE_ANNOTATIONS_SUCCESS](state, annotations) { + state.annotations = annotations; + }, + [types.RECEIVE_ANNOTATIONS_FAILURE](state) { + state.annotations = []; + }, + + /** * Individual panel/metric results */ [types.REQUEST_METRIC_RESULT](state, { metricId }) { diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 2b1907e8df7..e60510e747b 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -20,6 +20,7 @@ export default () => ({ allDashboards: [], // Other project data + annotations: [], deploymentData: [], environments: [], environmentsSearchTerm: '', diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ef3f4d0e3f6..1ff5b662d18 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -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" diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 2a3d022c5cd..e7777d0d3af 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -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" diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index accd6bf71f4..d4f23697e09 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -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" /> diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index f1106dc6ae9..571d305a50c 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -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 diff --git a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue b/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue index c90478db620..807f10bd9c6 100644 --- a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue +++ b/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue @@ -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> diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index e06149f2bcb..2b1291ac70f 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -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> diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 8ca590123ae..0e52d2d8010 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -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() { diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index e98f56d87f5..bc0034d397e 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -1,10 +1,11 @@ <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> diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue index ae6f451df18..44b4607e5a9 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -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 diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js new file mode 100644 index 00000000000..837c41cdf6b --- /dev/null +++ b/app/assets/javascripts/snippets/mixins/snippets.js @@ -0,0 +1,39 @@ +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 () => {}; diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue index e711510ba44..8deae2f2c8a 100644 --- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue +++ b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue @@ -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(); diff --git a/app/assets/javascripts/static_site_editor/store/getters.js b/app/assets/javascripts/static_site_editor/store/getters.js index 41256201c26..ebc68f8e9e6 100644 --- a/app/assets/javascripts/static_site_editor/store/getters.js +++ b/app/assets/javascripts/static_site_editor/store/getters.js @@ -1,2 +1,2 @@ -export const isContentLoaded = ({ originalContent }) => Boolean(originalContent); +// eslint-disable-next-line import/prefer-default-export export const contentChanged = ({ originalContent, content }) => originalContent !== content; diff --git a/app/assets/javascripts/static_site_editor/store/mutations.js b/app/assets/javascripts/static_site_editor/store/mutations.js index f98177bbc18..4727d04439c 100644 --- a/app/assets/javascripts/static_site_editor/store/mutations.js +++ b/app/assets/javascripts/static_site_editor/store/mutations.js @@ -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; diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js index 477ec540e02..d48cc8ed1a4 100644 --- a/app/assets/javascripts/static_site_editor/store/state.js +++ b/app/assets/javascripts/static_site_editor/store/state.js @@ -6,6 +6,8 @@ const createState = (initialState = {}) => ({ isLoadingContent: false, isSavingChanges: false, + isContentLoaded: false, + originalContent: '', content: '', title: '', diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 1bc28b15f74..05f73c4cdaf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -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, }); }, diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index 30a9633b6dc..fd45ac52647 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -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" diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 210d488f5a3..16254c74ba4 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -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: [], diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index e51a5c7b84d..09dc4d118a1 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -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] diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index f552c471eb2..96650e2cae9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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') diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 26de200a1c1..038b6146bab 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -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 diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb index 1caca6b3e44..6c3dcf8746b 100644 --- a/app/mailers/emails/pages_domains.rb +++ b/app/mailers/emails/pages_domains.rb @@ -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 diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 920ad3286d1..c96f086684f 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -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'], diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index ef0701b3874..c4ac10814a9 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -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? diff --git a/app/models/diff_note_position.rb b/app/models/diff_note_position.rb new file mode 100644 index 00000000000..78e4fbc49eb --- /dev/null +++ b/app/models/diff_note_position.rb @@ -0,0 +1,36 @@ +# 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 diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 6a86aebae39..c5233deaa96 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -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') diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 5c26c611e00..7b5bf6b32c2 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -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 diff --git a/app/services/clusters/management/validate_management_project_permissions_service.rb b/app/services/clusters/management/validate_management_project_permissions_service.rb new file mode 100644 index 00000000000..e89a0afe6d2 --- /dev/null +++ b/app/services/clusters/management/validate_management_project_permissions_service.rb @@ -0,0 +1,54 @@ +# 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 diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb index 8cb77040b14..2315df612a1 100644 --- a/app/services/clusters/update_service.rb +++ b/app/services/clusters/update_service.rb @@ -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 diff --git a/app/services/environments/auto_stop_service.rb b/app/services/environments/auto_stop_service.rb index ee7f25a4d76..bde598abf66 100644 --- a/app/services/environments/auto_stop_service.rb +++ b/app/services/environments/auto_stop_service.rb @@ -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 diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 62827f20929..91e19d190bd 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -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, diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb index 93445dd4ddd..1c03641469e 100644 --- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb +++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb @@ -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) diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 967fcdc704e..427314a87bb 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -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` diff --git a/app/views/admin/application_settings/_issue_limits.html.haml b/app/views/admin/application_settings/_issue_limits.html.haml new file mode 100644 index 00000000000..5906358fbb1 --- /dev/null +++ b/app/views/admin/application_settings/_issue_limits.html.haml @@ -0,0 +1,9 @@ += 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' } diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml index 8d88dedf832..db4611964b4 100644 --- a/app/views/admin/application_settings/network.html.haml +++ b/app/views/admin/application_settings/network.html.haml @@ -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' diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 9fffa97f969..4e9cfc13af0 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,7 +1,7 @@ - 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' diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml new file mode 100644 index 00000000000..1bc2cc15616 --- /dev/null +++ b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml @@ -0,0 +1,11 @@ +%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 } diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml new file mode 100644 index 00000000000..6f20d11c966 --- /dev/null +++ b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml @@ -0,0 +1,7 @@ += _("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 } diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 6ea4eeb66c5..e28c74dd650 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -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 diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 6da4956a036..69b030ed76a 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -1,6 +1,6 @@ - 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 diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index 0d40f375926..a9e2cbac890 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -1,9 +1,9 @@ - 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! diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb index de5e10a0976..ada52d3402d 100644 --- a/app/workers/environments/auto_stop_cron_worker.rb +++ b/app/workers/environments/auto_stop_cron_worker.rb @@ -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 |