Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
80e9fdc968
commit
76358aee81
38 changed files with 1368 additions and 297 deletions
|
@ -0,0 +1,96 @@
|
||||||
|
<script>
|
||||||
|
import { GlDeprecatedButton } from '@gitlab/ui';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
import csrf from '~/lib/utils/csrf';
|
||||||
|
import CustomMetricsFormFields from './custom_metrics_form_fields.vue';
|
||||||
|
import DeleteCustomMetricModal from './delete_custom_metric_modal.vue';
|
||||||
|
import { formDataValidator } from '../constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CustomMetricsFormFields,
|
||||||
|
DeleteCustomMetricModal,
|
||||||
|
GlDeprecatedButton,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
customMetricsPath: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
metricPersisted: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
editProjectServicePath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
validateQueryPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: formDataValidator,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formIsValid: null,
|
||||||
|
errorMessage: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
saveButtonText() {
|
||||||
|
return this.metricPersisted ? __('Save Changes') : s__('Metrics|Create metric');
|
||||||
|
},
|
||||||
|
titleText() {
|
||||||
|
return this.metricPersisted ? s__('Metrics|Edit metric') : s__('Metrics|New metric');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.csrf = csrf.token != null ? csrf.token : '';
|
||||||
|
this.formOperation = this.metricPersisted ? 'patch' : 'post';
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formValidation(isValid) {
|
||||||
|
this.formIsValid = isValid;
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
this.$refs.form.submit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="row my-3">
|
||||||
|
<h4 class="prepend-top-0 col-lg-8 offset-lg-2">{{ titleText }}</h4>
|
||||||
|
<form ref="form" class="col-lg-8 offset-lg-2" :action="customMetricsPath" method="post">
|
||||||
|
<custom-metrics-form-fields
|
||||||
|
:form-operation="formOperation"
|
||||||
|
:form-data="formData"
|
||||||
|
:metric-persisted="metricPersisted"
|
||||||
|
:validate-query-path="validateQueryPath"
|
||||||
|
@formValidation="formValidation"
|
||||||
|
/>
|
||||||
|
<div class="form-actions">
|
||||||
|
<gl-deprecated-button variant="success" :disabled="!formIsValid" @click="submit">
|
||||||
|
{{ saveButtonText }}
|
||||||
|
</gl-deprecated-button>
|
||||||
|
<gl-deprecated-button
|
||||||
|
variant="secondary"
|
||||||
|
class="float-right"
|
||||||
|
:href="editProjectServicePath"
|
||||||
|
>{{ __('Cancel') }}</gl-deprecated-button
|
||||||
|
>
|
||||||
|
<delete-custom-metric-modal
|
||||||
|
v-if="metricPersisted"
|
||||||
|
:delete-metric-url="customMetricsPath"
|
||||||
|
:csrf-token="csrf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,294 @@
|
||||||
|
<script>
|
||||||
|
import { GlFormInput, GlLink, GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import csrf from '~/lib/utils/csrf';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
import statusCodes from '~/lib/utils/http_status';
|
||||||
|
import { backOff } from '~/lib/utils/common_utils';
|
||||||
|
import { queryTypes, formDataValidator } from '../constants';
|
||||||
|
|
||||||
|
const VALIDATION_REQUEST_TIMEOUT = 10000;
|
||||||
|
const axiosCancelToken = axios.CancelToken;
|
||||||
|
let cancelTokenSource;
|
||||||
|
|
||||||
|
function backOffRequest(makeRequestCallback) {
|
||||||
|
return backOff((next, stop) => {
|
||||||
|
makeRequestCallback()
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.status === statusCodes.OK) {
|
||||||
|
stop(resp);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// If the request is cancelled by axios
|
||||||
|
// then consider it as noop so that its not
|
||||||
|
// caught by subsequent catches
|
||||||
|
.catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown)));
|
||||||
|
}, VALIDATION_REQUEST_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlFormInput,
|
||||||
|
GlLink,
|
||||||
|
GlFormGroup,
|
||||||
|
GlFormRadioGroup,
|
||||||
|
GlLoadingIcon,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
formOperation: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({
|
||||||
|
title: '',
|
||||||
|
yLabel: '',
|
||||||
|
query: '',
|
||||||
|
unit: '',
|
||||||
|
group: '',
|
||||||
|
legend: '',
|
||||||
|
}),
|
||||||
|
validator: formDataValidator,
|
||||||
|
},
|
||||||
|
metricPersisted: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validateQueryPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const group = this.formData.group.length ? this.formData.group : queryTypes.business;
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryIsValid: null,
|
||||||
|
queryValidateInFlight: false,
|
||||||
|
...this.formData,
|
||||||
|
group,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formIsValid() {
|
||||||
|
return Boolean(
|
||||||
|
this.queryIsValid &&
|
||||||
|
this.title.length &&
|
||||||
|
this.yLabel.length &&
|
||||||
|
this.unit.length &&
|
||||||
|
this.group.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validQueryMsg() {
|
||||||
|
return this.queryIsValid ? s__('Metrics|PromQL query is valid') : '';
|
||||||
|
},
|
||||||
|
invalidQueryMsg() {
|
||||||
|
return !this.queryIsValid ? this.errorMessage : '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
formIsValid(value) {
|
||||||
|
this.$emit('formValidation', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
if (this.metricPersisted) {
|
||||||
|
this.validateQuery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
requestValidation(query, cancelToken) {
|
||||||
|
return backOffRequest(() =>
|
||||||
|
axios.post(
|
||||||
|
this.validateQueryPath,
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cancelToken,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setFormState(isValid, inFlight, message) {
|
||||||
|
this.queryIsValid = isValid;
|
||||||
|
this.queryValidateInFlight = inFlight;
|
||||||
|
this.errorMessage = message;
|
||||||
|
},
|
||||||
|
validateQuery() {
|
||||||
|
if (!this.query) {
|
||||||
|
this.setFormState(null, false, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setFormState(null, true, '');
|
||||||
|
// cancel previously dispatched backoff request
|
||||||
|
if (cancelTokenSource) {
|
||||||
|
cancelTokenSource.cancel();
|
||||||
|
}
|
||||||
|
// Creating a new token for each request because
|
||||||
|
// if a single token is used it can cancel existing requests
|
||||||
|
// as well.
|
||||||
|
cancelTokenSource = axiosCancelToken.source();
|
||||||
|
this.requestValidation(this.query, cancelTokenSource.token)
|
||||||
|
.then(res => {
|
||||||
|
const response = res.data;
|
||||||
|
const { valid, error } = response.query;
|
||||||
|
if (response.success) {
|
||||||
|
this.setFormState(valid, false, valid ? '' : error);
|
||||||
|
} else {
|
||||||
|
throw new Error(__('There was an error trying to validate your query'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setFormState(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
s__('Metrics|There was an error trying to validate your query'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
debouncedValidateQuery: debounce(function checkQuery() {
|
||||||
|
this.validateQuery();
|
||||||
|
}, 500),
|
||||||
|
},
|
||||||
|
csrfToken: csrf.token || '',
|
||||||
|
formGroupOptions: [
|
||||||
|
{ text: __('Business'), value: queryTypes.business },
|
||||||
|
{ text: __('Response'), value: queryTypes.response },
|
||||||
|
{ text: __('System'), value: queryTypes.system },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input ref="method" type="hidden" name="_method" :value="formOperation" />
|
||||||
|
<input :value="$options.csrfToken" type="hidden" name="authenticity_token" />
|
||||||
|
<gl-form-group :label="__('Name')" label-for="prometheus_metric_title" label-class="label-bold">
|
||||||
|
<gl-form-input
|
||||||
|
id="prometheus_metric_title"
|
||||||
|
v-model="title"
|
||||||
|
name="prometheus_metric[title]"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="s__('Metrics|e.g. Throughput')"
|
||||||
|
data-qa-selector="custom_metric_prometheus_title_field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="form-text text-muted">{{ s__('Metrics|Used as a title for the chart') }}</span>
|
||||||
|
</gl-form-group>
|
||||||
|
<gl-form-group :label="__('Type')" label-for="prometheus_metric_group" label-class="label-bold">
|
||||||
|
<gl-form-radio-group
|
||||||
|
id="metric-group"
|
||||||
|
v-model="group"
|
||||||
|
:options="$options.formGroupOptions"
|
||||||
|
:checked="group"
|
||||||
|
name="prometheus_metric[group]"
|
||||||
|
/>
|
||||||
|
<span class="form-text text-muted">{{ s__('Metrics|For grouping similar metrics') }}</span>
|
||||||
|
</gl-form-group>
|
||||||
|
<gl-form-group
|
||||||
|
:label="__('Query')"
|
||||||
|
label-for="prometheus_metric_query"
|
||||||
|
label-class="label-bold"
|
||||||
|
:state="queryIsValid"
|
||||||
|
>
|
||||||
|
<gl-form-input
|
||||||
|
id="prometheus_metric_query"
|
||||||
|
v-model.trim="query"
|
||||||
|
data-qa-selector="custom_metric_prometheus_query_field"
|
||||||
|
name="prometheus_metric[query]"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="s__('Metrics|e.g. rate(http_requests_total[5m])')"
|
||||||
|
required
|
||||||
|
:state="queryIsValid"
|
||||||
|
@input="debouncedValidateQuery"
|
||||||
|
/>
|
||||||
|
<span v-if="queryValidateInFlight" class="form-text text-muted">
|
||||||
|
<gl-loading-icon :inline="true" class="mr-1 align-middle" />
|
||||||
|
{{ s__('Metrics|Validating query') }}
|
||||||
|
</span>
|
||||||
|
<slot v-if="!queryValidateInFlight" name="valid-feedback">
|
||||||
|
<span class="form-text cgreen">
|
||||||
|
{{ validQueryMsg }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
<slot v-if="!queryValidateInFlight" name="invalid-feedback">
|
||||||
|
<span class="form-text cred">
|
||||||
|
{{ invalidQueryMsg }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
<span v-show="query.length === 0" class="form-text text-muted">
|
||||||
|
{{ s__('Metrics|Must be a valid PromQL query.') }}
|
||||||
|
<gl-link href="https://prometheus.io/docs/prometheus/latest/querying/basics/" tabindex="-1">
|
||||||
|
{{ s__('Metrics|Prometheus Query Documentation') }}
|
||||||
|
<icon name="external-link" :size="12" />
|
||||||
|
</gl-link>
|
||||||
|
</span>
|
||||||
|
</gl-form-group>
|
||||||
|
<gl-form-group
|
||||||
|
:label="s__('Metrics|Y-axis label')"
|
||||||
|
label-for="prometheus_metric_y_label"
|
||||||
|
label-class="label-bold"
|
||||||
|
>
|
||||||
|
<gl-form-input
|
||||||
|
id="prometheus_metric_y_label"
|
||||||
|
v-model="yLabel"
|
||||||
|
data-qa-selector="custom_metric_prometheus_y_label_field"
|
||||||
|
name="prometheus_metric[y_label]"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="s__('Metrics|e.g. Requests/second')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="form-text text-muted">
|
||||||
|
{{
|
||||||
|
s__('Metrics|Label of the y-axis (usually the unit). The x-axis always represents time.')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</gl-form-group>
|
||||||
|
<gl-form-group
|
||||||
|
:label="s__('Metrics|Unit label')"
|
||||||
|
label-for="prometheus_metric_unit"
|
||||||
|
label-class="label-bold"
|
||||||
|
>
|
||||||
|
<gl-form-input
|
||||||
|
id="prometheus_metric_unit"
|
||||||
|
v-model="unit"
|
||||||
|
data-qa-selector="custom_metric_prometheus_unit_label_field"
|
||||||
|
name="prometheus_metric[unit]"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="s__('Metrics|e.g. req/sec')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</gl-form-group>
|
||||||
|
<gl-form-group
|
||||||
|
:label="s__('Metrics|Legend label (optional)')"
|
||||||
|
label-for="prometheus_metric_legend"
|
||||||
|
label-class="label-bold"
|
||||||
|
>
|
||||||
|
<gl-form-input
|
||||||
|
id="prometheus_metric_legend"
|
||||||
|
v-model="legend"
|
||||||
|
data-qa-selector="custom_metric_prometheus_legend_label_field"
|
||||||
|
name="prometheus_metric[legend]"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="s__('Metrics|e.g. HTTP requests')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="form-text text-muted">
|
||||||
|
{{
|
||||||
|
s__(
|
||||||
|
'Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response.',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</gl-form-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
import { GlModal, GlModalDirective, GlDeprecatedButton } from '@gitlab/ui';
|
||||||
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlModal,
|
||||||
|
GlDeprecatedButton,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
'gl-modal': GlModalDirective,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
deleteMetricUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
csrfToken: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSubmit() {
|
||||||
|
this.$refs.form.submit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
descriptionText: s__(
|
||||||
|
`Metrics|You're about to permanently delete this metric. This cannot be undone.`,
|
||||||
|
),
|
||||||
|
modalId: 'delete-custom-metric-modal',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="d-inline-block float-right mr-3">
|
||||||
|
<gl-deprecated-button v-gl-modal="$options.modalId" variant="danger">
|
||||||
|
{{ __('Delete') }}
|
||||||
|
</gl-deprecated-button>
|
||||||
|
<gl-modal
|
||||||
|
:title="s__('Metrics|Delete metric?')"
|
||||||
|
:ok-title="s__('Metrics|Delete metric')"
|
||||||
|
:modal-id="$options.modalId"
|
||||||
|
ok-variant="danger"
|
||||||
|
@ok="onSubmit"
|
||||||
|
>
|
||||||
|
{{ $options.descriptionText }}
|
||||||
|
|
||||||
|
<form ref="form" :action="deleteMetricUrl" method="post">
|
||||||
|
<input type="hidden" name="_method" value="delete" />
|
||||||
|
<input :value="csrfToken" type="hidden" name="authenticity_token" />
|
||||||
|
</form>
|
||||||
|
</gl-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
12
app/assets/javascripts/custom_metrics/constants.js
Normal file
12
app/assets/javascripts/custom_metrics/constants.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export const queryTypes = {
|
||||||
|
business: 'business',
|
||||||
|
response: 'response',
|
||||||
|
system: 'system',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formDataValidator = val => {
|
||||||
|
const fieldNames = Object.keys(val);
|
||||||
|
const requiredFields = ['title', 'query', 'yLabel', 'unit', 'group', 'legend'];
|
||||||
|
|
||||||
|
return requiredFields.every(name => fieldNames.includes(name));
|
||||||
|
};
|
47
app/assets/javascripts/custom_metrics/index.js
Normal file
47
app/assets/javascripts/custom_metrics/index.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
|
import CustomMetricsForm from './components/custom_metrics_form.vue';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Vue({
|
||||||
|
el: '#js-custom-metrics',
|
||||||
|
components: {
|
||||||
|
CustomMetricsForm,
|
||||||
|
},
|
||||||
|
render(createElement) {
|
||||||
|
const domEl = document.querySelector(this.$options.el);
|
||||||
|
const {
|
||||||
|
customMetricsPath,
|
||||||
|
editProjectServicePath,
|
||||||
|
validateQueryPath,
|
||||||
|
title,
|
||||||
|
query,
|
||||||
|
yLabel,
|
||||||
|
unit,
|
||||||
|
group,
|
||||||
|
legend,
|
||||||
|
} = domEl.dataset;
|
||||||
|
let { metricPersisted } = domEl.dataset;
|
||||||
|
|
||||||
|
metricPersisted = parseBoolean(metricPersisted);
|
||||||
|
|
||||||
|
return createElement('custom-metrics-form', {
|
||||||
|
props: {
|
||||||
|
customMetricsPath,
|
||||||
|
metricPersisted,
|
||||||
|
editProjectServicePath,
|
||||||
|
validateQueryPath,
|
||||||
|
formData: {
|
||||||
|
title,
|
||||||
|
query,
|
||||||
|
yLabel,
|
||||||
|
unit,
|
||||||
|
group,
|
||||||
|
legend,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -23,7 +23,7 @@ function getErrorMessage(res) {
|
||||||
return res.message;
|
return res.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function dropzoneInput(form) {
|
export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
||||||
const divHover = '<div class="div-dropzone-hover"></div>';
|
const divHover = '<div class="div-dropzone-hover"></div>';
|
||||||
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
||||||
const $attachButton = form.find('.button-attach-file');
|
const $attachButton = form.find('.button-attach-file');
|
||||||
|
@ -69,6 +69,7 @@ export default function dropzoneInput(form) {
|
||||||
uploadMultiple: false,
|
uploadMultiple: false,
|
||||||
headers: csrf.headers,
|
headers: csrf.headers,
|
||||||
previewContainer: false,
|
previewContainer: false,
|
||||||
|
...config,
|
||||||
processing: () => $('.div-dropzone-alert').alert('close'),
|
processing: () => $('.div-dropzone-alert').alert('close'),
|
||||||
dragover: () => {
|
dragover: () => {
|
||||||
$mdArea.addClass('is-dropzone-hover');
|
$mdArea.addClass('is-dropzone-hover');
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class GLForm {
|
||||||
);
|
);
|
||||||
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
|
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
|
||||||
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
|
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
|
||||||
dropzoneInput(this.form);
|
dropzoneInput(this.form, { parallelUploads: 1 });
|
||||||
autosize(this.textarea);
|
autosize(this.textarea);
|
||||||
}
|
}
|
||||||
// form and textarea event listeners
|
// form and textarea event listeners
|
||||||
|
|
|
@ -109,3 +109,9 @@ export const initialStateKeys = [...endpointKeys, 'currentEnvironmentName'];
|
||||||
* Constant to indicate if a metric exists in the database
|
* Constant to indicate if a metric exists in the database
|
||||||
*/
|
*/
|
||||||
export const NOT_IN_DB_PREFIX = 'NO_DB';
|
export const NOT_IN_DB_PREFIX = 'NO_DB';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* graphQL environments API value for active environments.
|
||||||
|
* Used as a value for the 'states' query filter
|
||||||
|
*/
|
||||||
|
export const ENVIRONMENT_AVAILABLE_STATE = 'available';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
query getEnvironments($projectPath: ID!, $search: String) {
|
query getEnvironments($projectPath: ID!, $search: String, $states: [String!]) {
|
||||||
project(fullPath: $projectPath) {
|
project(fullPath: $projectPath) {
|
||||||
data: environments(search: $search) {
|
data: environments(search: $search, states: $states) {
|
||||||
environments: nodes {
|
environments: nodes {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
|
|
|
@ -10,7 +10,7 @@ import statusCodes from '../../lib/utils/http_status';
|
||||||
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
|
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
|
||||||
import { s__, sprintf } from '../../locale';
|
import { s__, sprintf } from '../../locale';
|
||||||
|
|
||||||
import { PROMETHEUS_TIMEOUT } from '../constants';
|
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
|
||||||
|
|
||||||
function prometheusMetricQueryParams(timeRange) {
|
function prometheusMetricQueryParams(timeRange) {
|
||||||
const { start, end } = convertToFixedRange(timeRange);
|
const { start, end } = convertToFixedRange(timeRange);
|
||||||
|
@ -238,6 +238,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
|
||||||
variables: {
|
variables: {
|
||||||
projectPath: removeLeadingSlash(state.projectPath),
|
projectPath: removeLeadingSlash(state.projectPath),
|
||||||
search: state.environmentsSearchTerm,
|
search: state.environmentsSearchTerm,
|
||||||
|
states: [ENVIRONMENT_AVAILABLE_STATE],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(resp =>
|
.then(resp =>
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script>
|
||||||
|
import { GlLink } from '@gitlab/ui';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AccessibilityIssueBody',
|
||||||
|
components: {
|
||||||
|
GlLink,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
issue: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isNew: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
parsedTECHSCode() {
|
||||||
|
/*
|
||||||
|
* In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
|
||||||
|
* or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
|
||||||
|
*
|
||||||
|
* The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
|
||||||
|
* Here we simply split the string on `.` and get the code in the 5th position
|
||||||
|
*/
|
||||||
|
if (this.issue.code === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.issue.code.split('.')[4] || null;
|
||||||
|
},
|
||||||
|
learnMoreUrl() {
|
||||||
|
if (this.parsedTECHSCode === null) {
|
||||||
|
return 'https://www.w3.org/TR/WCAG20-TECHS/Overview.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode}.html`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
|
||||||
|
<div ref="accessibility-issue-description" class="report-block-list-issue-description-text">
|
||||||
|
<div
|
||||||
|
v-if="isNew"
|
||||||
|
ref="accessibility-issue-is-new-badge"
|
||||||
|
class="badge badge-danger append-right-5"
|
||||||
|
>
|
||||||
|
{{ s__('AccessibilityReport|New') }}
|
||||||
|
</div>
|
||||||
|
{{ issue.name }}
|
||||||
|
<gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
|
||||||
|
s__('AccessibilityReport|Learn More')
|
||||||
|
}}</gl-link>
|
||||||
|
{{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,9 +1,12 @@
|
||||||
import TestIssueBody from './test_issue_body.vue';
|
import TestIssueBody from './test_issue_body.vue';
|
||||||
|
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
|
||||||
|
|
||||||
export const components = {
|
export const components = {
|
||||||
|
AccessibilityIssueBody,
|
||||||
TestIssueBody,
|
TestIssueBody,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const componentNames = {
|
export const componentNames = {
|
||||||
|
AccessibilityIssueBody: AccessibilityIssueBody.name,
|
||||||
TestIssueBody: TestIssueBody.name,
|
TestIssueBody: TestIssueBody.name,
|
||||||
};
|
};
|
||||||
|
|
10
app/assets/stylesheets/bootstrap_migration.scss
vendored
10
app/assets/stylesheets/bootstrap_migration.scss
vendored
|
@ -41,16 +41,6 @@ html [type="button"],
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
color: $gl-text-color;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
.h1,
|
.h1,
|
||||||
h2,
|
h2,
|
||||||
|
|
|
@ -567,16 +567,6 @@ body {
|
||||||
font-weight: $gl-font-weight-bold;
|
font-weight: $gl-font-weight-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
color: $gl-text-color;
|
|
||||||
font-weight: $gl-font-weight-bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-header {
|
.light-header {
|
||||||
font-weight: $gl-font-weight-bold;
|
font-weight: $gl-font-weight-bold;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ $h3-font-size: 14px * 1.75;
|
||||||
$h4-font-size: 14px * 1.5;
|
$h4-font-size: 14px * 1.5;
|
||||||
$h5-font-size: 14px * 1.25;
|
$h5-font-size: 14px * 1.25;
|
||||||
$h6-font-size: 14px;
|
$h6-font-size: 14px;
|
||||||
|
$headings-color: $gl-text-color;
|
||||||
|
$headings-font-weight: $gl-font-weight-bold;
|
||||||
$spacer: $grid-size;
|
$spacer: $grid-size;
|
||||||
$spacers: (
|
$spacers: (
|
||||||
0: 0,
|
0: 0,
|
||||||
|
|
|
@ -10,7 +10,7 @@ module GroupsHelper
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_nav_link_paths
|
def group_settings_nav_link_paths
|
||||||
%w[
|
%w[
|
||||||
groups#projects
|
groups#projects
|
||||||
groups#edit
|
groups#edit
|
||||||
|
|
|
@ -14,4 +14,12 @@ class ImportExportUploader < AttachmentUploader
|
||||||
def move_to_cache
|
def move_to_cache
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def work_dir
|
||||||
|
File.join(Settings.shared['path'], 'tmp', 'work')
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_dir
|
||||||
|
File.join(Settings.shared['path'], 'tmp', 'cache')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
= _('Members')
|
= _('Members')
|
||||||
|
|
||||||
- if group_sidebar_link?(:settings)
|
- if group_sidebar_link?(:settings)
|
||||||
= nav_link(path: group_nav_link_paths) do
|
= nav_link(path: group_settings_nav_link_paths) do
|
||||||
= link_to edit_group_path(@group) do
|
= link_to edit_group_path(@group) do
|
||||||
.nav-icon-container
|
.nav-icon-container
|
||||||
= sprite_icon('settings')
|
= sprite_icon('settings')
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Show only active environments in monitoring dropdown
|
||||||
|
merge_request: 28456
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Align color and font-weight styles of heading elements and their typography
|
||||||
|
classes
|
||||||
|
merge_request: 28422
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -17,7 +17,7 @@ how GitLab can be scaled out and made highly available. These examples progress
|
||||||
from simple to complex as scaling or highly-available components are added.
|
from simple to complex as scaling or highly-available components are added.
|
||||||
|
|
||||||
For larger setups serving 2,000 or more users, we provide
|
For larger setups serving 2,000 or more users, we provide
|
||||||
[reference architectures](#reference-architectures) based on GitLab's
|
[reference architectures](../scaling/index.md#reference-architectures) based on GitLab's
|
||||||
experience with GitLab.com and internal scale testing that aim to achieve the
|
experience with GitLab.com and internal scale testing that aim to achieve the
|
||||||
right balance of scalability and availability.
|
right balance of scalability and availability.
|
||||||
|
|
||||||
|
@ -93,147 +93,6 @@ them.
|
||||||
|
|
||||||
In some cases, components can be combined on the same nodes to reduce complexity as well.
|
In some cases, components can be combined on the same nodes to reduce complexity as well.
|
||||||
|
|
||||||
## Recommended setups based on number of users
|
|
||||||
|
|
||||||
- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [requirements page](../../install/requirements.md) for further details of the specs you will require.
|
|
||||||
- 1000 - 10000 Users: A scaled environment based on one of our [Reference Architectures](#reference-architectures), without the HA components applied. This can be a reasonable step towards a fully HA environment.
|
|
||||||
- 2000 - 50000+ Users: A scaled HA environment based on one of our [Reference Architectures](#reference-architectures) below.
|
|
||||||
|
|
||||||
## Reference architectures
|
|
||||||
|
|
||||||
In this section we'll detail the Reference Architectures that can support large numbers
|
|
||||||
of users. These were built, tested and verified by our Quality and Support teams.
|
|
||||||
|
|
||||||
Testing was done with our GitLab Performance Tool at specific coded workloads, and the
|
|
||||||
throughputs used for testing were calculated based on sample customer data. We
|
|
||||||
test each endpoint type with the following number of requests per second (RPS)
|
|
||||||
per 1000 users:
|
|
||||||
|
|
||||||
- API: 20 RPS
|
|
||||||
- Web: 2 RPS
|
|
||||||
- Git: 2 RPS
|
|
||||||
|
|
||||||
NOTE: **Note:** Note that depending on your workflow the below recommended
|
|
||||||
reference architectures may need to be adapted accordingly. Your workload
|
|
||||||
is influenced by factors such as - but not limited to - how active your users are,
|
|
||||||
how much automation you use, mirroring, and repo/change size. Additionally the
|
|
||||||
shown memory values are given directly by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
|
|
||||||
On different cloud vendors a best effort like for like can be used.
|
|
||||||
|
|
||||||
### 2,000 user configuration
|
|
||||||
|
|
||||||
- **Supported users (approximate):** 2,000
|
|
||||||
- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
|
|
||||||
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
|
||||||
|
|
||||||
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
|
||||||
| ----------------------------|-------|-----------------------|---------------|--------------|
|
|
||||||
| GitLab Rails[^1] | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
|
|
||||||
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
|
||||||
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Gitaly[^2] [^5] [^7] | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
|
||||||
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
|
||||||
| Cloud Object Storage[^4] | - | - | - | - |
|
|
||||||
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
|
|
||||||
### 5,000 user configuration
|
|
||||||
|
|
||||||
- **Supported users (approximate):** 5,000
|
|
||||||
- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
|
|
||||||
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
|
||||||
|
|
||||||
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
|
||||||
| ----------------------------|-------|------------------------|---------------|--------------|
|
|
||||||
| GitLab Rails[^1] | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge |
|
|
||||||
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
|
||||||
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Gitaly[^2] [^5] [^7] | X | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
|
|
||||||
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
|
||||||
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
|
||||||
| Cloud Object Storage[^4] | - | - | - | - |
|
|
||||||
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
|
|
||||||
### 10,000 user configuration
|
|
||||||
|
|
||||||
- **Supported users (approximate):** 10,000
|
|
||||||
- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
|
|
||||||
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
|
||||||
|
|
||||||
| Service | Nodes | GCP Configuration[^8] | GCP type | AWS type[^9] |
|
|
||||||
| ----------------------------|-------|------------------------|----------------|--------------|
|
|
||||||
| GitLab Rails[^1] | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
|
|
||||||
| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Gitaly[^2] [^5] [^7] | X | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
|
|
||||||
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
|
||||||
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
|
||||||
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Cloud Object Storage[^4] | - | - | - | - |
|
|
||||||
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
|
|
||||||
### 25,000 user configuration
|
|
||||||
|
|
||||||
- **Supported users (approximate):** 25,000
|
|
||||||
- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
|
|
||||||
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
|
||||||
|
|
||||||
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
|
||||||
| ----------------------------|-------|------------------------|----------------|--------------|
|
|
||||||
| GitLab Rails[^1] | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
|
|
||||||
| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
|
|
||||||
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Gitaly[^2] [^5] [^7] | X | 32 vCPU, 120GB Memory | n1-standard-32 | m5.8xlarge |
|
|
||||||
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
|
||||||
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
|
||||||
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Cloud Object Storage[^4] | - | - | - | - |
|
|
||||||
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Internal load balancing node[^6] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
|
|
||||||
### 50,000 user configuration
|
|
||||||
|
|
||||||
- **Supported users (approximate):** 50,000
|
|
||||||
- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
|
|
||||||
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
|
||||||
|
|
||||||
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
|
||||||
| ----------------------------|-------|------------------------|----------------|--------------|
|
|
||||||
| GitLab Rails[^1] | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
|
|
||||||
| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
|
|
||||||
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Gitaly[^2] [^5] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 | m5.16xlarge |
|
|
||||||
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
|
||||||
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
|
||||||
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
|
||||||
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| Cloud Object Storage[^4] | - | - | - | - |
|
|
||||||
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
|
||||||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
|
||||||
| Internal load balancing node[^6] | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
|
|
||||||
|
|
||||||
[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
|
[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
|
||||||
and have its number of workers set to 90% of available CPUs along with 4 threads.
|
and have its number of workers set to 90% of available CPUs along with 4 threads.
|
||||||
|
|
||||||
|
|
|
@ -71,3 +71,190 @@ References:
|
||||||
- [Configure your NFS server to work with GitLab](../high_availability/nfs.md)
|
- [Configure your NFS server to work with GitLab](../high_availability/nfs.md)
|
||||||
- [Configure packaged PostgreSQL server to listen on TCP/IP](https://docs.gitlab.com/omnibus/settings/database.html#configure-packaged-postgresql-server-to-listen-on-tcpip)
|
- [Configure packaged PostgreSQL server to listen on TCP/IP](https://docs.gitlab.com/omnibus/settings/database.html#configure-packaged-postgresql-server-to-listen-on-tcpip)
|
||||||
- [Setting up a Redis-only server](https://docs.gitlab.com/omnibus/settings/redis.html#setting-up-a-redis-only-server)
|
- [Setting up a Redis-only server](https://docs.gitlab.com/omnibus/settings/redis.html#setting-up-a-redis-only-server)
|
||||||
|
|
||||||
|
## Recommended setups based on number of users
|
||||||
|
|
||||||
|
- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [requirements page](../../install/requirements.md) for further details of the specs you will require.
|
||||||
|
- 1000 - 10000 Users: A scaled environment based on one of our [Reference Architectures](#reference-architectures), without the HA components applied. This can be a reasonable step towards a fully HA environment.
|
||||||
|
- 2000 - 50000+ Users: A scaled HA environment based on one of our [Reference Architectures](#reference-architectures) below.
|
||||||
|
|
||||||
|
## Reference architectures
|
||||||
|
|
||||||
|
In this section we'll detail the Reference Architectures that can support large numbers
|
||||||
|
of users. These were built, tested and verified by our Quality and Support teams.
|
||||||
|
|
||||||
|
Testing was done with our GitLab Performance Tool at specific coded workloads, and the
|
||||||
|
throughputs used for testing were calculated based on sample customer data. We
|
||||||
|
test each endpoint type with the following number of requests per second (RPS)
|
||||||
|
per 1000 users:
|
||||||
|
|
||||||
|
- API: 20 RPS
|
||||||
|
- Web: 2 RPS
|
||||||
|
- Git: 2 RPS
|
||||||
|
|
||||||
|
NOTE: **Note:** Note that depending on your workflow the below recommended
|
||||||
|
reference architectures may need to be adapted accordingly. Your workload
|
||||||
|
is influenced by factors such as - but not limited to - how active your users are,
|
||||||
|
how much automation you use, mirroring, and repo/change size. Additionally the
|
||||||
|
shown memory values are given directly by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
|
||||||
|
On different cloud vendors a best effort like for like can be used.
|
||||||
|
|
||||||
|
### 2,000 user configuration
|
||||||
|
|
||||||
|
- **Supported users (approximate):** 2,000
|
||||||
|
- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
|
||||||
|
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
||||||
|
|
||||||
|
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
||||||
|
| ----------------------------|-------|-----------------------|---------------|--------------|
|
||||||
|
| GitLab Rails[^1] | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
|
||||||
|
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
||||||
|
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Gitaly[^2] [^5] [^7] | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
||||||
|
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
||||||
|
| Cloud Object Storage[^4] | - | - | - | - |
|
||||||
|
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
|
||||||
|
### 5,000 user configuration
|
||||||
|
|
||||||
|
- **Supported users (approximate):** 5,000
|
||||||
|
- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
|
||||||
|
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
||||||
|
|
||||||
|
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
||||||
|
| ----------------------------|-------|------------------------|---------------|--------------|
|
||||||
|
| GitLab Rails[^1] | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge |
|
||||||
|
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
||||||
|
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Gitaly[^2] [^5] [^7] | X | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
|
||||||
|
| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
||||||
|
| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
|
||||||
|
| Cloud Object Storage[^4] | - | - | - | - |
|
||||||
|
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
|
||||||
|
### 10,000 user configuration
|
||||||
|
|
||||||
|
- **Supported users (approximate):** 10,000
|
||||||
|
- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
|
||||||
|
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
||||||
|
|
||||||
|
| Service | Nodes | GCP Configuration[^8] | GCP type | AWS type[^9] |
|
||||||
|
| ----------------------------|-------|------------------------|----------------|--------------|
|
||||||
|
| GitLab Rails[^1] | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
|
||||||
|
| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Gitaly[^2] [^5] [^7] | X | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
|
||||||
|
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
||||||
|
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
||||||
|
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Cloud Object Storage[^4] | - | - | - | - |
|
||||||
|
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
|
||||||
|
### 25,000 user configuration
|
||||||
|
|
||||||
|
- **Supported users (approximate):** 25,000
|
||||||
|
- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
|
||||||
|
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
||||||
|
|
||||||
|
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
||||||
|
| ----------------------------|-------|------------------------|----------------|--------------|
|
||||||
|
| GitLab Rails[^1] | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
|
||||||
|
| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
|
||||||
|
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Gitaly[^2] [^5] [^7] | X | 32 vCPU, 120GB Memory | n1-standard-32 | m5.8xlarge |
|
||||||
|
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
||||||
|
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
||||||
|
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Cloud Object Storage[^4] | - | - | - | - |
|
||||||
|
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Internal load balancing node[^6] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
|
||||||
|
### 50,000 user configuration
|
||||||
|
|
||||||
|
- **Supported users (approximate):** 50,000
|
||||||
|
- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
|
||||||
|
- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
|
||||||
|
|
||||||
|
| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
|
||||||
|
| ----------------------------|-------|------------------------|----------------|--------------|
|
||||||
|
| GitLab Rails[^1] | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
|
||||||
|
| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
|
||||||
|
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Gitaly[^2] [^5] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 | m5.16xlarge |
|
||||||
|
| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
||||||
|
| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
|
||||||
|
| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||||
|
| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| Cloud Object Storage[^4] | - | - | - | - |
|
||||||
|
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||||
|
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||||
|
| Internal load balancing node[^6] | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
|
||||||
|
|
||||||
|
[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
|
||||||
|
and have its number of workers set to 90% of available CPUs along with 4 threads.
|
||||||
|
|
||||||
|
[^2]: Gitaly node requirements are dependent on customer data, specifically the number of
|
||||||
|
projects and their sizes. We recommend 2 nodes as an absolute minimum for HA environments
|
||||||
|
and at least 4 nodes should be used when supporting 50,000 or more users.
|
||||||
|
We also recommend that each Gitaly node should store no more than 5TB of data
|
||||||
|
and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
|
||||||
|
set to 20% of available CPUs. Additional nodes should be considered in conjunction
|
||||||
|
with a review of expected data size and spread based on the recommendations above.
|
||||||
|
|
||||||
|
[^3]: Recommended Redis setup differs depending on the size of the architecture.
|
||||||
|
For smaller architectures (up to 5,000 users) we suggest one Redis cluster for all
|
||||||
|
classes and that Redis Sentinel is hosted alongside Consul.
|
||||||
|
For larger architectures (10,000 users or more) we suggest running a separate
|
||||||
|
[Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
|
||||||
|
and another for the Queues and Shared State classes respectively. We also recommend
|
||||||
|
that you run the Redis Sentinel clusters separately as well for each Redis Cluster.
|
||||||
|
|
||||||
|
[^4]: For data objects such as LFS, Uploads, Artifacts, etc. We recommend a [Cloud Object Storage service](../object_storage.md)
|
||||||
|
over NFS where possible, due to better performance and availability.
|
||||||
|
|
||||||
|
[^5]: NFS can be used as an alternative for both repository data (replacing Gitaly) and
|
||||||
|
object storage but this isn't typically recommended for performance reasons. Note however it is required for
|
||||||
|
[GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
|
||||||
|
|
||||||
|
[^6]: Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
|
||||||
|
as the load balancer. However other reputable load balancers with similar feature sets
|
||||||
|
should also work instead but be aware these aren't validated.
|
||||||
|
|
||||||
|
[^7]: We strongly recommend that any Gitaly and / or NFS nodes are set up with SSD disks over
|
||||||
|
HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
|
||||||
|
as these components have heavy I/O. These IOPS values are recommended only as a starter
|
||||||
|
as with time they may be adjusted higher or lower depending on the scale of your
|
||||||
|
environment's workload. If you're running the environment on a Cloud provider
|
||||||
|
you may need to refer to their documentation on how configure IOPS correctly.
|
||||||
|
|
||||||
|
[^8]: The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
|
||||||
|
CPU platform on GCP. On different hardware you may find that adjustments, either lower
|
||||||
|
or higher, are required for your CPU or Node counts accordingly. For more information, a
|
||||||
|
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
|
||||||
|
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||||
|
|
||||||
|
[^9]: AWS-equivalent configurations are rough suggestions and may change in the
|
||||||
|
future. They have not yet been tested and validated.
|
||||||
|
|
|
@ -715,8 +715,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
|
||||||
++ CI_SERVER_VERSION_PATCH=0
|
++ CI_SERVER_VERSION_PATCH=0
|
||||||
++ export CI_SERVER_REVISION=f4cc00ae823
|
++ export CI_SERVER_REVISION=f4cc00ae823
|
||||||
++ CI_SERVER_REVISION=f4cc00ae823
|
++ CI_SERVER_REVISION=f4cc00ae823
|
||||||
++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,export_issues,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
|
++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
|
||||||
++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,export_issues,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
|
++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
|
||||||
++ export CI_PROJECT_ID=17893
|
++ export CI_PROJECT_ID=17893
|
||||||
++ CI_PROJECT_ID=17893
|
++ CI_PROJECT_ID=17893
|
||||||
++ export CI_PROJECT_NAME=ci-debug-trace
|
++ export CI_PROJECT_NAME=ci-debug-trace
|
||||||
|
|
|
@ -173,7 +173,7 @@ To see the status of your GitLab.com subscription, log into GitLab.com and go to
|
||||||
1. Go to **User Avatar > Settings**.
|
1. Go to **User Avatar > Settings**.
|
||||||
1. Click **Billing**.
|
1. Click **Billing**.
|
||||||
- For groups:
|
- For groups:
|
||||||
1. From the group page (*not* from a project within the group), go to **Administration > Billing**.
|
1. From the group page (*not* from a project within the group), go to **Settings > Billing**.
|
||||||
|
|
||||||
The following table describes details of your subscription for groups:
|
The following table describes details of your subscription for groups:
|
||||||
|
|
||||||
|
@ -430,7 +430,7 @@ CI pipeline minutes are the execution time for your [pipelines](../ci/pipelines/
|
||||||
|
|
||||||
Quotas apply to:
|
Quotas apply to:
|
||||||
|
|
||||||
- Groups, where the minutes are shared across all members of the group, its subgroups, and nested projects. To view the group's usage, navigate to the group, then **{settings}** **Administration > Usage Quotas**.
|
- Groups, where the minutes are shared across all members of the group, its subgroups, and nested projects. To view the group's usage, navigate to the group, then **{settings}** **Settings > Usage Quotas**.
|
||||||
- Your personal account, where the minutes are available for your personal projects. To view and buy personal minutes, click your avatar, then **{settings}** **Settings > Pipeline quota**.
|
- Your personal account, where the minutes are available for your personal projects. To view and buy personal minutes, click your avatar, then **{settings}** **Settings > Pipeline quota**.
|
||||||
|
|
||||||
Only pipeline minutes for GitLab shared runners are restricted. If you have a specific runner set up for your projects, there is no limit to your build time on GitLab.com.
|
Only pipeline minutes for GitLab shared runners are restricted. If you have a specific runner set up for your projects, there is no limit to your build time on GitLab.com.
|
||||||
|
@ -451,10 +451,10 @@ main quota. Additional minutes:
|
||||||
|
|
||||||
To purchase additional minutes for your group on GitLab.com:
|
To purchase additional minutes for your group on GitLab.com:
|
||||||
|
|
||||||
1. From your group, go to **{settings}** **Administration > Usage Quotas**.
|
1. From your group, go to **{settings}** **Settings > Usage Quotas**.
|
||||||
1. Locate the subscription card that's linked to your group on GitLab.com, click **Buy more CI minutes**, and complete the details about the transaction.
|
1. Locate the subscription card that's linked to your group on GitLab.com, click **Buy more CI minutes**, and complete the details about the transaction.
|
||||||
1. Once we have processed your payment, the extra CI minutes will be synced to your group.
|
1. Once we have processed your payment, the extra CI minutes will be synced to your group.
|
||||||
1. To confirm the available CI minutes, go to your group, then **{settings}** **Administration > Usage Quotas**.
|
1. To confirm the available CI minutes, go to your group, then **{settings}** **Settings > Usage Quotas**.
|
||||||
The **Additional minutes** displayed now includes the purchased additional CI minutes, plus any minutes rolled over from last month.
|
The **Additional minutes** displayed now includes the purchased additional CI minutes, plus any minutes rolled over from last month.
|
||||||
|
|
||||||
To purchase additional minutes for your personal namespace:
|
To purchase additional minutes for your personal namespace:
|
||||||
|
|
|
@ -121,6 +121,8 @@ License Compliance can be configured using environment variables.
|
||||||
| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
|
| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
|
||||||
| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
|
| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
|
||||||
| `SETUP_CMD` | no | Custom setup for the dependency installation. (experimental) |
|
| `SETUP_CMD` | no | Custom setup for the dependency installation. (experimental) |
|
||||||
|
| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
|
||||||
|
| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Python projects). |
|
||||||
|
|
||||||
### Installing custom dependencies
|
### Installing custom dependencies
|
||||||
|
|
||||||
|
@ -215,6 +217,37 @@ license_scanning:
|
||||||
LM_PYTHON_VERSION: 2
|
LM_PYTHON_VERSION: 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom root certificates for Python
|
||||||
|
|
||||||
|
You can supply a custom root certificate to complete TLS verification by using the
|
||||||
|
`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
|
||||||
|
|
||||||
|
To bypass TLS verification, you can use a custom [`pip.conf`](https://pip.pypa.io/en/stable/user_guide/#config-file)
|
||||||
|
file to configure trusted hosts.
|
||||||
|
|
||||||
|
The following `gitlab-ci.yml` file uses a [`before_script`](../../../ci/yaml/README.md#before_script-and-after_script)
|
||||||
|
to inject a custom [`pip.conf`](https://pip.pypa.io/en/stable/user_guide/#config-file):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
include:
|
||||||
|
- template: License-Scanning.gitlab-ci.yml
|
||||||
|
|
||||||
|
license_scanning:
|
||||||
|
variables:
|
||||||
|
PIP_INDEX_URL: 'https://pypi.example.com/simple/'
|
||||||
|
before_script:
|
||||||
|
- mkdir -p ~/.config/pip/
|
||||||
|
- cp pip.conf ~/.config/pip/pip.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
The [`pip.conf`](https://pip.pypa.io/en/stable/reference/pip/) allows you to specify a list of
|
||||||
|
[trusted hosts](https://pip.pypa.io/en/stable/reference/pip/#cmdoption-trusted-host):
|
||||||
|
|
||||||
|
```text
|
||||||
|
[global]
|
||||||
|
trusted-host = pypi.example.com
|
||||||
|
```
|
||||||
|
|
||||||
### Migration from `license_management` to `license_scanning`
|
### Migration from `license_management` to `license_scanning`
|
||||||
|
|
||||||
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.
|
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.
|
||||||
|
|
|
@ -24,7 +24,7 @@ Note the following:
|
||||||
|
|
||||||
## Configuring your Identity Provider
|
## Configuring your Identity Provider
|
||||||
|
|
||||||
1. Navigate to the group and click **Administration > SAML SSO**.
|
1. Navigate to the group and click **Settings > SAML SSO**.
|
||||||
1. Configure your SAML server using the **Assertion consumer service URL** and **Identifier**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [your identity provider's documentation](#providers) for more details.
|
1. Configure your SAML server using the **Assertion consumer service URL** and **Identifier**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [your identity provider's documentation](#providers) for more details.
|
||||||
1. Configure the SAML response to include a NameID that uniquely identifies each user.
|
1. Configure the SAML response to include a NameID that uniquely identifies each user.
|
||||||
1. Configure required assertions using the [table below](#assertions).
|
1. Configure required assertions using the [table below](#assertions).
|
||||||
|
@ -116,7 +116,7 @@ This feature is similar to the [Credentials inventory for self-managed instances
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34648) in GitLab 12.9.
|
||||||
|
|
||||||
Groups with group-managed accounts can disallow forking of projects to destinations outside the group.
|
Groups with group-managed accounts can disallow forking of projects to destinations outside the group.
|
||||||
To do so, enable the "Prohibit outer forks" option in **Administration > SAML SSO**.
|
To do so, enable the "Prohibit outer forks" option in **Settings > SAML SSO**.
|
||||||
When enabled, projects within the group can only be forked to other destinations within the group (including its subgroups).
|
When enabled, projects within the group can only be forked to other destinations within the group (including its subgroups).
|
||||||
|
|
||||||
##### Other restrictions for Group-managed accounts
|
##### Other restrictions for Group-managed accounts
|
||||||
|
@ -146,7 +146,7 @@ assertions to be able to create a user.
|
||||||
|
|
||||||
GitLab provides metadata XML that can be used to configure your Identity Provider.
|
GitLab provides metadata XML that can be used to configure your Identity Provider.
|
||||||
|
|
||||||
1. Navigate to the group and click **Administration > SAML SSO**.
|
1. Navigate to the group and click **Settings > SAML SSO**.
|
||||||
1. Copy the provided **GitLab metadata URL**.
|
1. Copy the provided **GitLab metadata URL**.
|
||||||
1. Follow your Identity Provider's documentation and paste the metadata URL when it is requested.
|
1. Follow your Identity Provider's documentation and paste the metadata URL when it is requested.
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ GitLab provides metadata XML that can be used to configure your Identity Provide
|
||||||
|
|
||||||
Once you've set up your identity provider to work with GitLab, you'll need to configure GitLab to use it for authentication:
|
Once you've set up your identity provider to work with GitLab, you'll need to configure GitLab to use it for authentication:
|
||||||
|
|
||||||
1. Navigate to the group's **Administration > SAML SSO**.
|
1. Navigate to the group's **Settings > SAML SSO**.
|
||||||
1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign on URL** field.
|
1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign on URL** field.
|
||||||
1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field.
|
1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field.
|
||||||
1. Click the **Enable SAML authentication for this group** toggle switch.
|
1. Click the **Enable SAML authentication for this group** toggle switch.
|
||||||
|
@ -288,14 +288,14 @@ If the information you need isn't listed above you may wish to check our [troubl
|
||||||
To link SAML to your existing GitLab.com account:
|
To link SAML to your existing GitLab.com account:
|
||||||
|
|
||||||
1. Sign in to your GitLab.com account.
|
1. Sign in to your GitLab.com account.
|
||||||
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on the group's **Administration > SAML SSO** page.
|
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on the group's **Settings > SAML SSO** page.
|
||||||
1. Visit the SSO URL and click **Authorize**.
|
1. Visit the SSO URL and click **Authorize**.
|
||||||
1. Enter your credentials on the Identity Provider if prompted.
|
1. Enter your credentials on the Identity Provider if prompted.
|
||||||
1. You will be redirected back to GitLab.com and should now have access to the group. In the future, you can use SAML to sign in to GitLab.com.
|
1. You will be redirected back to GitLab.com and should now have access to the group. In the future, you can use SAML to sign in to GitLab.com.
|
||||||
|
|
||||||
## Signing in to GitLab.com with SAML
|
## Signing in to GitLab.com with SAML
|
||||||
|
|
||||||
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on a group's **Administration > SAML SSO** page. If configured, it might also be possible to sign in to GitLab starting from your Identity Provider.
|
1. Locate the SSO URL for the group you are signing in to. A group Admin can find this on a group's **Settings > SAML SSO** page. If configured, it might also be possible to sign in to GitLab starting from your Identity Provider.
|
||||||
1. Visit the SSO URL and click the **Sign in with Single Sign-On** button.
|
1. Visit the SSO URL and click the **Sign in with Single Sign-On** button.
|
||||||
1. Enter your credentials on the Identity Provider if prompted.
|
1. Enter your credentials on the Identity Provider if prompted.
|
||||||
1. You will be signed in to GitLab.com and redirected to the group.
|
1. You will be signed in to GitLab.com and redirected to the group.
|
||||||
|
|
|
@ -30,7 +30,7 @@ The following identity providers are supported:
|
||||||
|
|
||||||
Once [Single sign-on](index.md) has been configured, we can:
|
Once [Single sign-on](index.md) has been configured, we can:
|
||||||
|
|
||||||
1. Navigate to the group and click **Administration > SAML SSO**.
|
1. Navigate to the group and click **Settings > SAML SSO**.
|
||||||
1. Click on the **Generate a SCIM token** button.
|
1. Click on the **Generate a SCIM token** button.
|
||||||
1. Save the token and URL so they can be used in the next step.
|
1. Save the token and URL so they can be used in the next step.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Export Issues to CSV **(STARTER)**
|
# Export Issues to CSV
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/releases/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/releases/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
|
||||||
|
|
||||||
|
|
|
@ -197,7 +197,7 @@ Feature.disable(:save_issuable_health_status)
|
||||||
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
|
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
|
||||||
in order to change their status, assignee, milestone, or labels in bulk.
|
in order to change their status, assignee, milestone, or labels in bulk.
|
||||||
- [Import issues](csv_import.md)
|
- [Import issues](csv_import.md)
|
||||||
- [Export issues](csv_export.md) **(STARTER)**
|
- [Export issues](csv_export.md)
|
||||||
- [Issues API](../../../api/issues.md)
|
- [Issues API](../../../api/issues.md)
|
||||||
- Configure an [external issue tracker](../../../integration/external-issue-tracker.md)
|
- Configure an [external issue tracker](../../../integration/external-issue-tracker.md)
|
||||||
such as Jira, Redmine, or Bugzilla.
|
such as Jira, Redmine, or Bugzilla.
|
||||||
|
|
|
@ -205,16 +205,6 @@ module API
|
||||||
|
|
||||||
post '/notify_post_receive' do
|
post '/notify_post_receive' do
|
||||||
status 200
|
status 200
|
||||||
|
|
||||||
# TODO: Re-enable when Gitaly is processing the post-receive notification
|
|
||||||
# return unless Gitlab::GitalyClient.enabled?
|
|
||||||
#
|
|
||||||
# begin
|
|
||||||
# repository = wiki? ? project.wiki.repository : project.repository
|
|
||||||
# Gitlab::GitalyClient::NotificationService.new(repository.raw_repository).post_receive
|
|
||||||
# rescue GRPC::Unavailable => e
|
|
||||||
# render_api_error!(e, 500)
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
post '/post_receive' do
|
post '/post_receive' do
|
||||||
|
|
|
@ -1024,6 +1024,15 @@ msgstr ""
|
||||||
msgid "AccessTokens|reset it"
|
msgid "AccessTokens|reset it"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AccessibilityReport|Learn More"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AccessibilityReport|Message: %{message}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AccessibilityReport|New"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Account"
|
msgid "Account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,334 @@
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
||||||
|
const { CancelToken } = axios;
|
||||||
|
|
||||||
|
describe('custom metrics form fields component', () => {
|
||||||
|
let component;
|
||||||
|
let mockAxios;
|
||||||
|
|
||||||
|
const getNamedInput = name => component.element.querySelector(`input[name="${name}"]`);
|
||||||
|
const validateQueryPath = `${TEST_HOST}/mock/path`;
|
||||||
|
const validQueryResponse = { data: { success: true, query: { valid: true, error: '' } } };
|
||||||
|
const csrfToken = 'mockToken';
|
||||||
|
const formOperation = 'post';
|
||||||
|
const debouncedValidateQueryMock = jest.fn();
|
||||||
|
const makeFormData = (data = {}) => ({
|
||||||
|
formData: {
|
||||||
|
title: '',
|
||||||
|
yLabel: '',
|
||||||
|
query: '',
|
||||||
|
unit: '',
|
||||||
|
group: '',
|
||||||
|
legend: '',
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mountComponent = (props, methods = {}) => {
|
||||||
|
component = mount(CustomMetricsFormFields, {
|
||||||
|
propsData: {
|
||||||
|
formOperation,
|
||||||
|
validateQueryPath,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
csrfToken,
|
||||||
|
methods,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAxios = new MockAdapter(axios);
|
||||||
|
mockAxios.onPost(validateQueryPath).reply(validQueryResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component.destroy();
|
||||||
|
mockAxios.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks form validity', done => {
|
||||||
|
mountComponent({
|
||||||
|
metricPersisted: true,
|
||||||
|
...makeFormData({
|
||||||
|
title: 'title',
|
||||||
|
yLabel: 'yLabel',
|
||||||
|
unit: 'unit',
|
||||||
|
group: 'group',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.vm.formIsValid).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hidden inputs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('specifies form operation _method', () => {
|
||||||
|
expect(getNamedInput('_method', 'input').value).toBe('post');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('specifies authenticity token', () => {
|
||||||
|
expect(getNamedInput('authenticity_token', 'input').value).toBe(csrfToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('name input', () => {
|
||||||
|
const name = 'prometheus_metric[title]';
|
||||||
|
|
||||||
|
it('is empty by default', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives a persisted value', () => {
|
||||||
|
const title = 'mockTitle';
|
||||||
|
mountComponent(makeFormData({ title }));
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe(title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('group input', () => {
|
||||||
|
it('has a default value', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(getNamedInput('prometheus_metric[group]', 'glformradiogroup-stub').value).toBe(
|
||||||
|
'business',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('query input', () => {
|
||||||
|
const queryInputName = 'prometheus_metric[query]';
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAxios.onPost(validateQueryPath).reply(validQueryResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is empty by default', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(getNamedInput(queryInputName).value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives and validates a persisted value', () => {
|
||||||
|
const query = 'persistedQuery';
|
||||||
|
const axiosPost = jest.spyOn(axios, 'post');
|
||||||
|
const source = CancelToken.source();
|
||||||
|
mountComponent({ metricPersisted: true, ...makeFormData({ query }) });
|
||||||
|
|
||||||
|
expect(axiosPost).toHaveBeenCalledWith(
|
||||||
|
validateQueryPath,
|
||||||
|
{ query },
|
||||||
|
{ cancelToken: source.token },
|
||||||
|
);
|
||||||
|
expect(getNamedInput(queryInputName).value).toBe(query);
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks validity on user input', () => {
|
||||||
|
const query = 'changedQuery';
|
||||||
|
mountComponent(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
debouncedValidateQuery: debouncedValidateQueryMock,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const queryInput = component.find(`input[name="${queryInputName}"]`);
|
||||||
|
queryInput.setValue(query);
|
||||||
|
queryInput.trigger('input');
|
||||||
|
|
||||||
|
expect(debouncedValidateQueryMock).toHaveBeenCalledWith(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when query validation is in flight', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mountComponent(
|
||||||
|
{ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) },
|
||||||
|
{
|
||||||
|
requestValidation: jest.fn().mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise(resolve =>
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(validQueryResponse);
|
||||||
|
}, 4000),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expect queryValidateInFlight is in flight', done => {
|
||||||
|
const queryInput = component.find(`input[name="${queryInputName}"]`);
|
||||||
|
queryInput.setValue('query');
|
||||||
|
queryInput.trigger('input');
|
||||||
|
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.vm.queryValidateInFlight).toBe(true);
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
waitForPromises()
|
||||||
|
.then(() => {
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.vm.queryValidateInFlight).toBe(false);
|
||||||
|
expect(component.vm.queryIsValid).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expect loading message to display', done => {
|
||||||
|
const queryInput = component.find(`input[name="${queryInputName}"]`);
|
||||||
|
queryInput.setValue('query');
|
||||||
|
queryInput.trigger('input');
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.text()).toContain('Validating query');
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expect loading message to disappear', done => {
|
||||||
|
const queryInput = component.find(`input[name="${queryInputName}"]`);
|
||||||
|
queryInput.setValue('query');
|
||||||
|
queryInput.trigger('input');
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
waitForPromises()
|
||||||
|
.then(() => {
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.vm.queryValidateInFlight).toBe(false);
|
||||||
|
expect(component.vm.queryIsValid).toBe(true);
|
||||||
|
expect(component.vm.errorMessage).toBe('');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when query is invalid', () => {
|
||||||
|
const errorMessage = 'mockErrorMessage';
|
||||||
|
const invalidQueryResponse = {
|
||||||
|
data: { success: true, query: { valid: false, error: errorMessage } },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent(
|
||||||
|
{ metricPersisted: true, ...makeFormData({ query: 'invalidQuery' }) },
|
||||||
|
{
|
||||||
|
requestValidation: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve(invalidQueryResponse)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets queryIsValid to false', done => {
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.vm.queryValidateInFlight).toBe(false);
|
||||||
|
expect(component.vm.queryIsValid).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows invalid query message', done => {
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.text()).toContain(errorMessage);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when query is valid', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent(
|
||||||
|
{ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) },
|
||||||
|
{
|
||||||
|
requestValidation: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve(validQueryResponse)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets queryIsValid to true when query is valid', done => {
|
||||||
|
component.vm.$nextTick(() => {
|
||||||
|
expect(component.vm.queryIsValid).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows valid query message', () => {
|
||||||
|
expect(component.text()).toContain('PromQL query is valid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('yLabel input', () => {
|
||||||
|
const name = 'prometheus_metric[y_label]';
|
||||||
|
|
||||||
|
it('is empty by default', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives a persisted value', () => {
|
||||||
|
const yLabel = 'mockYLabel';
|
||||||
|
mountComponent(makeFormData({ yLabel }));
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe(yLabel);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unit input', () => {
|
||||||
|
const name = 'prometheus_metric[unit]';
|
||||||
|
|
||||||
|
it('is empty by default', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives a persisted value', () => {
|
||||||
|
const unit = 'mockUnit';
|
||||||
|
mountComponent(makeFormData({ unit }));
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe(unit);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('legend input', () => {
|
||||||
|
const name = 'prometheus_metric[legend]';
|
||||||
|
|
||||||
|
it('is empty by default', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives a persisted value', () => {
|
||||||
|
const legend = 'mockLegend';
|
||||||
|
mountComponent(makeFormData({ legend }));
|
||||||
|
|
||||||
|
expect(getNamedInput(name).value).toBe(legend);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import CustomMetricsForm from '~/custom_metrics/components/custom_metrics_form.vue';
|
||||||
|
|
||||||
|
describe('CustomMetricsForm', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
function mountComponent({
|
||||||
|
metricPersisted = false,
|
||||||
|
formData = {
|
||||||
|
title: '',
|
||||||
|
query: '',
|
||||||
|
yLabel: '',
|
||||||
|
unit: '',
|
||||||
|
group: '',
|
||||||
|
legend: '',
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
wrapper = shallowMount(CustomMetricsForm, {
|
||||||
|
propsData: {
|
||||||
|
customMetricsPath: '',
|
||||||
|
editProjectServicePath: '',
|
||||||
|
metricPersisted,
|
||||||
|
validateQueryPath: '',
|
||||||
|
formData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Computed', () => {
|
||||||
|
it('Form button and title text indicate the custom metric is being edited', () => {
|
||||||
|
mountComponent({ metricPersisted: true });
|
||||||
|
|
||||||
|
expect(wrapper.vm.saveButtonText).toBe('Save Changes');
|
||||||
|
expect(wrapper.vm.titleText).toBe('Edit metric');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Form button and title text indicate the custom metric is being created', () => {
|
||||||
|
mountComponent({ metricPersisted: false });
|
||||||
|
|
||||||
|
expect(wrapper.vm.saveButtonText).toBe('Create metric');
|
||||||
|
expect(wrapper.vm.titleText).toBe('New metric');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import statusCodes from '~/lib/utils/http_status';
|
||||||
import * as commonUtils from '~/lib/utils/common_utils';
|
import * as commonUtils from '~/lib/utils/common_utils';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { defaultTimeRange } from '~/vue_shared/constants';
|
import { defaultTimeRange } from '~/vue_shared/constants';
|
||||||
|
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
|
||||||
|
|
||||||
import store from '~/monitoring/stores';
|
import store from '~/monitoring/stores';
|
||||||
import * as types from '~/monitoring/stores/mutation_types';
|
import * as types from '~/monitoring/stores/mutation_types';
|
||||||
|
@ -157,17 +158,21 @@ describe('Monitoring store actions', () => {
|
||||||
variables: {
|
variables: {
|
||||||
projectPath: state.projectPath,
|
projectPath: state.projectPath,
|
||||||
search: searchTerm,
|
search: searchTerm,
|
||||||
|
states: [ENVIRONMENT_AVAILABLE_STATE],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
state.environmentsSearchTerm = searchTerm;
|
state.environmentsSearchTerm = searchTerm;
|
||||||
mockMutate.mockReturnValue(Promise.resolve());
|
mockMutate.mockResolvedValue({});
|
||||||
|
|
||||||
return testAction(
|
return testAction(
|
||||||
fetchEnvironmentsData,
|
fetchEnvironmentsData,
|
||||||
null,
|
null,
|
||||||
state,
|
state,
|
||||||
[],
|
[],
|
||||||
[{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
|
[
|
||||||
|
{ type: 'requestEnvironmentsData' },
|
||||||
|
{ type: 'receiveEnvironmentsDataSuccess', payload: [] },
|
||||||
|
],
|
||||||
() => {
|
() => {
|
||||||
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
|
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
|
||||||
|
|
||||||
|
const issue = {
|
||||||
|
name:
|
||||||
|
'The accessibility scanning found 2 errors of the following type: WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
|
||||||
|
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
|
||||||
|
message: 'This element has insufficient contrast at this conformance level.',
|
||||||
|
status: 'failed',
|
||||||
|
className: 'spec.test_spec',
|
||||||
|
learnMoreUrl: 'https://www.w3.org/TR/WCAG20-TECHS/H91.html',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CustomMetricsForm', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const mountComponent = ({ name, code, message, status, className }, isNew = false) => {
|
||||||
|
wrapper = shallowMount(AccessibilityIssueBody, {
|
||||||
|
propsData: {
|
||||||
|
issue: {
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
},
|
||||||
|
isNew,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findIsNewBadge = () => wrapper.find({ ref: 'accessibility-issue-is-new-badge' });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent(issue);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Displays the issue message', () => {
|
||||||
|
const description = wrapper.find({ ref: 'accessibility-issue-description' }).text();
|
||||||
|
|
||||||
|
expect(description).toContain(`Message: ${issue.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When an issue code is present', () => {
|
||||||
|
it('Creates the correct URL for learning more about the issue code', () => {
|
||||||
|
const learnMoreUrl = wrapper
|
||||||
|
.find({ ref: 'accessibility-issue-learn-more' })
|
||||||
|
.attributes('href');
|
||||||
|
|
||||||
|
expect(learnMoreUrl).toEqual(issue.learnMoreUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When an issue code is not present', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent({
|
||||||
|
...issue,
|
||||||
|
code: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a URL leading to the overview documentation page', () => {
|
||||||
|
const learnMoreUrl = wrapper
|
||||||
|
.find({ ref: 'accessibility-issue-learn-more' })
|
||||||
|
.attributes('href');
|
||||||
|
|
||||||
|
expect(learnMoreUrl).toEqual('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When an issue code does not contain the TECHS code', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent({
|
||||||
|
...issue,
|
||||||
|
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a URL leading to the overview documentation page', () => {
|
||||||
|
const learnMoreUrl = wrapper
|
||||||
|
.find({ ref: 'accessibility-issue-learn-more' })
|
||||||
|
.attributes('href');
|
||||||
|
|
||||||
|
expect(learnMoreUrl).toEqual('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When issue is new', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent(issue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders the new badge', () => {
|
||||||
|
expect(findIsNewBadge().exists()).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When issue is not new', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent(issue, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not render the new badge', () => {
|
||||||
|
expect(findIsNewBadge().exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,7 +11,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
|
||||||
|
|
||||||
let(:shared) { project.import_export_shared }
|
let(:shared) { project.import_export_shared }
|
||||||
|
|
||||||
RSpec.shared_examples 'project tree restorer work properly' do |reader|
|
RSpec.shared_examples 'project tree restorer work properly' do |reader, ndjson_enabled|
|
||||||
describe 'restore project tree' do
|
describe 'restore project tree' do
|
||||||
before_all do
|
before_all do
|
||||||
# Using an admin for import, so we can check assignment of existing members
|
# Using an admin for import, so we can check assignment of existing members
|
||||||
|
@ -25,6 +25,9 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
|
||||||
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
|
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
|
||||||
@shared = @project.import_export_shared
|
@shared = @project.import_export_shared
|
||||||
|
|
||||||
|
allow(Feature).to receive(:enabled?).and_call_original
|
||||||
|
stub_feature_flags(project_import_ndjson: ndjson_enabled)
|
||||||
|
|
||||||
setup_import_export_config('complex')
|
setup_import_export_config('complex')
|
||||||
setup_reader(reader)
|
setup_reader(reader)
|
||||||
|
|
||||||
|
@ -999,23 +1002,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'enable ndjson import' do
|
context 'enable ndjson import' do
|
||||||
before_all do
|
it_behaves_like 'project tree restorer work properly', :legacy_reader, true
|
||||||
# Test suite `restore project tree` run `project_tree_restorer.restore` in `before_all`.
|
|
||||||
# `Enable all features by default for testing` happens in `before(:each)`
|
|
||||||
# So it requires manually enable feature flag to allow ndjson_reader
|
|
||||||
Feature.enable(:project_import_ndjson)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'project tree restorer work properly', :legacy_reader
|
it_behaves_like 'project tree restorer work properly', :ndjson_reader, true
|
||||||
|
|
||||||
it_behaves_like 'project tree restorer work properly', :ndjson_reader
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'disable ndjson import' do
|
context 'disable ndjson import' do
|
||||||
before do
|
it_behaves_like 'project tree restorer work properly', :legacy_reader, false
|
||||||
stub_feature_flags(project_import_ndjson: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'project tree restorer work properly', :legacy_reader
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,6 @@ describe Gitlab::ImportExport::Project::TreeSaver do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:project) { setup_project }
|
let_it_be(:project) { setup_project }
|
||||||
let_it_be(:shared) { project.import_export_shared }
|
let_it_be(:shared) { project.import_export_shared }
|
||||||
let_it_be(:project_tree_saver ) { described_class.new(project: project, current_user: user, shared: shared) }
|
|
||||||
|
|
||||||
let(:relation_name) { :projects }
|
let(:relation_name) { :projects }
|
||||||
|
|
||||||
|
@ -29,10 +28,18 @@ describe Gitlab::ImportExport::Project::TreeSaver do
|
||||||
end
|
end
|
||||||
|
|
||||||
before_all do
|
before_all do
|
||||||
Feature.enable(:project_export_as_ndjson) if ndjson_enabled
|
RSpec::Mocks.with_temporary_scope do
|
||||||
|
allow(Feature).to receive(:enabled?).and_call_original
|
||||||
|
stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
|
||||||
|
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
|
|
||||||
|
stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
|
||||||
|
project_tree_saver = described_class.new(project: project, current_user: user, shared: shared)
|
||||||
|
|
||||||
project_tree_saver.save
|
project_tree_saver.save
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
after :all do
|
after :all do
|
||||||
FileUtils.rm_rf(export_path)
|
FileUtils.rm_rf(export_path)
|
||||||
|
|
|
@ -882,88 +882,6 @@ describe API::Internal::Base do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Uncomment when the end-point is reenabled
|
|
||||||
# describe 'POST /notify_post_receive' do
|
|
||||||
# let(:valid_params) do
|
|
||||||
# { project: project.repository.path, secret_token: secret_token }
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# let(:valid_wiki_params) do
|
|
||||||
# { project: project.wiki.repository.path, secret_token: secret_token }
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# before do
|
|
||||||
# allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "calls the Gitaly client with the project's repository" do
|
|
||||||
# expect(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
|
|
||||||
# and_call_original
|
|
||||||
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:post_receive)
|
|
||||||
#
|
|
||||||
# post api("/internal/notify_post_receive"), valid_params
|
|
||||||
#
|
|
||||||
# expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "calls the Gitaly client with the wiki's repository if it's a wiki" do
|
|
||||||
# expect(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
|
|
||||||
# and_call_original
|
|
||||||
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:post_receive)
|
|
||||||
#
|
|
||||||
# post api("/internal/notify_post_receive"), valid_wiki_params
|
|
||||||
#
|
|
||||||
# expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "returns 500 if the gitaly call fails" do
|
|
||||||
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:post_receive).and_raise(GRPC::Unavailable)
|
|
||||||
#
|
|
||||||
# post api("/internal/notify_post_receive"), valid_params
|
|
||||||
#
|
|
||||||
# expect(response).to have_gitlab_http_status(:internal_server_error)
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# context 'with a gl_repository parameter' do
|
|
||||||
# let(:valid_params) do
|
|
||||||
# { gl_repository: "project-#{project.id}", secret_token: secret_token }
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# let(:valid_wiki_params) do
|
|
||||||
# { gl_repository: "wiki-#{project.id}", secret_token: secret_token }
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "calls the Gitaly client with the project's repository" do
|
|
||||||
# expect(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
|
|
||||||
# and_call_original
|
|
||||||
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:post_receive)
|
|
||||||
#
|
|
||||||
# post api("/internal/notify_post_receive"), valid_params
|
|
||||||
#
|
|
||||||
# expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# it "calls the Gitaly client with the wiki's repository if it's a wiki" do
|
|
||||||
# expect(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
|
|
||||||
# and_call_original
|
|
||||||
# expect_any_instance_of(Gitlab::GitalyClient::NotificationService).
|
|
||||||
# to receive(:post_receive)
|
|
||||||
#
|
|
||||||
# post api("/internal/notify_post_receive"), valid_wiki_params
|
|
||||||
#
|
|
||||||
# expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
|
describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
|
||||||
let(:identifier) { 'key-123' }
|
let(:identifier) { 'key-123' }
|
||||||
let(:branch_name) { 'feature' }
|
let(:branch_name) { 'feature' }
|
||||||
|
|
Loading…
Reference in a new issue