Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
075ce5ae31
commit
2c0e92d031
56 changed files with 1363 additions and 195 deletions
|
@ -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.'));
|
||||
|
|
|
@ -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>
|
41
app/assets/javascripts/alerts_settings/constants.js
Normal file
41
app/assets/javascripts/alerts_settings/constants.js
Normal 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') },
|
||||
];
|
53
app/assets/javascripts/alerts_settings/index.js
Normal file
53
app/assets/javascripts/alerts_settings/index.js
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
27
app/assets/javascripts/alerts_settings/services/index.js
Normal file
27
app/assets/javascripts/alerts_settings/services/index.js
Normal 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',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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') }}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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: '',
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
36
app/helpers/operations_helper.rb
Normal file
36
app/helpers/operations_helper.rb
Normal 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')
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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))
|
||||
|
||||
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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? } }
|
||||
|
|
10
app/views/projects/services/prometheus/_top.html.haml
Normal file
10
app/views/projects/services/prometheus/_top.html.haml
Normal 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'
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add project count with overridden approval rules in merge request to usage ping
|
||||
merge_request: 35224
|
||||
author:
|
||||
type: added
|
|
@ -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
|
||||
|
|
|
@ -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
|
5
changelogs/unreleased/sh-fix-api-error-handling.yml
Normal file
5
changelogs/unreleased/sh-fix-api-error-handling.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix 500 errors with invalid access tokens
|
||||
merge_request: 35895
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
|
@ -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
|
||||
\.
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.com’s 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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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>"
|
||||
`;
|
168
spec/frontend/alert_settings/alert_settings_form_spec.js
Normal file
168
spec/frontend/alert_settings/alert_settings_form_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 }],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
112
spec/helpers/operations_helper_spec.rb
Normal file
112
spec/helpers/operations_helper_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue