Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-02 21:09:14 +00:00
parent 075ce5ae31
commit 2c0e92d031
56 changed files with 1363 additions and 195 deletions

View file

@ -123,9 +123,6 @@ export default {
.then(() => {
this.activated = value;
this.loadingActivated = false;
if (value) {
window.location.reload();
}
})
.catch(() => {
createFlash(__('Update failed. Please try again.'));

View file

@ -0,0 +1,343 @@
<script>
import {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlModal,
GlModalDirective,
GlSprintf,
GlFormSelect,
} from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import csrf from '~/lib/utils/csrf';
import service from '../services';
import { i18n, serviceOptions } from '../constants';
export default {
i18n,
csrf,
components: {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlFormSelect,
GlLink,
GlModal,
GlSprintf,
ClipboardButton,
ToggleButton,
},
directives: {
'gl-modal': GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
prometheus: {
type: Object,
required: true,
validator: ({ prometheusIsActivated }) => {
return prometheusIsActivated !== undefined;
},
},
generic: {
type: Object,
required: true,
validator: ({ formPath }) => {
return formPath !== undefined;
},
},
},
data() {
return {
activated: {
generic: this.generic.initialActivated,
prometheus: this.prometheus.prometheusIsActivated,
},
loading: false,
authorizationKey: {
generic: this.generic.initialAuthorizationKey,
prometheus: this.prometheus.prometheusAuthorizationKey,
},
selectedEndpoint: null,
options: serviceOptions,
prometheusApiKey: this.prometheus.prometheusApiUrl,
feedback: {
variant: 'danger',
feedbackMessage: null,
isFeedbackDismissed: false,
},
};
},
computed: {
sections() {
return [
{
text: this.$options.i18n.usageSection,
url: this.generic.alertsUsageUrl,
},
{
text: this.$options.i18n.setupSection,
url: this.generic.alertsSetupUrl,
},
];
},
isGeneric() {
return this.selectedEndpoint === 'generic';
},
selectedService() {
return this.isGeneric
? {
url: this.generic.url,
authKey: this.authorizationKey.generic,
active: this.activated.generic,
resetKey: this.resetGenericKey.bind(this),
}
: {
authKey: this.authorizationKey.prometheus,
url: this.prometheus.prometheusUrl,
active: this.activated.prometheus,
resetKey: this.resetPrometheusKey.bind(this),
};
},
showFeedbackMsg() {
return this.feedback.feedbackMessage && !this.isFeedbackDismissed;
},
prometheusInfo() {
return !this.isGeneric ? this.$options.i18n.prometheusInfo : '';
},
prometheusFeatureEnabled() {
return !this.isGeneric && this.glFeatures.alertIntegrationsDropdown;
},
},
created() {
if (this.glFeatures.alertIntegrationsDropdown) {
this.selectedEndpoint = this.prometheus.prometheusIsActivated
? this.options[1].value
: this.options[0].value;
} else {
this.selectedEndpoint = this.options[0].value;
}
},
methods: {
dismissFeedback() {
this.feedback = { ...this.feedback, feedbackMessage: null };
this.isFeedbackDismissed = false;
},
resetGenericKey() {
return service
.updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } })
.then(({ data: { token } }) => {
this.authorizationKey.generic = token;
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
resetPrometheusKey() {
return service
.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath })
.then(({ data: { token } }) => {
this.authorizationKey.prometheus = token;
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
toggleActivated(value) {
return this.isGeneric
? this.toggleGenericActivated(value)
: this.togglePrometheusActive(value);
},
toggleGenericActivated(value) {
this.loading = true;
return service
.updateGenericActive({
endpoint: this.generic.formPath,
params: { service: { active: value } },
})
.then(() => {
this.activated.generic = value;
if (value) {
this.setFeedback({
feedbackMessage: this.$options.i18n.endPointActivated,
variant: 'success',
});
}
})
.catch(() => {})
.finally(() => {
this.loading = false;
});
},
togglePrometheusActive(value) {
this.loading = true;
return service
.updatePrometheusActive({
endpoint: this.prometheus.prometheusFormPath,
params: {
token: this.$options.csrf.token,
config: value ? 1 : 0,
url: this.prometheusApiKey,
redirect: window.location,
},
})
.then(() => {
this.activated.prometheus = value;
if (value) {
this.setFeedback({
feedbackMessage: this.$options.i18n.endPointActivated,
variant: 'success',
});
}
})
.catch(() => {
this.setFeedback({
feedbackMessage: this.$options.i18n.errorApiUrlMsg,
variant: 'danger',
});
})
.finally(() => {
this.loading = false;
});
},
setFeedback({ feedbackMessage, variant }) {
this.feedback = { feedbackMessage, variant };
},
onSubmit(evt) {
// TODO: Add form submit as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356
evt.preventDefault();
},
onReset(evt) {
// TODO: Add form reset as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356
evt.preventDefault();
},
},
};
</script>
<template>
<div>
<gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback">
{{ feedback.feedbackMessage }}
</gl-alert>
<div data-testid="alert-settings-description" class="gl-mt-5">
<p v-for="section in sections" :key="section.text">
<gl-sprintf :message="section.text">
<template #link="{ content }">
<gl-link :href="section.url" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<gl-form @submit="onSubmit" @reset="onReset">
<gl-form-group
v-if="glFeatures.alertIntegrationsDropdown"
:label="$options.i18n.integrationsLabel"
label-for="integrations"
label-class="label-bold"
>
<gl-form-select
v-model="selectedEndpoint"
:options="options"
data-testid="alert-settings-select"
/>
<span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.integrationsInfo">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
href="https://gitlab.com/groups/gitlab-org/-/epics/3362"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</gl-form-group>
<gl-form-group
:label="$options.i18n.activeLabel"
label-for="activated"
label-class="label-bold"
>
<toggle-button
id="activated"
:disabled-input="loading"
:is-loading="loading"
:value="selectedService.active"
@change="toggleActivated"
/>
</gl-form-group>
<gl-form-group
v-if="prometheusFeatureEnabled"
:label="$options.i18n.apiBaseUrlLabel"
label-for="api-url"
label-class="label-bold"
>
<gl-form-input
id="api-url"
v-model="prometheusApiKey"
type="url"
:value="prometheusApiKey"
:placeholder="$options.i18n.prometheusApiPlaceholder"
/>
<span class="gl-text-gray-400">
{{ $options.i18n.apiBaseUrlHelpText }}
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.urlLabel" label-for="url" label-class="label-bold">
<div class="input-group">
<gl-form-input id="url" :readonly="true" :value="selectedService.url" />
<span class="input-group-append">
<clipboard-button :text="selectedService.url" :title="$options.i18n.copyToClipboard" />
</span>
</div>
<span class="gl-text-gray-400">
{{ prometheusInfo }}
</span>
</gl-form-group>
<gl-form-group
:label="$options.i18n.authKeyLabel"
label-for="authorization-key"
label-class="label-bold"
>
<div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="selectedService.authKey" />
<span class="input-group-append">
<clipboard-button
:text="selectedService.authKey"
:title="$options.i18n.copyToClipboard"
/>
</span>
</div>
<gl-button v-gl-modal.authKeyModal class="gl-mt-3">{{ $options.i18n.resetKey }}</gl-button>
<gl-modal
modal-id="authKeyModal"
:title="$options.i18n.resetKey"
:ok-title="$options.i18n.resetKey"
ok-variant="danger"
@ok="selectedService.resetKey"
>
{{ $options.i18n.restKeyInfo }}
</gl-modal>
</gl-form-group>
<div
class="footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none"
>
<gl-button type="submit" variant="success" category="primary">
{{ __('Save and test changes') }}
</gl-button>
<gl-button type="reset" variant="default" category="primary">
{{ __('Cancel') }}
</gl-button>
</div>
</gl-form>
</div>
</template>

View file

@ -0,0 +1,41 @@
import { s__ } from '~/locale';
export const i18n = {
usageSection: s__(
'AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.',
),
setupSection: s__(
"AlertSettings|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.",
),
errorMsg: s__(
'AlertSettings|There was an error updating the the alert settings. Please refresh the page to try again.',
),
errorKeyMsg: s__(
'AlertSettings|There was an error while trying to reset the key. Please refresh the page to try again.',
),
errorApiUrlMsg: s__(
'AlertSettings|There was an error while trying to enable the alert settings. Please ensure you are using a valid URL.',
),
prometheusApiPlaceholder: s__('AlertSettings|http://prometheus.example.com/'),
restKeyInfo: s__(
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
endPointActivated: s__('AlertSettings|Alerts endpoint successfully activated.'),
prometheusInfo: s__('AlertSettings|Add URL and auth key to your Prometheus config file'),
integrationsInfo: s__(
'AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}',
),
resetKey: s__('AlertSettings|Reset key'),
copyToClipboard: s__('AlertSettings|Copy'),
integrationsLabel: s__('AlertSettings|Integrations'),
apiBaseUrlLabel: s__('AlertSettings|Prometheus API Base URL'),
authKeyLabel: s__('AlertSettings|Authorization key'),
urlLabel: s__('AlertSettings|Webhook URL'),
activeLabel: s__('AlertSettings|Active'),
apiBaseUrlHelpText: s__(' AlertSettings|URL cannot be blank and must start with http or https'),
};
export const serviceOptions = [
{ value: 'generic', text: s__('AlertSettings|Generic') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
];

View file

@ -0,0 +1,53 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertSettingsForm from './components/alerts_settings_form.vue';
export default el => {
if (!el) {
return null;
}
const {
prometheusActivated,
prometheusUrl,
prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
activated: activatedStr,
alertsSetupUrl,
alertsUsageUrl,
formPath,
authorizationKey,
url,
} = el.dataset;
const activated = parseBoolean(activatedStr);
const prometheusIsActivated = parseBoolean(prometheusActivated);
return new Vue({
el,
render(createElement) {
return createElement(AlertSettingsForm, {
props: {
prometheus: {
prometheusIsActivated,
prometheusUrl,
prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
},
generic: {
alertsSetupUrl,
alertsUsageUrl,
initialActivated: activated,
formPath,
initialAuthorizationKey: authorizationKey,
url,
},
},
});
},
});
};

View file

@ -0,0 +1,27 @@
import axios from '~/lib/utils/axios_utils';
export default {
updateGenericKey({ endpoint, params }) {
return axios.put(endpoint, params);
},
updatePrometheusKey({ endpoint }) {
return axios.post(endpoint);
},
updateGenericActive({ endpoint, params }) {
return axios.put(endpoint, params);
},
updatePrometheusActive({ endpoint, params: { token, config, url, redirect } }) {
const data = new FormData();
data.set('_method', 'put');
data.set('authenticity_token', token);
data.set('service[manual_configuration]', config);
data.set('service[api_url]', url);
data.set('redirect_to', redirect);
return axios.post(endpoint, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
},
};

View file

@ -16,6 +16,7 @@ import {
} from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState } from 'vuex';
import { mapComputed } from '~/vuex_shared/bindings';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
@ -30,6 +31,9 @@ import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
tokens: awsTokens,
tokenList: awsTokenList,
awsTipMessage: AWS_TIP_MESSAGE,
components: {
CiEnvironmentsDropdown,
CiKeyField,
@ -48,9 +52,6 @@ export default {
GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
tokens: awsTokens,
tokenList: awsTokenList,
awsTipMessage: AWS_TIP_MESSAGE,
data() {
return {
isTipDismissed: Cookies.get(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
@ -74,22 +75,34 @@ export default {
'protectedEnvironmentVariablesLink',
'maskedEnvironmentVariablesLink',
]),
...mapComputed(
[
{ key: 'key', updateFn: 'updateVariableKey' },
{ key: 'secret_value', updateFn: 'updateVariableValue' },
{ key: 'variable_type', updateFn: 'updateVariableType' },
{ key: 'environment_scope', updateFn: 'setEnvironmentScope' },
{ key: 'protected_variable', updateFn: 'updateVariableProtected' },
{ key: 'masked', updateFn: 'updateVariableMasked' },
],
false,
'variable',
),
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variableData.key);
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
canSubmit() {
return (
this.variableValidationState &&
this.variableData.key !== '' &&
this.variableData.secret_value !== ''
this.variable.key !== '' &&
this.variable.secret_value !== ''
);
},
canMask() {
const regex = RegExp(this.maskableRegex);
return regex.test(this.variableData.secret_value);
return regex.test(this.variable.secret_value);
},
displayMaskedError() {
return !this.canMask && this.variableData.masked;
return !this.canMask && this.variable.masked;
},
maskedState() {
if (this.displayMaskedError) {
@ -97,9 +110,6 @@ export default {
}
return true;
},
variableData() {
return this.variableBeingEdited || this.variable;
},
modalActionText() {
return this.variableBeingEdited ? __('Update variable') : __('Add variable');
},
@ -107,7 +117,7 @@ export default {
return this.displayMaskedError ? __('This variable can not be masked.') : '';
},
tokenValidationFeedback() {
const tokenSpecificFeedback = this.$options.tokens?.[this.variableData.key]?.invalidMessage;
const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
if (!this.tokenValidationState && tokenSpecificFeedback) {
return tokenSpecificFeedback;
}
@ -119,10 +129,10 @@ export default {
return true;
}
const validator = this.$options.tokens?.[this.variableData.key]?.validation;
const validator = this.$options.tokens?.[this.variable.key]?.validation;
if (validator) {
return validator(this.variableData.secret_value);
return validator(this.variable.secret_value);
}
return true;
@ -131,14 +141,7 @@ export default {
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
},
variableValidationState() {
if (
this.variableData.secret_value === '' ||
(this.tokenValidationState && this.maskedState)
) {
return true;
}
return false;
return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
},
},
methods: {
@ -160,7 +163,7 @@ export default {
this.isTipDismissed = true;
},
deleteVarAndClose() {
this.deleteVariable(this.variableBeingEdited);
this.deleteVariable();
this.hideModal();
},
hideModal() {
@ -169,14 +172,14 @@ export default {
resetModalHandler() {
if (this.variableBeingEdited) {
this.resetEditing();
} else {
this.clearModal();
}
this.clearModal();
this.resetSelectedEnvironment();
},
updateOrAddVariable() {
if (this.variableBeingEdited) {
this.updateVariable(this.variableBeingEdited);
this.updateVariable();
} else {
this.addVariable();
}
@ -204,14 +207,14 @@ export default {
<form>
<ci-key-field
v-if="glFeatures.ciKeyAutocomplete"
v-model="variableData.key"
v-model="key"
:token-list="$options.tokenList"
/>
<gl-form-group v-else :label="__('Key')" label-for="ci-variable-key">
<gl-form-input
id="ci-variable-key"
v-model="variableData.key"
v-model="key"
data-qa-selector="ci_variable_key_field"
/>
</gl-form-group>
@ -225,7 +228,7 @@ export default {
<gl-form-textarea
id="ci-variable-value"
ref="valueField"
v-model="variableData.secret_value"
v-model="secret_value"
:state="variableValidationState"
rows="3"
max-rows="6"
@ -241,11 +244,7 @@ export default {
class="w-50 append-right-15"
:class="{ 'w-100': isGroup }"
>
<gl-form-select
id="ci-variable-type"
v-model="variableData.variable_type"
:options="typeOptions"
/>
<gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
</gl-form-group>
<gl-form-group
@ -256,7 +255,7 @@ export default {
>
<ci-environments-dropdown
class="w-100"
:value="variableData.environment_scope"
:value="environment_scope"
@selectEnvironment="setEnvironmentScope"
@createClicked="addWildCardScope"
/>
@ -264,7 +263,7 @@ export default {
</div>
<gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
<gl-form-checkbox v-model="variableData.protected" class="mb-0">
<gl-form-checkbox v-model="protected_variable" class="mb-0">
{{ __('Protect variable') }}
<gl-link target="_blank" :href="protectedEnvironmentVariablesLink">
<gl-icon name="question" :size="12" />
@ -276,7 +275,7 @@ export default {
<gl-form-checkbox
ref="masked-ci-variable"
v-model="variableData.masked"
v-model="masked"
data-qa-selector="ci_variable_masked_checkbox"
>
{{ __('Mask variable') }}

View file

@ -65,10 +65,10 @@ export const receiveUpdateVariableError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_VARIABLE_ERROR, error);
};
export const updateVariable = ({ state, dispatch }, variable) => {
export const updateVariable = ({ state, dispatch }) => {
dispatch('requestUpdateVariable');
const updatedVariable = prepareDataForApi(variable);
const updatedVariable = prepareDataForApi(state.variable);
updatedVariable.secrect_value = updateVariable.value;
return axios
@ -121,13 +121,13 @@ export const receiveDeleteVariableError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_VARIABLE_ERROR, error);
};
export const deleteVariable = ({ dispatch, state }, variable) => {
export const deleteVariable = ({ dispatch, state }) => {
dispatch('requestDeleteVariable');
const destroy = true;
return axios
.patch(state.endpoint, { variables_attributes: [prepareDataForApi(variable, destroy)] })
.patch(state.endpoint, { variables_attributes: [prepareDataForApi(state.variable, destroy)] })
.then(() => {
dispatch('receiveDeleteVariableSuccess');
dispatch('fetchVariables');
@ -176,3 +176,23 @@ export const resetSelectedEnvironment = ({ commit }) => {
export const setSelectedEnvironment = ({ commit }, environment) => {
commit(types.SET_SELECTED_ENVIRONMENT, environment);
};
export const updateVariableKey = ({ commit }, { key }) => {
commit(types.UPDATE_VARIABLE_KEY, key);
};
export const updateVariableValue = ({ commit }, { secret_value }) => {
commit(types.UPDATE_VARIABLE_VALUE, secret_value);
};
export const updateVariableType = ({ commit }, { variable_type }) => {
commit(types.UPDATE_VARIABLE_TYPE, variable_type);
};
export const updateVariableProtected = ({ commit }, { protected_variable }) => {
commit(types.UPDATE_VARIABLE_PROTECTED, protected_variable);
};
export const updateVariableMasked = ({ commit }, { masked }) => {
commit(types.UPDATE_VARIABLE_MASKED, masked);
};

View file

@ -25,3 +25,9 @@ export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
export const ADD_WILD_CARD_SCOPE = 'ADD_WILD_CARD_SCOPE';
export const RESET_SELECTED_ENVIRONMENT = 'RESET_SELECTED_ENVIRONMENT';
export const SET_SELECTED_ENVIRONMENT = 'SET_SELECTED_ENVIRONMENT';
export const UPDATE_VARIABLE_KEY = 'UPDATE_VARIABLE_KEY';
export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE';
export const UPDATE_VARIABLE_TYPE = 'UPDATE_VARIABLE_TYPE';
export const UPDATE_VARIABLE_PROTECTED = 'UPDATE_VARIABLE_PROTECTED';
export const UPDATE_VARIABLE_MASKED = 'UPDATE_VARIABLE_MASKED';

View file

@ -65,7 +65,8 @@ export default {
},
[types.VARIABLE_BEING_EDITED](state, variable) {
state.variableBeingEdited = variable;
state.variableBeingEdited = true;
state.variable = variable;
},
[types.CLEAR_MODAL](state) {
@ -80,16 +81,12 @@ export default {
},
[types.RESET_EDITING](state) {
state.variableBeingEdited = null;
state.variableBeingEdited = false;
state.showInputValue = false;
},
[types.SET_ENVIRONMENT_SCOPE](state, environment) {
if (state.variableBeingEdited) {
state.variableBeingEdited.environment_scope = environment;
} else {
state.variable.environment_scope = environment;
}
state.variable.environment_scope = environment;
},
[types.ADD_WILD_CARD_SCOPE](state, environment) {
@ -108,4 +105,24 @@ export default {
[types.SET_VARIABLE_PROTECTED](state) {
state.variable.protected = true;
},
[types.UPDATE_VARIABLE_KEY](state, key) {
state.variable.key = key;
},
[types.UPDATE_VARIABLE_VALUE](state, value) {
state.variable.secret_value = value;
},
[types.UPDATE_VARIABLE_TYPE](state, type) {
state.variable.variable_type = type;
},
[types.UPDATE_VARIABLE_PROTECTED](state, bool) {
state.variable.protected_variable = bool;
},
[types.UPDATE_VARIABLE_MASKED](state, bool) {
state.variable.masked = bool;
},
};

View file

@ -12,7 +12,7 @@ export default () => ({
variable_type: displayText.variableText,
key: '',
secret_value: '',
protected: false,
protected_variable: false,
masked: false,
environment_scope: displayText.allEnvironmentsText,
},
@ -21,6 +21,6 @@ export default () => ({
error: null,
environments: [],
typeOptions: [displayText.variableText, displayText.fileText],
variableBeingEdited: null,
variableBeingEdited: false,
selectedEnvironment: '',
});

View file

@ -18,6 +18,7 @@ export const prepareDataForDisplay = variables => {
if (variableCopy.environment_scope === types.allEnvironmentsType) {
variableCopy.environment_scope = displayText.allEnvironmentsText;
}
variableCopy.protected_variable = variableCopy.protected;
variablesToDisplay.push(variableCopy);
});
return variablesToDisplay;
@ -25,7 +26,8 @@ export const prepareDataForDisplay = variables => {
export const prepareDataForApi = (variable, destroy = false) => {
const variableCopy = cloneDeep(variable);
variableCopy.protected = variableCopy.protected.toString();
variableCopy.protected = variableCopy.protected_variable.toString();
delete variableCopy.protected_variable;
variableCopy.masked = variableCopy.masked.toString();
variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type);
if (variableCopy.environment_scope === displayText.allEnvironmentsText) {

View file

@ -1,5 +1,5 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
import initAlertsSettings from '~/alerts_service_settings';
import mountAlertsSettings from '~/alerts_settings';
import mountOperationSettings from '~/operation_settings';
import mountGrafanaIntegration from '~/grafana_integration';
import initSettingsPanels from '~/settings_panels';
@ -11,5 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
if (!IS_EE) {
initSettingsPanels();
}
initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
mountAlertsSettings(document.querySelector('.js-alerts-settings'));
});

View file

@ -41,6 +41,11 @@ export default {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -88,7 +93,11 @@ export default {
<div class="input-group">
<gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" />
<span class="input-group-append">
<clipboard-button :text="notifyUrl" :title="$options.copyToClipboard" />
<clipboard-button
:text="notifyUrl"
:title="$options.copyToClipboard"
:disabled="disabled"
/>
</span>
</div>
</gl-form-group>
@ -100,7 +109,11 @@ export default {
<div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
<span class="input-group-append">
<clipboard-button :text="authorizationKey" :title="$options.copyToClipboard" />
<clipboard-button
:text="authorizationKey"
:title="$options.copyToClipboard"
:disabled="disabled"
/>
</span>
</div>
</gl-form-group>
@ -118,13 +131,20 @@ export default {
)
}}
</gl-modal>
<gl-deprecated-button v-gl-modal.authKeyModal class="js-reset-auth-key">{{
__('Reset key')
}}</gl-deprecated-button>
<gl-deprecated-button
v-gl-modal.authKeyModal
class="js-reset-auth-key"
:disabled="disabled"
>{{ __('Reset key') }}</gl-deprecated-button
>
</template>
<gl-deprecated-button v-else class="js-reset-auth-key" @click="resetKey">{{
__('Generate key')
}}</gl-deprecated-button>
<gl-deprecated-button
v-else
:disabled="disabled"
class="js-reset-auth-key"
@click="resetKey"
>{{ __('Generate key') }}</gl-deprecated-button
>
</div>
</div>
</template>

View file

@ -8,7 +8,7 @@ export default () => {
return;
}
const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl } = el.dataset;
const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl, disabled } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@ -20,6 +20,7 @@ export default () => {
changeKeyUrl,
notifyUrl,
learnMoreUrl,
disabled,
},
});
},

View file

@ -5,15 +5,14 @@ module Projects
class OperationsController < Projects::ApplicationController
before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
before_action do
push_frontend_feature_flag(:alert_integrations_dropdown, project)
end
respond_to :json, only: [:reset_alerting_token]
helper_method :error_tracking_setting
def show
render locals: { prometheus_service: prometheus_service, alerts_service: alerts_service }
end
def update
result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute
@ -48,14 +47,6 @@ module Projects
{ alerting_setting_attributes: { regenerate_token: true } }
end
def prometheus_service
project.find_or_initialize_service(::PrometheusService.to_param)
end
def alerts_service
project.find_or_initialize_service(::AlertsService.to_param)
end
def render_update_response(result)
respond_to do |format|
format.html do

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
module OperationsHelper
include Gitlab::Utils::StrongMemoize
def prometheus_service
strong_memoize(:prometheus_service) do
@project.find_or_initialize_service(::PrometheusService.to_param)
end
end
def alerts_service
strong_memoize(:alerts_service) do
@project.find_or_initialize_service(::AlertsService.to_param)
end
end
def alerts_settings_data
{
'prometheus_activated' => prometheus_service.activated?.to_s,
'activated' => alerts_service.activated?.to_s,
'prometheus_form_path' => scoped_integration_path(prometheus_service),
'form_path' => scoped_integration_path(alerts_service),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(@project),
'prometheus_authorization_key' => @project.alerting_setting&.token,
'prometheus_api_url' => prometheus_service.api_url,
'authorization_key' => alerts_service.token,
'prometheus_url' => notify_project_prometheus_alerts_url(@project, format: :json),
'url' => alerts_service.url,
'alerts_setup_url' => help_page_path('user/project/integrations/generic_alerts.html', anchor: 'setting-up-generic-alerts'),
'alerts_usage_url' => project_alert_management_index_path(@project)
}
end
end
OperationsHelper.prepend_if_ee('EE::OperationsHelper')

View file

@ -48,8 +48,8 @@ module ServicesHelper
end
end
def service_save_button
button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do
def service_save_button(disabled: false)
button_tag(class: 'btn btn-success', type: 'submit', disabled: disabled, data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end

View file

@ -113,7 +113,10 @@ module AlertManagement
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { where(status: status) }
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
scope :for_environment, -> (environment) { where(environment: environment) }
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
scope :open, -> { with_status(:triggered, :acknowledged) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
@ -122,6 +125,7 @@ module AlertManagement
scope :order_status, -> (sort_order) { order(status: sort_order) }
scope :counts_by_status, -> { group(:status).count }
scope :counts_by_project_id, -> { group(:project_id).count }
def self.sort_by_attribute(method)
case method.to_s
@ -140,6 +144,11 @@ module AlertManagement
end
end
def self.last_prometheus_alert_by_project_id
ids = select(arel_table[:id].maximum).group(:project_id)
with_prometheus_alert.where(id: ids)
end
def details
details_payload = payload.except(*attributes.keys, *DETAILS_IGNORED_PARAMS)

View file

@ -135,7 +135,7 @@ module Projects
def create_readme
commit_attrs = {
branch_name: 'master',
branch_name: Gitlab::CurrentSettings.default_branch_name.presence || 'master',
commit_message: 'Initial commit',
file_path: 'README.md',
file_content: "# #{@project.name}\n\n#{@project.description}"

View file

@ -17,7 +17,7 @@
= render 'shared/service_settings', form: form, integration: @service
.footer-block.row-content-block{ :class => "#{'gl-display-none' if @service.is_a?(AlertsService)}" }
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= service_save_button
= service_save_button(disabled: @service.is_a?(AlertsService))
&nbsp;
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'

View file

@ -1,11 +1,7 @@
- return unless show_alerts_moved_alert?
.row
.col-lg-12
.gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert', data: { feature_id: UserCalloutsHelper::ALERTS_MOVED, dismiss_endpoint: user_callouts_path } }
.gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
.gl-alert-body
= _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.')
.gl-alert-actions

View file

@ -5,4 +5,4 @@
- authorization_key = @project.alerting_setting.try(:token)
- learn_more_url = help_page_path('user/project/integrations/prometheus', anchor: 'external-prometheus-instances')
#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url } }
#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: Feature.enabled?(:alert_integrations_dropdown, @service.project) && @service.manual_configuration? } }

View file

@ -0,0 +1,10 @@
- return unless Feature.enabled?(:alert_integrations_dropdown, @service.project) && @service.manual_configuration?
.row
.col-lg-12
.gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
.gl-alert-actions
= link_to _('Visit settings page'), project_settings_operations_path(@project), class: 'btn gl-alert-action btn-info gl-button'

View file

@ -10,9 +10,4 @@
= _('Display alerts from all your monitoring tools directly within GitLab.')
= link_to _('More information'), help_page_path('user/project/operations/alert_management'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-alerts-service-settings{ data: { activated: service.activated?.to_s,
form_path: scoped_integration_path(service),
authorization_key: service.token,
url: service.url || _('<namespace / project>'),
alerts_setup_url: help_page_path('user/project/integrations/generic_alerts.html', anchor: 'setting-up-generic-alerts'),
alerts_usage_url: project_alert_management_index_path(@project) } }
.js-alerts-settings{ data: alerts_settings_data }

View file

@ -2,7 +2,7 @@
- page_title _('Operations Settings')
- breadcrumb_title _('Operations Settings')
= render 'projects/settings/operations/alert_management', service: alerts_service
= render 'projects/settings/operations/alert_management', alerts_service: alerts_service, prometheus_service: prometheus_service
= render 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/prometheus', service: prometheus_service if Feature.enabled?(:settings_operations_prometheus_service)

View file

@ -0,0 +1,5 @@
---
title: Add project count with overridden approval rules in merge request to usage ping
merge_request: 35224
author:
type: added

View file

@ -1,5 +1,5 @@
---
title: Add default_branch_name to application_setteings
title: Add default_branch_name to application_settings
merge_request: 35282
author:
type: other

View file

@ -0,0 +1,6 @@
---
title: Use the application's default_branch_name when available when initializing a new repo with
a README
merge_request: 35801
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix 500 errors with invalid access tokens
merge_request: 35895
author:
type: fixed

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
class AddDisableOverridingApproversPerMergeRequestIndices < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
DISABLE_OVERRIDING_APPROVERS_TRUE_INDEX_NAME = "idx_projects_id_created_at_disable_overriding_approvers_true"
DISABLE_OVERRIDING_APPROVERS_FALSE_INDEX_NAME = "idx_projects_id_created_at_disable_overriding_approvers_false"
disable_ddl_transaction!
def up
add_concurrent_index :projects, [:id, :created_at],
where: "disable_overriding_approvers_per_merge_request = TRUE",
name: DISABLE_OVERRIDING_APPROVERS_TRUE_INDEX_NAME
add_concurrent_index :projects, [:id, :created_at],
where: "(disable_overriding_approvers_per_merge_request = FALSE) OR (disable_overriding_approvers_per_merge_request IS NULL)",
name: DISABLE_OVERRIDING_APPROVERS_FALSE_INDEX_NAME
end
def down
remove_concurrent_index_by_name :projects, DISABLE_OVERRIDING_APPROVERS_TRUE_INDEX_NAME
remove_concurrent_index_by_name :projects, DISABLE_OVERRIDING_APPROVERS_FALSE_INDEX_NAME
end
end

View file

@ -18393,6 +18393,10 @@ CREATE UNIQUE INDEX idx_project_id_payload_key_self_managed_prometheus_alert_eve
CREATE INDEX idx_project_repository_check_partial ON public.projects USING btree (repository_storage, created_at) WHERE (last_repository_check_at IS NULL);
CREATE INDEX idx_projects_id_created_at_disable_overriding_approvers_false ON public.projects USING btree (id, created_at) WHERE ((disable_overriding_approvers_per_merge_request = false) OR (disable_overriding_approvers_per_merge_request IS NULL));
CREATE INDEX idx_projects_id_created_at_disable_overriding_approvers_true ON public.projects USING btree (id, created_at) WHERE (disable_overriding_approvers_per_merge_request = true);
CREATE INDEX idx_projects_on_repository_storage_last_repository_updated_at ON public.projects USING btree (id, repository_storage, last_repository_updated_at);
CREATE INDEX idx_repository_states_on_last_repository_verification_ran_at ON public.project_repository_states USING btree (project_id, last_repository_verification_ran_at) WHERE ((repository_verification_checksum IS NOT NULL) AND (last_repository_verification_failure IS NULL));
@ -23531,6 +23535,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200625045442
20200625082258
20200625190458
20200626060151
20200626130220
\.

View file

@ -461,7 +461,7 @@ group = Group.find_by_path_or_name("groupname")
# Count users from subgroup and up (inherited)
group.members_with_parents.count
# Count users from parent group and down (specific grants)
# Count users from the parent group and down (specific grants)
parent.members_with_descendants.count
```

View file

@ -122,7 +122,7 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------------ | ----------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the immediate parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence |
| `search` | string | no | Return the list of authorized groups matching the search criteria |

View file

@ -312,22 +312,13 @@ We want to avoid a situation when a contributor picks an
because we realize that it does not fit our vision, or we want to solve it in a
different way.
We add the ~"Accepting merge requests" label to:
We automatically add the ~"Accepting merge requests" label to issues
that match the [triage policy](https://about.gitlab.com/handbook/engineering/quality/triage-operations/#accepting-merge-requests).
- Low priority ~bug issues (i.e. we do not add it to the bugs that we want to
solve in the ~"Next Patch Release")
- Small ~feature
- Small ~"technical debt" issues
After adding the ~"Accepting merge requests" label, we try to estimate the
[weight](#issue-weight) of the issue. We use issue weight to let contributors
know how difficult the issue is. Additionally:
- We advertise [`Accepting merge requests` issues with weight < 5](https://gitlab.com/groups/gitlab-org/-/issues?state=opened&label_name[]=Accepting+merge+requests&assignee_id=None&sort=weight)
as suitable for people that have never contributed to GitLab before on the
[Up For Grabs campaign](https://up-for-grabs.net/#/)
- We encourage people that have never contributed to any open source project to
look for [`Accepting merge requests` issues with a weight of 1](https://gitlab.com/groups/gitlab-org/-/issues?state=opened&label_name[]=Accepting+merge+requests&assignee_id=None&sort=weight&weight=1)
We recommend people that have never contributed to any open source project to
look for issues labeled `~"Accepting merge requests"` with a [weight of 1](https://gitlab.com/groups/gitlab-org/-/issues?state=opened&label_name[]=Accepting+merge+requests&assignee_id=None&sort=weight&weight=1).
More experienced contributors are very welcome to tackle
[any of them](https://gitlab.com/groups/gitlab-org/-/issues?state=opened&label_name[]=Accepting+merge+requests&assignee_id=None).
If you've decided that you would like to work on an issue, please @-mention
the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what)

View file

@ -188,7 +188,7 @@ For example, to add support for files referenced by a `Widget` model with a
1. Create `ee/app/replicators/geo/widget_replicator.rb`. Implement the
`#carrierwave_uploader` method which should return a `CarrierWave::Uploader`.
And implement the private `#model` method to return the `Widget` class.
And implement the class method `.model` to return the `Widget` class.
```ruby
# frozen_string_literal: true
@ -197,14 +197,12 @@ For example, to add support for files referenced by a `Widget` model with a
class WidgetReplicator < Gitlab::Geo::Replicator
include ::Geo::BlobReplicatorStrategy
def carrierwave_uploader
model_record.file
def self.model
::Widget
end
private
def model
::Widget
def carrierwave_uploader
model_record.file
end
end
end
@ -235,20 +233,32 @@ For example, to add support for files referenced by a `Widget` model with a
class CreateWidgetRegistry < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :widget_registry, id: :serial, force: :cascade do |t|
t.integer :widget_id, null: false
t.integer :state, default: 0, null: false
t.integer :retry_count, default: 0
t.string :last_sync_failure, limit: 255
t.datetime_with_timezone :retry_at
t.datetime_with_timezone :last_synced_at
t.datetime_with_timezone :created_at, null: false
disable_ddl_transaction!
t.index :widget_id, name: :index_widget_registry_on_repository_id, using: :btree
t.index :retry_at, name: :index_widget_registry_on_retry_at, using: :btree
t.index :state, name: :index_widget_registry_on_state, using: :btree
def up
unless table_exists?(:widget_registry)
ActiveRecord::Base.transaction do
create_table :widget_registry, id: :bigserial, force: :cascade do |t|
t.integer :widget_id, null: false
t.integer :state, default: 0, null: false, limit: 2
t.integer :retry_count, default: 0, limit: 2
t.text :last_sync_failure
t.datetime_with_timezone :retry_at
t.datetime_with_timezone :last_synced_at
t.datetime_with_timezone :created_at, null: false
t.index :widget_id
t.index :retry_at
t.index :state
end
end
end
add_text_limit :widget_registry, :last_sync_failure, 255
end
def down
drop_table :widget_registry
end
end
```

View file

@ -55,10 +55,10 @@ levels are available (defined in the `Gitlab::Access` module):
- Maintainer (`40`)
- Owner (`50`)
If a user is the member of both a project and the project parent group, the
If a user is the member of both a project and the project parent group(s), the
higher permission is taken into account for the project.
If a user is the member of a project, but not the parent group (or groups), they
If a user is the member of a project, but not the parent group(s), they
can still view the groups and their entities (like epics).
Project membership (where the group membership is already taken into account)

View file

@ -299,7 +299,7 @@ Paste the SQL query into `#database-lab` to see how the query performs at scale.
- `#database-lab` is a Slack channel which uses a production-sized environment to test your queries.
- GitLab.coms production database has a 15 second timeout.
- For each query we require an execution time of under 1 second due to cold caches which can 10x this time.
- Any single query must stay below 1 second execution time with cold caches.
- Add a specialized index on columns involved to reduce the execution time.
In order to have an understanding of the query's execution we add in the MR description the following information:

View file

@ -59,7 +59,7 @@ it. The restriction for visibility levels on the application setting level also
applies to groups, so if that's set to internal, the explore page will be empty
for anonymous users. The group page now has a visibility level icon.
Admin users cannot create subgroups or projects with higher visibility level than that of the parent group.
Admin users cannot create subgroups or projects with higher visibility level than that of the immediate parent group.
## Visibility of users

View file

@ -229,7 +229,7 @@ To move an issue to another epic:
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
If you have the necessary [permissions](../../permissions.md) to close an issue and create an
epic in the parent group, you can promote an issue to an epic with the `/promote`
epic in the immediate parent group, you can promote an issue to an epic with the `/promote`
[quick action](../../project/quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
Only issues from projects that are in groups can be promoted. When attempting to promote a confidential
issue, a warning will display. Promoting a confidential issue to an epic will make all information

View file

@ -31,7 +31,7 @@ Each group on the **Groups** page is listed with:
- How many subgroups it has.
- How many projects it contains.
- How many members the group has, not including members inherited from parent groups.
- How many members the group has, not including members inherited from parent group(s).
- The group's visibility.
- A link to the group's settings, if you have sufficient permissions.
- A link to leave the group, if you are a member.
@ -397,7 +397,7 @@ When transferring groups, note:
- Changing a group's parent can have unintended side effects. See [Redirects when changing repository paths](../project/index.md#redirects-when-changing-repository-paths).
- You can only transfer groups to groups you manage.
- You must update your local repositories to point to the new location.
- If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will change to match the new parent group's visibility.
- If the immediate parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will change to match the new parent group's visibility.
- Only explicit group membership is transferred, not inherited membership. If the group's owners have only inherited membership, this leaves the group without an owner. In this case, the user transferring the group becomes the group's owner.
## Group settings
@ -571,9 +571,9 @@ You can only choose projects in the group as the template source.
This includes projects shared with the group, but it **excludes** projects in
subgroups or parent groups of the group being configured.
You can configure this feature for both subgroups and parent groups. A project
You can configure this feature for both subgroups and immediate parent groups. A project
in a subgroup will have access to the templates for that subgroup, as well as
any parent groups.
any immediate parent groups.
![Group file template dropdown](img/group_file_template_dropdown.png)

View file

@ -215,7 +215,7 @@ On subsequent visits, you should be able to go [sign in to GitLab.com with SAML]
### Role
The first time you sign in, GitLab adds you to the parent group with the Guest role. Existing members with appropriate privileges can promote that new user.
The first time you sign in, GitLab adds you to the top-level parent group with the Guest role. Existing members with appropriate privileges can promote that new user.
If a user is already a member of the group, linking the SAML identity does not change their role.

View file

@ -25,7 +25,7 @@ For more information on allowed permissions in groups and projects, see
## Overview
A group can have many subgroups inside it, and at the same time a group can have
only 1 parent group. It resembles a directory behavior or a nested items list:
only one immediate parent group. It resembles a directory behavior or a nested items list:
- Group 1
- Group 1.1
@ -89,7 +89,7 @@ of words that are not allowed to be used as group names see the
[reserved names](../../reserved_names.md).
Users can always create subgroups if they are explicitly added as an Owner (or
Maintainer, if that setting is enabled) to a parent group, even if group
Maintainer, if that setting is enabled) to an immediate parent group, even if group
creation is disabled by an administrator in their settings.
To create a subgroup:
@ -99,9 +99,9 @@ To create a subgroup:
![Subgroups page](img/create_subgroup_button.png)
1. Create a new group like you would normally do. Notice that the parent group
1. Create a new group like you would normally do. Notice that the immediate parent group
namespace is fixed under **Group path**. The visibility level can differ from
the parent group.
the immediate parent group.
![Subgroups page](img/create_new_group.png)
@ -113,12 +113,13 @@ Follow the same process to create any subsequent groups.
## Membership
When you add a member to a subgroup, they inherit the membership and permission
level from the parent group. This model allows access to nested groups if you
level from the parent group(s). This model allows access to nested groups if you
have membership in one of its parents.
Jobs for pipelines in subgroups can use [Runners](../../../ci/runners/README.md) registered to the parent group. This means secrets configured for the parent group are available to subgroup jobs.
Jobs for pipelines in subgroups can use [Runners](../../../ci/runners/README.md) registered to the parent group(s).
This means secrets configured for the parent group are available to subgroup jobs.
In addition, maintainers of projects that belong to subgroups can see the details of Runners registered to parent groups.
In addition, maintainers of projects that belong to subgroups can see the details of Runners registered to parent group(s).
The group permissions for a member can be changed only by Owners, and only on
the **Members** page of the group the member was added.

View file

@ -283,7 +283,7 @@ group.
### Subgroup permissions
When you add a member to a subgroup, they inherit the membership and
permission level from the parent group. This model allows access to
permission level from the parent group(s). This model allows access to
nested groups if you have membership in one of its parents.
To learn more, read through the documentation on

View file

@ -54,7 +54,7 @@ and edit labels.
View the project labels list by going to the project and clicking **Issues > Labels**.
The list includes all labels that are defined at the project level, as well as all
labels inherited from the parent group. You can filter the list by entering a search
labels inherited from the immediate parent group. You can filter the list by entering a search
query at the top and clicking search (**{search}**).
To create a new project label:

View file

@ -153,16 +153,14 @@ module API
{ scope: e.scopes })
end
finished_response = nil
response.finish do |rack_response|
# Grape expects a Rack::Response
# (https://github.com/ruby-grape/grape/commit/c117bff7d22971675f4b34367d3a98bc31c8fc02),
# and we need to retrieve it here:
# https://github.com/nov/rack-oauth2/blob/40c9a99fd80486ccb8de0e4869ae384547c0d703/lib/rack/oauth2/server/abstract/error.rb#L28
finished_response = rack_response
end
status, headers, body = response.finish
finished_response
# Grape expects a Rack::Response
# (https://github.com/ruby-grape/grape/commit/c117bff7d22971675f4b34367d3a98bc31c8fc02),
# so we need to recreate the response again even though
# response.finish already does this.
# (https://github.com/nov/rack-oauth2/blob/40c9a99fd80486ccb8de0e4869ae384547c0d703/lib/rack/oauth2/server/abstract/error.rb#L26).
Rack::Response.new(body, status, headers)
end
end
end

View file

@ -16,6 +16,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid " AlertSettings|URL cannot be blank and must start with http or https"
msgstr ""
msgid " %{start} to %{end}"
msgstr ""
@ -2059,6 +2062,66 @@ msgstr ""
msgid "AlertService|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page."
msgstr ""
msgid "AlertSettings|Active"
msgstr ""
msgid "AlertSettings|Add URL and auth key to your Prometheus config file"
msgstr ""
msgid "AlertSettings|Alerts endpoint successfully activated."
msgstr ""
msgid "AlertSettings|Authorization key"
msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
msgid "AlertSettings|External Prometheus"
msgstr ""
msgid "AlertSettings|Generic"
msgstr ""
msgid "AlertSettings|Integrations"
msgstr ""
msgid "AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}"
msgstr ""
msgid "AlertSettings|Prometheus API Base URL"
msgstr ""
msgid "AlertSettings|Reset key"
msgstr ""
msgid "AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in."
msgstr ""
msgid "AlertSettings|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
msgid "AlertSettings|There was an error updating the the alert settings. Please refresh the page to try again."
msgstr ""
msgid "AlertSettings|There was an error while trying to enable the alert settings. Please ensure you are using a valid URL."
msgstr ""
msgid "AlertSettings|There was an error while trying to reset the key. Please refresh the page to try again."
msgstr ""
msgid "AlertSettings|Webhook URL"
msgstr ""
msgid "AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated."
msgstr ""
msgid "AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page."
msgstr ""
msgid "AlertSettings|http://prometheus.example.com/"
msgstr ""
msgid "Alerts"
msgstr ""
@ -19717,6 +19780,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
msgid "Save and test changes"
msgstr ""
msgid "Save anyway"
msgstr ""

View file

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm prometheus is active renders a valid "select" 1`] = `"<gl-form-select-stub options=\\"[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"prometheus\\"></gl-form-select-stub>"`;
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
"<div>
<!---->
<div data-testid=\\"alert-settings-description\\" class=\\"gl-mt-5\\">
<p>
<gl-sprintf-stub message=\\"You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.\\"></gl-sprintf-stub>
</p>
<p>
<gl-sprintf-stub message=\\"Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.\\"></gl-sprintf-stub>
</p>
</div>
<gl-form-stub>
<!---->
<gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\" label-class=\\"label-bold\\">
<toggle-button-stub id=\\"activated\\"></toggle-button-stub>
</gl-form-group-stub>
<!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\" label-class=\\"label-bold\\">
<div class=\\"input-group\\">
<gl-form-input-stub id=\\"url\\" readonly=\\"true\\" value=\\"/alerts/notify.json\\"></gl-form-input-stub> <span class=\\"input-group-append\\"><clipboard-button-stub text=\\"/alerts/notify.json\\" title=\\"Copy\\" tooltipplacement=\\"top\\" cssclass=\\"btn-default\\"></clipboard-button-stub></span>
</div> <span class=\\"gl-text-gray-400\\">
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<div class=\\"input-group\\">
<gl-form-input-stub id=\\"authorization-key\\" readonly=\\"true\\" value=\\"abcedfg123\\"></gl-form-input-stub> <span class=\\"input-group-append\\"><clipboard-button-stub text=\\"abcedfg123\\" title=\\"Copy\\" tooltipplacement=\\"top\\" cssclass=\\"btn-default\\"></clipboard-button-stub></span>
</div>
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
</gl-modal-stub>
</gl-form-group-stub>
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none\\">
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" type=\\"submit\\">
Save and test changes
</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" type=\\"reset\\">
Cancel
</gl-button-stub>
</div>
</gl-form-stub>
</div>"
`;

View file

@ -0,0 +1,168 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlAlert } from '@gitlab/ui';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
const PROMETHEUS_URL = '/prometheus/alerts/notify.json';
const GENERIC_URL = '/alerts/notify.json';
const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid';
const ACTIVATED = false;
const defaultProps = {
generic: {
initialAuthorizationKey: KEY,
formPath: INVALID_URL,
url: GENERIC_URL,
alertsSetupUrl: INVALID_URL,
alertsUsageUrl: INVALID_URL,
initialActivated: ACTIVATED,
},
prometheus: {
prometheusAuthorizationKey: KEY,
prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL,
prometheusIsActivated: ACTIVATED,
},
};
describe('AlertsSettingsForm', () => {
let wrapper;
let mockAxios;
const createComponent = (
props = defaultProps,
{ methods } = {},
alertIntegrationsDropdown = false,
) => {
wrapper = shallowMount(AlertsSettingsForm, {
propsData: {
...defaultProps,
...props,
},
methods,
provide: {
glFeatures: {
alertIntegrationsDropdown,
},
},
});
};
const findSelect = () => wrapper.find('[data-testid="alert-settings-select"]');
const findUrl = () => wrapper.find('#url');
const findAuthorizationKey = () => wrapper.find('#authorization-key');
const findApiUrl = () => wrapper.find('#api-url');
beforeEach(() => {
mockAxios = new MockAdapter(axios);
setFixtures(`
<div>
<span class="js-service-active-status fa fa-circle" data-value="true"></span>
<span class="js-service-active-status fa fa-power-off" data-value="false"></span>
</div>`);
});
afterEach(() => {
wrapper.destroy();
mockAxios.restore();
});
describe('with default values', () => {
beforeEach(() => {
createComponent();
});
it('renders the initial template', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('reset key', () => {
it('triggers resetKey method', () => {
const resetGenericKey = jest.fn();
const methods = { resetGenericKey };
createComponent(defaultProps, { methods });
wrapper.find(GlModal).vm.$emit('ok');
expect(resetGenericKey).toHaveBeenCalled();
});
it('updates the authorization key on success', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' });
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.resetGenericKey().then(() => {
expect(findAuthorizationKey().attributes('value')).toBe('newToken');
});
});
it('shows a alert message on error', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(404);
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.resetGenericKey().then(() => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
});
});
});
describe('activate toggle', () => {
it('triggers toggleActivated method', () => {
const toggleActivated = jest.fn();
const methods = { toggleActivated };
createComponent(defaultProps, { methods });
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(toggleActivated).toHaveBeenCalled();
});
describe('error is encountered', () => {
beforeEach(() => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(500);
});
it('restores previous value', () => {
createComponent({ generic: { ...defaultProps.generic, initialActivated: false } });
return wrapper.vm.resetGenericKey().then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(false);
});
});
});
});
describe('prometheus is active', () => {
beforeEach(() => {
createComponent(
{ prometheus: { ...defaultProps.prometheus, prometheusIsActivated: true } },
{},
true,
);
});
it('renders a valid "select"', () => {
expect(findSelect().html()).toMatchSnapshot();
});
it('shows the API URL input', () => {
expect(findApiUrl().exists()).toBe(true);
});
it('show a valid Alert URL', () => {
expect(findUrl().exists()).toBe(true);
expect(findUrl().attributes('value')).toBe(PROMETHEUS_URL);
});
it('should not show a footer block', () => {
expect(wrapper.find('.footer-block').classes('d-none')).toBe(true);
});
});
});

View file

@ -159,10 +159,7 @@ describe('Ci variable modal', () => {
it('Update variable button dispatches updateVariable with correct variable', () => {
addOrUpdateButton(2).vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'updateVariable',
store.state.variableBeingEdited,
);
expect(store.dispatch).toHaveBeenCalledWith('updateVariable');
});
it('Resets the editing state once modal is hidden', () => {
@ -172,7 +169,7 @@ describe('Ci variable modal', () => {
it('dispatches deleteVariable with correct variable to delete', () => {
deleteVariableButton().vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('deleteVariable', mockData.mockVariables[0]);
expect(store.dispatch).toHaveBeenCalledWith('deleteVariable');
});
});

View file

@ -42,6 +42,7 @@ export default {
key: 'test_var',
masked: false,
protected: false,
protected_variable: false,
secret_value: 'test_val',
value: 'test_val',
variable_type: 'Variable',
@ -52,6 +53,7 @@ export default {
key: 'test_var_2',
masked: false,
protected: false,
protected_variable: false,
secret_value: 'test_val_2',
value: 'test_val_2',
variable_type: 'File',

View file

@ -91,7 +91,7 @@ describe('CI variable list store actions', () => {
testAction(
actions.deleteVariable,
mockVariable,
{},
state,
[],
[
@ -110,7 +110,7 @@ describe('CI variable list store actions', () => {
testAction(
actions.deleteVariable,
mockVariable,
{},
state,
[],
[
@ -134,7 +134,7 @@ describe('CI variable list store actions', () => {
testAction(
actions.updateVariable,
mockVariable,
{},
state,
[],
[
@ -286,4 +286,66 @@ describe('CI variable list store actions', () => {
);
});
});
describe('Update variable values', () => {
it('updateVariableKey', () => {
testAction(
actions.updateVariableKey,
{ key: mockVariable.key },
{},
[
{
type: types.UPDATE_VARIABLE_KEY,
payload: mockVariable.key,
},
],
[],
);
});
it('updateVariableValue', () => {
testAction(
actions.updateVariableValue,
{ secret_value: mockVariable.value },
{},
[
{
type: types.UPDATE_VARIABLE_VALUE,
payload: mockVariable.value,
},
],
[],
);
});
it('updateVariableType', () => {
testAction(
actions.updateVariableType,
{ variable_type: mockVariable.variable_type },
{},
[{ type: types.UPDATE_VARIABLE_TYPE, payload: mockVariable.variable_type }],
[],
);
});
it('updateVariableProtected', () => {
testAction(
actions.updateVariableProtected,
{ protected_variable: mockVariable.protected },
{},
[{ type: types.UPDATE_VARIABLE_PROTECTED, payload: mockVariable.protected }],
[],
);
});
it('updateVariableMasked', () => {
testAction(
actions.updateVariableMasked,
{ masked: mockVariable.masked },
{},
[{ type: types.UPDATE_VARIABLE_MASKED, payload: mockVariable.masked }],
[],
);
});
});
});

View file

@ -4,15 +4,6 @@ import * as types from '~/ci_variable_list/store/mutation_types';
describe('CI variable list mutations', () => {
let stateCopy;
const variableBeingEdited = {
environment_scope: '*',
id: 63,
key: 'test_var',
masked: false,
protected: false,
value: 'test_val',
variable_type: 'env_var',
};
beforeEach(() => {
stateCopy = state();
@ -29,18 +20,18 @@ describe('CI variable list mutations', () => {
});
describe('VARIABLE_BEING_EDITED', () => {
it('should set variable that is being edited', () => {
mutations[types.VARIABLE_BEING_EDITED](stateCopy, variableBeingEdited);
it('should set the variable that is being edited', () => {
mutations[types.VARIABLE_BEING_EDITED](stateCopy);
expect(stateCopy.variableBeingEdited).toEqual(variableBeingEdited);
expect(stateCopy.variableBeingEdited).toBe(true);
});
});
describe('RESET_EDITING', () => {
it('should reset variableBeingEdited to null', () => {
it('should reset variableBeingEdited to false', () => {
mutations[types.RESET_EDITING](stateCopy);
expect(stateCopy.variableBeingEdited).toEqual(null);
expect(stateCopy.variableBeingEdited).toBe(false);
});
});
@ -74,15 +65,7 @@ describe('CI variable list mutations', () => {
describe('SET_ENVIRONMENT_SCOPE', () => {
const environment = 'production';
it('should set scope to variable being updated if updating variable', () => {
stateCopy.variableBeingEdited = variableBeingEdited;
mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
expect(stateCopy.variableBeingEdited.environment_scope).toBe('production');
});
it('should set scope to variable if adding new variable', () => {
it('should set environment scope on variable', () => {
mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
expect(stateCopy.variable.environment_scope).toBe('production');
@ -105,4 +88,49 @@ describe('CI variable list mutations', () => {
expect(stateCopy.variable.protected).toBe(true);
});
});
describe('UPDATE_VARIABLE_KEY', () => {
it('should update variable key value', () => {
const key = 'new_var';
mutations[types.UPDATE_VARIABLE_KEY](stateCopy, key);
expect(stateCopy.variable.key).toBe(key);
});
});
describe('UPDATE_VARIABLE_VALUE', () => {
it('should update variable value', () => {
const value = 'variable_value';
mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, value);
expect(stateCopy.variable.secret_value).toBe(value);
});
});
describe('UPDATE_VARIABLE_TYPE', () => {
it('should update variable type value', () => {
const type = 'File';
mutations[types.UPDATE_VARIABLE_TYPE](stateCopy, type);
expect(stateCopy.variable.variable_type).toBe(type);
});
});
describe('UPDATE_VARIABLE_PROTECTED', () => {
it('should update variable protected value', () => {
const protectedValue = true;
mutations[types.UPDATE_VARIABLE_PROTECTED](stateCopy, protectedValue);
expect(stateCopy.variable.protected_variable).toBe(protectedValue);
});
});
describe('UPDATE_VARIABLE_MASKED', () => {
it('should update variable masked value', () => {
const masked = true;
mutations[types.UPDATE_VARIABLE_MASKED](stateCopy, masked);
expect(stateCopy.variable.masked).toBe(masked);
});
});
});

View file

@ -0,0 +1,112 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe OperationsHelper do
include Gitlab::Routing
describe '#alerts_settings_data' do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project) }
subject { helper.alerts_settings_data }
before do
helper.instance_variable_set(:@project, project)
allow(helper).to receive(:current_user) { user }
allow(helper).to receive(:can?).with(user, :admin_operations, project) { true }
end
context 'initial service configuration' do
let_it_be(:alerts_service) { AlertsService.new(project: project) }
let_it_be(:prometheus_service) { PrometheusService.new(project: project) }
before do
allow(project).to receive(:find_or_initialize_service).with('alerts').and_return(alerts_service)
allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return(prometheus_service)
end
it 'returns the correct values' do
expect(subject).to eq(
'activated' => 'false',
'url' => alerts_service.url,
'authorization_key' => nil,
'form_path' => project_service_path(project, alerts_service),
'alerts_setup_url' => help_page_path('user/project/integrations/generic_alerts.html', anchor: 'setting-up-generic-alerts'),
'alerts_usage_url' => project_alert_management_index_path(project),
'prometheus_form_path' => project_service_path(project, prometheus_service),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project),
'prometheus_authorization_key' => nil,
'prometheus_api_url' => nil,
'prometheus_activated' => 'false',
'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json)
)
end
end
context 'with external Prometheus configured' do
let_it_be(:prometheus_service, reload: true) { create(:prometheus_service, project: project) }
context 'with external Prometheus enabled' do
it 'returns the correct values' do
expect(subject).to include(
'prometheus_activated' => 'true',
'prometheus_api_url' => prometheus_service.api_url
)
end
end
context 'with external Prometheus disabled' do
before do
# Prometheus services uses manual_configuration as an alias for active, beware
prometheus_service.update!(manual_configuration: false)
end
it 'returns the correct values' do
expect(subject).to include(
'prometheus_activated' => 'false',
'prometheus_api_url' => prometheus_service.api_url
)
end
end
context 'with project alert setting' do
let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
it 'returns the correct values' do
expect(subject).to include(
'prometheus_authorization_key' => project_alerting_setting.token,
'prometheus_api_url' => prometheus_service.api_url
)
end
end
end
context 'with generic alerts service configured' do
let_it_be(:alerts_service) { create(:alerts_service, project: project) }
context 'with generic alerts enabled' do
it 'returns the correct values' do
expect(subject).to include(
'activated' => 'true',
'authorization_key' => alerts_service.token,
'url' => alerts_service.url
)
end
end
context 'with generic alerts disabled' do
before do
alerts_service.update!(active: false)
end
it 'returns the correct values' do
expect(subject).to include(
'activated' => 'false',
'authorization_key' => alerts_service.token
)
end
end
end
end
end

View file

@ -165,6 +165,15 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to contain_exactly(alert_with_fingerprint) }
end
describe '.for_environment' do
let(:environment) { create(:environment, project: project) }
let!(:env_alert) { create(:alert_management_alert, project: project, environment: environment) }
subject { described_class.for_environment(environment) }
it { is_expected.to match_array(env_alert) }
end
describe '.counts_by_status' do
subject { described_class.counts_by_status }
@ -176,6 +185,43 @@ RSpec.describe AlertManagement::Alert do
)
end
end
describe '.counts_by_project_id' do
subject { described_class.counts_by_project_id }
let!(:alert_other_project) { create(:alert_management_alert) }
it do
is_expected.to eq(
project.id => 3,
alert_other_project.project.id => 1
)
end
end
describe '.open' do
subject { described_class.open }
let!(:acknowledged_alert) { create(:alert_management_alert, :acknowledged, project: project)}
it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
end
end
describe '.last_prometheus_alert_by_project_id' do
subject { described_class.last_prometheus_alert_by_project_id }
let(:project_1) { create(:project) }
let!(:alert_1) { create(:alert_management_alert, project: project_1) }
let!(:alert_2) { create(:alert_management_alert, project: project_1) }
let(:project_2) { create(:project) }
let!(:alert_3) { create(:alert_management_alert, project: project_2) }
let!(:alert_4) { create(:alert_management_alert, project: project_2) }
it 'returns the latest alert for each project' do
expect(subject).to contain_exactly(alert_2, alert_4)
end
end
describe '.search' do

View file

@ -36,6 +36,14 @@ RSpec.describe API::API do
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not authorize user for revoked token' do
revoked = create(:personal_access_token, :revoked, user: user, scopes: [:read_api])
get api('/groups', personal_access_token: revoked)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'does not authorize user for post request' do
params = attributes_for_group_api

View file

@ -446,14 +446,35 @@ RSpec.describe Projects::CreateService, '#execute' do
end
context 'when readme initialization is requested' do
it 'creates README.md' do
let(:project) { create_project(user, opts) }
before do
opts[:initialize_with_readme] = '1'
end
project = create_project(user, opts)
shared_examples 'creates README.md' do
it { expect(project.repository.commit_count).to be(1) }
it { expect(project.repository.readme.name).to eql('README.md') }
it { expect(project.repository.readme.data).to include('# GitLab') }
end
expect(project.repository.commit_count).to be(1)
expect(project.repository.readme.name).to eql('README.md')
expect(project.repository.readme.data).to include('# GitLab')
it_behaves_like 'creates README.md'
context 'and a default_branch_name is specified' do
before do
allow(Gitlab::CurrentSettings)
.to receive(:default_branch_name)
.and_return('example_branch')
end
it_behaves_like 'creates README.md'
it 'creates README.md within the specified branch rather than master' do
branches = project.repository.branches
expect(branches.size).to eq(1)
expect(branches.collect(&:name)).to contain_exactly('example_branch')
end
end
end