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 { ...@@ -28,7 +28,7 @@ export default {
{{ s__('Badges|Your badges') }} {{ s__('Badges|Your badges') }}
<span v-show="!isLoading" class="badge badge-pill">{{ badges.length }}</span> <span v-show="!isLoading" class="badge badge-pill">{{ badges.length }}</span>
</div> </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"> <div v-if="hasNoBadges" class="card-body">
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span> <span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span> <span v-else>{{ s__('Badges|This project has no badges') }}</span>
......
...@@ -197,7 +197,7 @@ export default { ...@@ -197,7 +197,7 @@ export default {
<template> <template>
<div> <div>
<div v-if="loading" class="contributors-loader text-center"> <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>
<div v-else-if="showChart" class="contributors-charts"> <div v-else-if="showChart" class="contributors-charts">
......
...@@ -119,7 +119,7 @@ export default { ...@@ -119,7 +119,7 @@ export default {
<gl-loading-icon <gl-loading-icon
v-if="isLoading && !hasKeys" v-if="isLoading && !hasKeys"
:label="s__('DeployKeys|Loading deploy keys')" :label="s__('DeployKeys|Loading deploy keys')"
:size="2" size="lg"
/> />
<template v-else-if="hasKeys"> <template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
......
...@@ -58,12 +58,6 @@ export default { ...@@ -58,12 +58,6 @@ export default {
required: true, required: true,
}, },
shouldShowAutoStopDate: {
type: Boolean,
required: false,
default: false,
},
tableData: { tableData: {
type: Object, type: Object,
required: true, required: true,
...@@ -638,12 +632,7 @@ export default { ...@@ -638,12 +632,7 @@ export default {
</span> </span>
</div> </div>
<div <div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
v-if="!isFolder && shouldShowAutoStopDate"
class="table-section"
:class="tableData.autoStop.spacing"
role="gridcell"
>
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div> <div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
<span <span
v-if="canShowAutoStopDate" v-if="canShowAutoStopDate"
...@@ -662,10 +651,7 @@ export default { ...@@ -662,10 +651,7 @@ export default {
role="gridcell" role="gridcell"
> >
<div class="btn-group table-action-buttons" role="group"> <div class="btn-group table-action-buttons" role="group">
<pin-component <pin-component v-if="canShowAutoStopDate" :auto-stop-url="autoStopUrl" />
v-if="canShowAutoStopDate && shouldShowAutoStopDate"
:auto-stop-url="autoStopUrl"
/>
<external-url-component <external-url-component
v-if="externalURL && canReadEnvironment" v-if="externalURL && canReadEnvironment"
......
...@@ -6,7 +6,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -6,7 +6,6 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp'; import { flow, reverse, sortBy } from 'lodash/fp';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin'; import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentItem from './environment_item.vue'; import EnvironmentItem from './environment_item.vue';
export default { export default {
...@@ -17,7 +16,7 @@ export default { ...@@ -17,7 +16,7 @@ export default {
CanaryDeploymentCallout: () => CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'), import('ee_component/environments/components/canary_deployment_callout.vue'),
}, },
mixins: [environmentTableMixin, glFeatureFlagsMixin()], mixins: [environmentTableMixin],
props: { props: {
environments: { environments: {
type: Array, type: Array,
...@@ -43,9 +42,6 @@ export default { ...@@ -43,9 +42,6 @@ export default {
: env, : env,
); );
}, },
shouldShowAutoStopDate() {
return this.glFeatures.autoStopEnvironments;
},
tableData() { tableData() {
return { return {
// percent spacing for cols, should add up to 100 // percent spacing for cols, should add up to 100
...@@ -74,7 +70,7 @@ export default { ...@@ -74,7 +70,7 @@ export default {
spacing: 'section-5', spacing: 'section-5',
}, },
actions: { actions: {
spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30', spacing: 'section-25',
}, },
}; };
}, },
...@@ -131,12 +127,7 @@ export default { ...@@ -131,12 +127,7 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader"> <div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }} {{ tableData.date.title }}
</div> </div>
<div <div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
v-if="shouldShowAutoStopDate"
class="table-section"
:class="tableData.autoStop.spacing"
role="columnheader"
>
{{ tableData.autoStop.title }} {{ tableData.autoStop.title }}
</div> </div>
</div> </div>
...@@ -146,7 +137,6 @@ export default { ...@@ -146,7 +137,6 @@ export default {
:key="`environment-item-${i}`" :key="`environment-item-${i}`"
:model="model" :model="model"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:should-show-auto-stop-date="shouldShowAutoStopDate"
:table-data="tableData" :table-data="tableData"
/> />
......
...@@ -225,7 +225,7 @@ export default { ...@@ -225,7 +225,7 @@ export default {
<template> <template>
<div> <div>
<div v-if="errorLoading" class="py-3"> <div v-if="errorLoading" class="py-3">
<gl-loading-icon :size="3" /> <gl-loading-icon size="lg" />
</div> </div>
<div v-else-if="error" class="error-details"> <div v-else-if="error" class="error-details">
<gl-alert v-if="isAlertVisible" @dismiss="isAlertVisible = false"> <gl-alert v-if="isAlertVisible" @dismiss="isAlertVisible = false">
...@@ -405,7 +405,7 @@ export default { ...@@ -405,7 +405,7 @@ export default {
</ul> </ul>
<div v-if="loadingStacktrace" class="py-3"> <div v-if="loadingStacktrace" class="py-3">
<gl-loading-icon :size="3" /> <gl-loading-icon size="lg" />
</div> </div>
<template v-else-if="showStacktrace"> <template v-else-if="showStacktrace">
......
...@@ -107,7 +107,7 @@ export default { ...@@ -107,7 +107,7 @@ export default {
<gl-loading-icon <gl-loading-icon
v-if="isLoadingItems" v-if="isLoadingItems"
:label="translations.loadingMessage" :label="translations.loadingMessage"
:size="2" size="lg"
class="loading-animation prepend-top-20" class="loading-animation prepend-top-20"
/> />
<div v-if="!isLoadingItems && !hasSearchQuery" class="section-header"> <div v-if="!isLoadingItems && !hasSearchQuery" class="section-header">
......
...@@ -72,7 +72,7 @@ export default { ...@@ -72,7 +72,7 @@ export default {
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:size="2" size="lg"
class="mt-3 mb-3 align-self-center ml-auto mr-auto" class="mt-3 mb-3 align-self-center ml-auto mr-auto"
/> />
<ul v-else class="mb-0 w-100"> <ul v-else class="mb-0 w-100">
......
...@@ -88,7 +88,7 @@ export default { ...@@ -88,7 +88,7 @@ export default {
<i aria-hidden="true" class="fa fa-search dropdown-input-search"></i> <i aria-hidden="true" class="fa fa-search dropdown-input-search"></i>
</div> </div>
<div class="dropdown-content"> <div class="dropdown-content">
<gl-loading-icon v-if="showLoading" :size="2" /> <gl-loading-icon v-if="showLoading" size="lg" />
<ul v-else> <ul v-else>
<li v-for="(item, index) in outputData" :key="index"> <li v-for="(item, index) in outputData" :key="index">
<button type="button" @click="clickItem(item)">{{ item.name }}</button> <button type="button" @click="clickItem(item)">{{ item.name }}</button>
......
...@@ -26,7 +26,7 @@ export default { ...@@ -26,7 +26,7 @@ export default {
<template> <template>
<div> <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> <template v-else>
<stage <stage
v-for="stage in stages" v-for="stage in stages"
......
...@@ -90,7 +90,7 @@ export default { ...@@ -90,7 +90,7 @@ export default {
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:size="2" size="lg"
class="mt-3 mb-3 align-self-center ml-auto mr-auto" class="mt-3 mb-3 align-self-center ml-auto mr-auto"
/> />
<template v-else> <template v-else>
......
...@@ -56,7 +56,7 @@ export default { ...@@ -56,7 +56,7 @@ export default {
<template> <template>
<div class="ide-pipeline"> <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"> <template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header"> <header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" /> <ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
......
...@@ -176,6 +176,6 @@ export default { ...@@ -176,6 +176,6 @@ export default {
{{ s__('IDE|Get started with Live Preview') }} {{ s__('IDE|Get started with Live Preview') }}
</a> </a>
</div> </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> </div>
</template> </template>
...@@ -910,3 +910,18 @@ export const setCookie = (name, value) => Cookies.set(name, value, { expires: 36 ...@@ -910,3 +910,18 @@ export const setCookie = (name, value) => Cookies.set(name, value, { expires: 36
export const getCookie = name => Cookies.get(name); export const getCookie = name => Cookies.get(name);
export const removeCookie = name => Cookies.remove(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 { ...@@ -55,6 +55,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
annotations: {
type: Array,
required: false,
default: () => [],
},
projectPath: { projectPath: {
type: String, type: String,
required: false, required: false,
...@@ -143,6 +148,7 @@ export default { ...@@ -143,6 +148,7 @@ export default {
return (this.option.series || []).concat( return (this.option.series || []).concat(
generateAnnotationsSeries({ generateAnnotationsSeries({
deployments: this.recentDeployments, deployments: this.recentDeployments,
annotations: this.annotations,
}), }),
); );
}, },
......
...@@ -213,7 +213,6 @@ export default { ...@@ -213,7 +213,6 @@ export default {
'dashboard', 'dashboard',
'emptyState', 'emptyState',
'showEmptyState', 'showEmptyState',
'deploymentData',
'useDashboardEndpoint', 'useDashboardEndpoint',
'allDashboards', 'allDashboards',
'additionalPanelTypesEnabled', 'additionalPanelTypesEnabled',
......
...@@ -89,6 +89,9 @@ export default { ...@@ -89,6 +89,9 @@ export default {
deploymentData(state) { deploymentData(state) {
return state[this.namespace].deploymentData; return state[this.namespace].deploymentData;
}, },
annotations(state) {
return state[this.namespace].annotations;
},
projectPath(state) { projectPath(state) {
return state[this.namespace].projectPath; return state[this.namespace].projectPath;
}, },
...@@ -310,6 +313,7 @@ export default { ...@@ -310,6 +313,7 @@ export default {
ref="timeChart" ref="timeChart"
:graph-data="graphData" :graph-data="graphData"
:deployment-data="deploymentData" :deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath" :project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)" :thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId" :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'; ...@@ -6,8 +6,13 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils'; import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper'; import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql'; import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import statusCodes from '../../lib/utils/http_status'; 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 { s__, sprintf } from '../../locale';
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants'; import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
...@@ -80,6 +85,14 @@ export const setShowErrorBanner = ({ commit }, enabled) => { ...@@ -80,6 +85,14 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
export const fetchData = ({ dispatch }) => { export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData'); dispatch('fetchEnvironmentsData');
dispatch('fetchDashboard'); 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 // Metrics dashboard
...@@ -269,6 +282,40 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => { ...@@ -269,6 +282,40 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); 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 // Dashboard manipulation
/** /**
......
...@@ -3,6 +3,11 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; ...@@ -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_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; 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 // Git project deployments
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA'; export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS'; export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
......
...@@ -92,6 +92,16 @@ export default { ...@@ -92,6 +92,16 @@ export default {
state.environments = []; state.environments = [];
}, },
/**
* Annotations
*/
[types.RECEIVE_ANNOTATIONS_SUCCESS](state, annotations) {
state.annotations = annotations;
},
[types.RECEIVE_ANNOTATIONS_FAILURE](state) {
state.annotations = [];
},
/** /**
* Individual panel/metric results * Individual panel/metric results
*/ */
......
...@@ -20,6 +20,7 @@ export default () => ({ ...@@ -20,6 +20,7 @@ export default () => ({
allDashboards: [], allDashboards: [],
// Other project data // Other project data
annotations: [],
deploymentData: [], deploymentData: [],
environments: [], environments: [],
environmentsSearchTerm: '', environmentsSearchTerm: '',
......
...@@ -135,7 +135,7 @@ export default { ...@@ -135,7 +135,7 @@ export default {
paddingRight: `${graphRightPadding}px`, 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 <pipeline-graph
v-if="pipelineTypeUpstream" v-if="pipelineTypeUpstream"
......
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
/> />
</ci-header> </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 <gl-modal
:modal-id="$options.DELETE_MODAL_ID" :modal-id="$options.DELETE_MODAL_ID"
......
...@@ -271,7 +271,7 @@ export default { ...@@ -271,7 +271,7 @@ export default {
<gl-loading-icon <gl-loading-icon
v-if="stateToRender === $options.stateMap.loading" v-if="stateToRender === $options.stateMap.loading"
:label="s__('Pipelines|Loading Pipelines')" :label="s__('Pipelines|Loading Pipelines')"
:size="3" size="lg"
class="prepend-top-20" class="prepend-top-20"
/> />
......
...@@ -94,7 +94,7 @@ export default { ...@@ -94,7 +94,7 @@ export default {
</script> </script>
<template> <template>
<div class="ci-status-link"> <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"> <a v-else :href="ciStatus.details_path">
<ci-icon <ci-icon
v-tooltip v-tooltip
......
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
</div> </div>
</div> </div>
<div v-if="loadingStacktrace" class="card"> <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> </div>
<stacktrace v-else :entries="stacktrace" /> <stacktrace v-else :entries="stacktrace" />
</div> </div>
......
...@@ -77,7 +77,7 @@ export default { ...@@ -77,7 +77,7 @@ export default {
<section id="serverless-functions" class="flex-grow"> <section id="serverless-functions" class="flex-grow">
<gl-loading-icon <gl-loading-icon
v-if="checkingInstalled" v-if="checkingInstalled"
:size="2" size="lg"
class="prepend-top-default append-bottom-default" class="prepend-top-default append-bottom-default"
/> />
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
</template> </template>
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:size="2" size="lg"
class="prepend-top-default append-bottom-default js-functions-loader" class="prepend-top-default append-bottom-default js-functions-loader"
/> />
</div> </div>
......
...@@ -33,7 +33,7 @@ export default class SmartInterval { ...@@ -33,7 +33,7 @@ export default class SmartInterval {
this.state = { this.state = {
intervalId: null, intervalId: null,
currentInterval: this.cfg.startingInterval, currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible', pagevisibile: true,
}; };
this.initInterval(); this.initInterval();
...@@ -91,8 +91,10 @@ export default class SmartInterval { ...@@ -91,8 +91,10 @@ export default class SmartInterval {
} }
destroy() { destroy() {
document.removeEventListener('visibilitychange', this.onVisibilityChange);
window.removeEventListener('blur', this.onWindowVisibilityChange);
window.removeEventListener('focus', this.onWindowVisibilityChange);
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document) $(document)
.off('visibilitychange') .off('visibilitychange')
.off('beforeunload'); .off('beforeunload');
...@@ -124,9 +126,21 @@ export default class SmartInterval { ...@@ -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() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab or window is no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this));
window.addEventListener('blur', this.onWindowVisibilityChange.bind(this));
window.addEventListener('focus', this.onWindowVisibilityChange.bind(this));
} }
initPageUnloadHandling() { initPageUnloadHandling() {
...@@ -135,8 +149,7 @@ export default class SmartInterval { ...@@ -135,8 +149,7 @@ export default class SmartInterval {
$(document).on('beforeunload', () => this.cancel()); $(document).on('beforeunload', () => this.cancel());
} }
handleVisibilityChange(e) { handleVisibilityChange() {
this.state.pageVisibility = e.target.visibilityState;
const intervalAction = this.isPageVisible() const intervalAction = this.isPageVisible()
? this.onVisibilityVisible ? this.onVisibilityVisible
: this.onVisibilityHidden; : this.onVisibilityHidden;
...@@ -166,7 +179,7 @@ export default class SmartInterval { ...@@ -166,7 +179,7 @@ export default class SmartInterval {
} }
isPageVisible() { isPageVisible() {
return this.state.pageVisibility === 'visible'; return this.state.pagevisibile;
} }
stopTimer() { stopTimer() {
......
<script> <script>
import GetSnippetQuery from '../queries/snippet.query.graphql';
import SnippetHeader from './snippet_header.vue'; import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue'; import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue'; import SnippetBlob from './snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { getSnippetMixin } from '../mixins/snippets';
export default { export default {
components: { components: {
SnippetHeader, SnippetHeader,
...@@ -12,33 +13,7 @@ export default { ...@@ -12,33 +13,7 @@ export default {
GlLoadingIcon, GlLoadingIcon,
SnippetBlob, SnippetBlob,
}, },
apollo: { mixins: [getSnippetMixin],
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;
},
},
}; };
</script> </script>
<template> <template>
...@@ -46,7 +21,7 @@ export default { ...@@ -46,7 +21,7 @@ export default {
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:label="__('Loading snippet')" :label="__('Loading snippet')"
:size="2" size="lg"
class="loading-animation prepend-top-20 append-bottom-20" class="loading-animation prepend-top-20 append-bottom-20"
/> />
<template v-else> <template v-else>
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
:label="__('Loading snippet')" :label="__('Loading snippet')"
:size="2" size="lg"
class="loading-animation prepend-top-20 append-bottom-20" class="loading-animation prepend-top-20 append-bottom-20"
/> />
<blob-content-edit <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 { ...@@ -12,8 +12,8 @@ export default {
Toolbar, Toolbar,
}, },
computed: { computed: {
...mapState(['content', 'isLoadingContent', 'isSavingChanges']), ...mapState(['content', 'isLoadingContent', 'isSavingChanges', 'isContentLoaded']),
...mapGetters(['isContentLoaded', 'contentChanged']), ...mapGetters(['contentChanged']),
}, },
mounted() { mounted() {
this.loadContent(); this.loadContent();
......
export const isContentLoaded = ({ originalContent }) => Boolean(originalContent); // eslint-disable-next-line import/prefer-default-export
export const contentChanged = ({ originalContent, content }) => originalContent !== content; export const contentChanged = ({ originalContent, content }) => originalContent !== content;
...@@ -6,6 +6,7 @@ export default { ...@@ -6,6 +6,7 @@ export default {
}, },
[types.RECEIVE_CONTENT_SUCCESS](state, { title, content }) { [types.RECEIVE_CONTENT_SUCCESS](state, { title, content }) {
state.isLoadingContent = false; state.isLoadingContent = false;
state.isContentLoaded = true;
state.title = title; state.title = title;
state.content = content; state.content = content;
state.originalContent = content; state.originalContent = content;
......
...@@ -6,6 +6,8 @@ const createState = (initialState = {}) => ({ ...@@ -6,6 +6,8 @@ const createState = (initialState = {}) => ({
isLoadingContent: false, isLoadingContent: false,
isSavingChanges: false, isSavingChanges: false,
isContentLoaded: false,
originalContent: '', originalContent: '',
content: '', content: '',
title: '', title: '',
......
...@@ -214,8 +214,6 @@ export default { ...@@ -214,8 +214,6 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store)); return new MRWidgetService(this.getServiceEndpoints(store));
}, },
checkStatus(cb, isRebased) { checkStatus(cb, isRebased) {
if (document.visibilityState !== 'visible') return Promise.resolve();
return this.service return this.service
.checkStatus() .checkStatus()
.then(({ data }) => { .then(({ data }) => {
...@@ -238,10 +236,10 @@ export default { ...@@ -238,10 +236,10 @@ export default {
initPolling() { initPolling() {
this.pollingInterval = new SmartInterval({ this.pollingInterval = new SmartInterval({
callback: this.checkStatus, callback: this.checkStatus,
startingInterval: 10000, startingInterval: 10 * 1000,
maxInterval: 30000, maxInterval: 240 * 1000,
hiddenInterval: 120000, hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
incrementByFactorOf: 5000, incrementByFactorOf: 2,
}); });
}, },
initDeploymentsPolling() { initDeploymentsPolling() {
...@@ -253,10 +251,9 @@ export default { ...@@ -253,10 +251,9 @@ export default {
deploymentsPoll(callback) { deploymentsPoll(callback) {
return new SmartInterval({ return new SmartInterval({
callback, callback,
startingInterval: 30000, startingInterval: 30 * 1000,
maxInterval: 120000, maxInterval: 240 * 1000,
hiddenInterval: 240000, incrementByFactorOf: 4,
incrementByFactorOf: 15000,
immediateExecution: true, immediateExecution: true,
}); });
}, },
......
...@@ -80,7 +80,7 @@ export default { ...@@ -80,7 +80,7 @@ export default {
@input="onInput" @input="onInput"
/> />
<div class="d-flex flex-column"> <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 <gl-infinite-scroll
:max-list-height="402" :max-list-height="402"
:fetched-items="projectSearchResults.length" :fetched-items="projectSearchResults.length"
......
...@@ -219,6 +219,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -219,6 +219,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:domain_blacklist_file, :domain_blacklist_file,
:raw_blob_request_limit, :raw_blob_request_limit,
:namespace_storage_size_limit, :namespace_storage_size_limit,
:issues_create_limit,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
repository_storages: [], repository_storages: [],
......
...@@ -14,9 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -14,9 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? } before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
end push_frontend_feature_flag(:metrics_dashboard_annotations)
before_action do
push_frontend_feature_flag(:auto_stop_environments, default_enabled: true)
end end
after_action :expire_etag_cache, only: [:cancel_auto_stop] after_action :expire_etag_cache, only: [:cancel_auto_stop]
......
...@@ -42,6 +42,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -42,6 +42,9 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_import_issues!, only: [:import_csv] before_action :authorize_import_issues!, only: [:import_csv]
before_action :authorize_download_code!, only: [:related_branches] 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 before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group) push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true) push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true)
...@@ -296,6 +299,22 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -296,6 +299,22 @@ class Projects::IssuesController < Projects::ApplicationController
# 3. https://gitlab.com/gitlab-org/gitlab-foss/issues/42426 # 3. https://gitlab.com/gitlab-org/gitlab-foss/issues/42426
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42422') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42422')
end 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 end
Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController') Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController')
...@@ -24,6 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -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(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project) push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
end end
before_action do before_action do
......
...@@ -41,5 +41,16 @@ module Emails ...@@ -41,5 +41,16 @@ module Emails
subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'") subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'")
) )
end 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
end end
...@@ -79,6 +79,7 @@ module ApplicationSettingImplementation ...@@ -79,6 +79,7 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200, housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'], import_sources: Settings.gitlab['import_sources'],
issues_create_limit: 300,
local_markdown_version: 0, local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
......
...@@ -73,12 +73,14 @@ module Ci ...@@ -73,12 +73,14 @@ module Ci
validates :file_format, presence: true, unless: :trace?, on: :create validates :file_format, presence: true, unless: :trace?, on: :create
validate :valid_file_format?, 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? 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_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } 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 }) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
...@@ -226,6 +228,15 @@ module Ci ...@@ -226,6 +228,15 @@ module Ci
self.size = file.size self.size = file.size
end 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? def project_destroyed?
# Use job.project to avoid extra DB query for project # Use job.project to avoid extra DB query for project
job.project.pending_delete? 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 ...@@ -17,6 +17,8 @@ class LfsObject < ApplicationRecord
mount_uploader :file, LfsObjectUploader 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? after_save :update_file_store, if: :saved_change_to_file?
def self.not_linked_to_project(project) def self.not_linked_to_project(project)
...@@ -55,6 +57,17 @@ class LfsObject < ApplicationRecord ...@@ -55,6 +57,17 @@ class LfsObject < ApplicationRecord
def self.calculate_oid(path) def self.calculate_oid(path)
self.hexdigest(path) self.hexdigest(path)
end 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 end
LfsObject.prepend_if_ee('EE::LfsObject') LfsObject.prepend_if_ee('EE::LfsObject')
...@@ -23,6 +23,8 @@ module Clusters ...@@ -23,6 +23,8 @@ module Clusters
cluster.errors.add(:base, _('Instance does not support multiple Kubernetes clusters')) cluster.errors.add(:base, _('Instance does not support multiple Kubernetes clusters'))
end end
validate_management_project_permissions(cluster)
return cluster if cluster.errors.present? return cluster if cluster.errors.present?
cluster.tap do |cluster| cluster.tap do |cluster|
...@@ -57,6 +59,11 @@ module Clusters ...@@ -57,6 +59,11 @@ module Clusters
def can_create_cluster? def can_create_cluster?
clusterable.clusters.empty? clusterable.clusters.empty?
end end
def validate_management_project_permissions(cluster)
Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
.execute(cluster, params[:management_project_id])
end
end 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 ...@@ -18,46 +18,9 @@ module Clusters
private private
def can_admin_pipeline_for_project?(project)
Ability.allowed?(current_user, :admin_pipeline, project)
end
def validate_params(cluster) def validate_params(cluster)
if params[:management_project_id].present? ::Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
management_project = management_project_scope(cluster).find_by_id(params[:management_project_id]) .execute(cluster, 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
end end
end end
end end
...@@ -30,7 +30,7 @@ module Environments ...@@ -30,7 +30,7 @@ module Environments
def stop_in_batch def stop_in_batch
environments = Environment.auto_stoppable(BATCH_SIZE) 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) Ci::StopEnvironmentsService.execute_in_batch(environments)
end end
......
...@@ -489,6 +489,12 @@ class NotificationService ...@@ -489,6 +489,12 @@ class NotificationService
end end
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) def issue_due(issue)
recipients = NotificationRecipients::BuildService.build_recipients( recipients = NotificationRecipients::BuildService.build_recipients(
issue, issue,
......
...@@ -57,6 +57,8 @@ module PagesDomains ...@@ -57,6 +57,8 @@ module PagesDomains
pages_domain.save!(validate: false) pages_domain.save!(validate: false)
acme_order.destroy! acme_order.destroy!
NotificationService.new.pages_domain_auto_ssl_failed(pages_domain)
end end
def log_error(api_order) def log_error(api_order)
......
...@@ -56,10 +56,31 @@ module RecordsUploads ...@@ -56,10 +56,31 @@ module RecordsUploads
size: file.size, size: file.size,
path: upload_path, path: upload_path,
model: model, model: model,
mount_point: mounted_as mount_point: mounted_as,
store: initial_store
) )
end 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 # Before removing an attachment, destroy any Upload records at the same path
# #
# Called `before :remove` # 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 @@ ...@@ -46,4 +46,15 @@
.settings-content .settings-content
= render 'protected_paths' = 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' = render_if_exists 'admin/application_settings/ee_network_settings'
- page_title _('Deploy Keys') - page_title _('Deploy Keys')
%h3.page-title.deploy-keys-title %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 .float-right
= link_to _('New deploy key'), new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted' = 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 @@ ...@@ -18,7 +18,7 @@
= f.submit _('Add email address'), class: 'btn btn-success', data: { qa_selector: 'add_email_address_button' } = f.submit _('Add email address'), class: 'btn btn-success', data: { qa_selector: 'add_email_address_button' }
%hr %hr
%h4.prepend-top-0 %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 .account-well.append-bottom-default
%ul %ul
%li %li
......
- if @related_branches.any? - if @related_branches.any?
%h2.related-branches-title %h2.related-branches-title
= pluralize(@related_branches.count, 'Related Branch') = pluralize(@related_branches.size, 'Related Branch')
%ul.unstyled-list.related-merge-requests %ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch| - @related_branches.each do |branch|
%li %li
......
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? - 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
.card-header .card-header
Domains (#{@domains.count}) Domains (#{@domains.size})
%ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) } %ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain| - @domains.each do |domain|
- domain = Gitlab::View::Presenter::Factory.new(domain, current_user: current_user).fabricate! - domain = Gitlab::View::Presenter::Factory.new(domain, current_user: current_user).fabricate!
......
...@@ -8,8 +8,6 @@ module Environments ...@@ -8,8 +8,6 @@ module Environments
feature_category :continuous_delivery feature_category :continuous_delivery
def perform def perform
return unless Feature.enabled?(:auto_stop_environments, default_enabled: true)
AutoStopService.new.execute AutoStopService.new.execute
end end
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 ( ...@@ -397,6 +397,7 @@ CREATE TABLE public.application_settings (
email_restrictions text, email_restrictions text,
npm_package_requests_forwarding boolean DEFAULT true NOT NULL, npm_package_requests_forwarding boolean DEFAULT true NOT NULL,
namespace_storage_size_limit bigint DEFAULT 0 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, seat_link_enabled boolean DEFAULT true NOT NULL,
container_expiration_policies_enable_historic_entries boolean DEFAULT false 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 ...@@ -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; 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 ( CREATE TABLE public.draft_notes (
id bigint NOT NULL, id bigint NOT NULL,
merge_request_id integer 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 ...@@ -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.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.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); 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 ...@@ -7670,6 +7697,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.ci_group_variables ALTER TABLE ONLY public.ci_group_variables
ADD CONSTRAINT ci_group_variables_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY public.ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id); ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id);
...@@ -7829,6 +7859,9 @@ ALTER TABLE ONLY public.design_management_versions ...@@ -7829,6 +7859,9 @@ ALTER TABLE ONLY public.design_management_versions
ALTER TABLE ONLY public.design_user_mentions ALTER TABLE ONLY public.design_user_mentions
ADD CONSTRAINT design_user_mentions_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY public.draft_notes
ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id); ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id);
...@@ -8024,6 +8057,9 @@ ALTER TABLE ONLY public.ldap_group_links ...@@ -8024,6 +8057,9 @@ ALTER TABLE ONLY public.ldap_group_links
ALTER TABLE ONLY public.lfs_file_locks ALTER TABLE ONLY public.lfs_file_locks
ADD CONSTRAINT lfs_file_locks_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY public.lfs_objects
ADD CONSTRAINT lfs_objects_pkey PRIMARY KEY (id); ADD CONSTRAINT lfs_objects_pkey PRIMARY KEY (id);
...@@ -8417,6 +8453,9 @@ ALTER TABLE ONLY public.u2f_registrations ...@@ -8417,6 +8453,9 @@ ALTER TABLE ONLY public.u2f_registrations
ALTER TABLE ONLY public.uploads ALTER TABLE ONLY public.uploads
ADD CONSTRAINT uploads_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY public.user_agent_details
ADD CONSTRAINT user_agent_details_pkey PRIMARY KEY (id); 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 ...@@ -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_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_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); 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 ...@@ -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_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_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)); 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 ...@@ -11068,6 +11111,9 @@ ALTER TABLE ONLY public.project_statistics
ALTER TABLE ONLY public.user_details ALTER TABLE ONLY public.user_details
ADD CONSTRAINT fk_rails_12e0b3043d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; 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 ALTER TABLE ONLY public.users_security_dashboard_projects
ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; 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; ...@@ -13064,10 +13110,12 @@ COPY "schema_migrations" (version) FROM STDIN;
20200323134519 20200323134519
20200324093258 20200324093258
20200324115359 20200324115359
20200325111432
20200325152327 20200325152327
20200325160952 20200325160952
20200325183636 20200325183636
20200326114443 20200326114443
20200326122700
20200326124443 20200326124443
20200326134443 20200326134443
20200326135443 20200326135443
...@@ -13090,10 +13138,14 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13090,10 +13138,14 @@ COPY "schema_migrations" (version) FROM STDIN;
20200403185127 20200403185127
20200403185422 20200403185422
20200406135648 20200406135648
20200406165950
20200406171857
20200406172135
20200406192059 20200406192059
20200407094005 20200407094005
20200407094923 20200407094923
20200408110856 20200408110856
20200408153842
20200408175424 20200408175424
\. \.
...@@ -224,6 +224,7 @@ Parameters: ...@@ -224,6 +224,7 @@ Parameters:
| `cluster_id` | integer | yes | The ID of the cluster | | `cluster_id` | integer | yes | The ID of the cluster |
| `name` | string | no | The name 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 | | `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[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[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. | | `platform_kubernetes_attributes[ca_cert]` | string | no | TLS certificate. Required if API is using a self-signed TLS certificate. |
......
...@@ -179,6 +179,7 @@ Parameters: ...@@ -179,6 +179,7 @@ Parameters:
| `id` | integer | yes | The ID of the project owned by the authenticated user | | `id` | integer | yes | The ID of the project owned by the authenticated user |
| `name` | string | yes | The name of the cluster | | `name` | string | yes | The name of the cluster |
| `domain` | string | no | The [base domain](../user/project/clusters/index.md#base-domain) 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 | | `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 | | `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 | | `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, ...@@ -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**, [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. 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) ![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 Then you can add labels, a milestone, and assign the merge request to someone who can review
......
...@@ -53,6 +53,7 @@ module API ...@@ -53,6 +53,7 @@ module API
requires :name, type: String, desc: 'Cluster name' 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 :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 :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' 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 :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' requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
......
...@@ -56,6 +56,7 @@ module API ...@@ -56,6 +56,7 @@ module API
requires :name, type: String, desc: 'Cluster name' 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 :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 :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' 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 :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' requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
......
...@@ -19,8 +19,9 @@ module Gitlab ...@@ -19,8 +19,9 @@ module Gitlab
# and only do that when it's needed. # and only do that when it's needed.
def rate_limits def rate_limits
{ {
project_export: { threshold: 1, interval: 5.minutes }, issues_create: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.issues_create_limit }, interval: 1.minute },
project_download_export: { threshold: 10, interval: 10.minutes }, project_export: { threshold: 1, interval: 5.minutes },
project_download_export: { threshold: 10, interval: 10.minutes },
project_repositories_archive: { threshold: 5, interval: 1.minute }, project_repositories_archive: { threshold: 5, interval: 1.minute },
project_generate_new_export: { threshold: 1, interval: 5.minutes }, project_generate_new_export: { threshold: 1, interval: 5.minutes },
project_import: { threshold: 30, 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 ...@@ -67,7 +67,7 @@ module Gitlab
end end
def droppable? def droppable?
idempotent? && duplicate? && DuplicateJobs.drop_duplicates?(queue_name) idempotent? && duplicate?
end end
private private
......
...@@ -894,6 +894,9 @@ msgstr "" ...@@ -894,6 +894,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option" msgid "A user with write access to the source branch selected this option"
msgstr "" msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
msgid "API Help" msgid "API Help"
msgstr "" msgstr ""
...@@ -5334,6 +5337,9 @@ msgstr "" ...@@ -5334,6 +5337,9 @@ msgstr ""
msgid "Configure existing installation" msgid "Configure existing installation"
msgstr "" msgstr ""
msgid "Configure limit for issues created per minute by web and API requests."
msgstr ""
msgid "Configure limits for web and API requests." msgid "Configure limits for web and API requests."
msgstr "" msgstr ""
...@@ -11385,6 +11391,9 @@ msgstr "" ...@@ -11385,6 +11391,9 @@ msgstr ""
msgid "Issues Analytics" msgid "Issues Analytics"
msgstr "" msgstr ""
msgid "Issues Rate Limits"
msgstr ""
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable." msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "" msgstr ""
...@@ -12941,9 +12950,15 @@ msgstr "" ...@@ -12941,9 +12950,15 @@ msgstr ""
msgid "Metrics|There was an error creating the dashboard. %{error}" msgid "Metrics|There was an error creating the dashboard. %{error}"
msgstr "" 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" msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr "" msgstr ""
msgid "Metrics|There was an error getting annotations information."
msgstr ""
msgid "Metrics|There was an error getting deployment information." msgid "Metrics|There was an error getting deployment information."
msgstr "" msgstr ""
...@@ -14900,6 +14915,12 @@ msgstr "" ...@@ -14900,6 +14915,12 @@ msgstr ""
msgid "Please fill in a descriptive name for your group." msgid "Please fill in a descriptive name for your group."
msgstr "" 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}" msgid "Please migrate all existing projects to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
msgstr "" msgstr ""
...@@ -18939,6 +18960,9 @@ msgstr "" ...@@ -18939,6 +18960,9 @@ msgstr ""
msgid "Something went wrong while moving issues." msgid "Something went wrong while moving issues."
msgstr "" msgstr ""
msgid "Something went wrong while obtaining the Let's Encrypt certificate."
msgstr ""
msgid "Something went wrong while performing the action." msgid "Something went wrong while performing the action."
msgstr "" 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 ...@@ -1085,6 +1085,48 @@ describe Projects::IssuesController do
expect { subject }.to change(SentryIssue, :count) expect { subject }.to change(SentryIssue, :count)
end end
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 end
describe 'POST #mark_as_spam' do describe 'POST #mark_as_spam' do
......
...@@ -13,7 +13,7 @@ FactoryBot.define do ...@@ -13,7 +13,7 @@ FactoryBot.define do
end end
trait :remote_store do trait :remote_store do
file_store { JobArtifactUploader::Store::REMOTE} file_store { JobArtifactUploader::Store::REMOTE }
end end
after :build do |artifact| 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 ...@@ -399,10 +399,12 @@ describe 'Environments page', :js do
describe 'environments folders' do describe 'environments folders' do
before do before do
create(:environment, project: project, create(:environment, :will_auto_stop,
project: project,
name: 'staging/review-1', name: 'staging/review-1',
state: :available) state: :available)
create(:environment, project: project, create(:environment, :will_auto_stop,
project: project,
name: 'staging/review-2', name: 'staging/review-2',
state: :available) state: :available)
end end
...@@ -420,6 +422,14 @@ describe 'Environments page', :js do ...@@ -420,6 +422,14 @@ describe 'Environments page', :js do
expect(page).to have_content 'review-1' expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2' 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
end end
......
...@@ -50,6 +50,7 @@ describe('Time series component', () => { ...@@ -50,6 +50,7 @@ describe('Time series component', () => {
propsData: { propsData: {
graphData: { ...graphData, type }, graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData, deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations,
projectPath: `${mockHost}${mockProjectDir}`, projectPath: `${mockHost}${mockProjectDir}`,
}, },
store, store,
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
fetchDeploymentsData, fetchDeploymentsData,
fetchEnvironmentsData, fetchEnvironmentsData,
fetchDashboardData, fetchDashboardData,
fetchAnnotations,
fetchPrometheusMetric, fetchPrometheusMetric,
setInitialState, setInitialState,
filterEnvironments, filterEnvironments,
...@@ -24,10 +25,12 @@ import { ...@@ -24,10 +25,12 @@ import {
} from '~/monitoring/stores/actions'; } from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql'; import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
import storeState from '~/monitoring/stores/state'; import storeState from '~/monitoring/stores/state';
import { import {
deploymentData, deploymentData,
environmentData, environmentData,
annotationsData,
metricsDashboardResponse, metricsDashboardResponse,
metricsDashboardViewModel, metricsDashboardViewModel,
dashboardGitResponse, dashboardGitResponse,
...@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => { ...@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => {
}); });
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => { it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue( jest.spyOn(gqClient, 'mutate').mockReturnValue({
Promise.resolve({ data: {
data: { project: {
project: { data: {
data: { environments: [],
environments: [],
},
}, },
}, },
}), },
); });
return testAction( return testAction(
filterEnvironments, filterEnvironments,
...@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => { ...@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => {
}); });
it('dispatches receiveEnvironmentsDataSuccess on success', () => { it('dispatches receiveEnvironmentsDataSuccess on success', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue( jest.spyOn(gqClient, 'mutate').mockResolvedValue({
Promise.resolve({ data: {
data: { project: {
project: { data: {
data: { environments: environmentData,
environments: environmentData,
},
}, },
}, },
}), },
); });
return testAction( return testAction(
fetchEnvironmentsData, fetchEnvironmentsData,
...@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => { ...@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => {
}); });
it('dispatches receiveEnvironmentsDataFailure on error', () => { it('dispatches receiveEnvironmentsDataFailure on error', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject()); jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
return testAction( return testAction(
fetchEnvironmentsData, fetchEnvironmentsData,
...@@ -220,6 +219,80 @@ describe('Monitoring store actions', () => { ...@@ -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', () => { describe('Set initial state', () => {
let mockedState; let mockedState;
beforeEach(() => { 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', () => { ...@@ -30,7 +30,6 @@ describe('StaticSiteEditor', () => {
store = new Vuex.Store({ store = new Vuex.Store({
state: createState(initialState), state: createState(initialState),
getters: { getters: {
isContentLoaded: () => false,
contentChanged: () => false, contentChanged: () => false,
...getters, ...getters,
}, },
...@@ -43,9 +42,11 @@ describe('StaticSiteEditor', () => { ...@@ -43,9 +42,11 @@ describe('StaticSiteEditor', () => {
}; };
const buildContentLoadedStore = ({ initialState, getters } = {}) => { const buildContentLoadedStore = ({ initialState, getters } = {}) => {
buildStore({ buildStore({
initialState, initialState: {
isContentLoaded: true,
...initialState,
},
getters: { getters: {
isContentLoaded: () => true,
...getters, ...getters,
}, },
}); });
...@@ -85,7 +86,7 @@ describe('StaticSiteEditor', () => { ...@@ -85,7 +86,7 @@ describe('StaticSiteEditor', () => {
const content = 'edit area content'; const content = 'edit area content';
beforeEach(() => { beforeEach(() => {
buildStore({ initialState: { content }, getters: { isContentLoaded: () => true } }); buildContentLoadedStore({ initialState: { content } });
buildWrapper(); buildWrapper();
}); });
......
import createState from '~/static_site_editor/store/state'; 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'; import { sourceContent as content } from '../mock_data';
describe('Static Site Editor Store getters', () => { 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', () => { describe('contentChanged', () => {
it('returns true when content and originalContent are different', () => { it('returns true when content and originalContent are different', () => {
const state = createState({ content, originalContent: 'something else' }); const state = createState({ content, originalContent: 'something else' });
......
...@@ -19,6 +19,7 @@ describe('Static Site Editor Store mutations', () => { ...@@ -19,6 +19,7 @@ describe('Static Site Editor Store mutations', () => {
mutation | stateProperty | payload | expectedValue mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true} ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false} ${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} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content} ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${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