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;
|
||||
}
|
||||
|
||||
export default function dropzoneInput(form) {
|
||||
export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
|
||||
const divHover = '<div class="div-dropzone-hover"></div>';
|
||||
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
||||
const $attachButton = form.find('.button-attach-file');
|
||||
|
@ -69,6 +69,7 @@ export default function dropzoneInput(form) {
|
|||
uploadMultiple: false,
|
||||
headers: csrf.headers,
|
||||
previewContainer: false,
|
||||
...config,
|
||||
processing: () => $('.div-dropzone-alert').alert('close'),
|
||||
dragover: () => {
|
||||
$mdArea.addClass('is-dropzone-hover');
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class GLForm {
|
|||
);
|
||||
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
|
||||
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
|
||||
dropzoneInput(this.form);
|
||||
dropzoneInput(this.form, { parallelUploads: 1 });
|
||||
autosize(this.textarea);
|
||||
}
|
||||
// form and textarea event listeners
|
||||
|
|
|
@ -109,3 +109,9 @@ export const initialStateKeys = [...endpointKeys, 'currentEnvironmentName'];
|
|||
* Constant to indicate if a metric exists in the database
|
||||
*/
|
||||
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) {
|
||||
data: environments(search: $search) {
|
||||
data: environments(search: $search, states: $states) {
|
||||
environments: nodes {
|
||||
name
|
||||
id
|
||||
|
|
|
@ -10,7 +10,7 @@ import statusCodes from '../../lib/utils/http_status';
|
|||
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
|
||||
import { s__, sprintf } from '../../locale';
|
||||
|
||||
import { PROMETHEUS_TIMEOUT } from '../constants';
|
||||
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
|
||||
|
||||
function prometheusMetricQueryParams(timeRange) {
|
||||
const { start, end } = convertToFixedRange(timeRange);
|
||||
|
@ -238,6 +238,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
|
|||
variables: {
|
||||
projectPath: removeLeadingSlash(state.projectPath),
|
||||
search: state.environmentsSearchTerm,
|
||||
states: [ENVIRONMENT_AVAILABLE_STATE],
|
||||
},
|
||||
})
|
||||
.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 AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
|
||||
|
||||
export const components = {
|
||||
AccessibilityIssueBody,
|
||||
TestIssueBody,
|
||||
};
|
||||
|
||||
export const componentNames = {
|
||||
AccessibilityIssueBody: AccessibilityIssueBody.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;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: $gl-text-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1,
|
||||
.h1,
|
||||
h2,
|
||||
|
|
|
@ -567,16 +567,6 @@ body {
|
|||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: $gl-text-color;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
.light-header {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ $h3-font-size: 14px * 1.75;
|
|||
$h4-font-size: 14px * 1.5;
|
||||
$h5-font-size: 14px * 1.25;
|
||||
$h6-font-size: 14px;
|
||||
$headings-color: $gl-text-color;
|
||||
$headings-font-weight: $gl-font-weight-bold;
|
||||
$spacer: $grid-size;
|
||||
$spacers: (
|
||||
0: 0,
|
||||
|
|
|
@ -10,7 +10,7 @@ module GroupsHelper
|
|||
]
|
||||
end
|
||||
|
||||
def group_nav_link_paths
|
||||
def group_settings_nav_link_paths
|
||||
%w[
|
||||
groups#projects
|
||||
groups#edit
|
||||
|
|
|
@ -14,4 +14,12 @@ class ImportExportUploader < AttachmentUploader
|
|||
def move_to_cache
|
||||
false
|
||||
end
|
||||
|
||||
def work_dir
|
||||
File.join(Settings.shared['path'], 'tmp', 'work')
|
||||
end
|
||||
|
||||
def cache_dir
|
||||
File.join(Settings.shared['path'], 'tmp', 'cache')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
= _('Members')
|
||||
|
||||
- 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
|
||||
.nav-icon-container
|
||||
= 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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
|
@ -71,3 +71,190 @@ References:
|
|||
- [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)
|
||||
- [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
|
||||
++ export 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
|
||||
++ 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,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
|
||||
++ CI_PROJECT_ID=17893
|
||||
++ 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. Click **Billing**.
|
||||
- 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:
|
||||
|
||||
|
@ -430,7 +430,7 @@ CI pipeline minutes are the execution time for your [pipelines](../ci/pipelines/
|
|||
|
||||
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**.
|
||||
|
||||
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:
|
||||
|
||||
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. 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.
|
||||
|
||||
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_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) |
|
||||
| `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
|
||||
|
||||
|
@ -215,6 +217,37 @@ license_scanning:
|
|||
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`
|
||||
|
||||
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
|
||||
|
||||
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 the SAML response to include a NameID that uniquely identifies each user.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
##### 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.
|
||||
|
||||
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. 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:
|
||||
|
||||
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 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.
|
||||
|
@ -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:
|
||||
|
||||
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. 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.
|
||||
|
||||
## 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. Enter your credentials on the Identity Provider if prompted.
|
||||
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:
|
||||
|
||||
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. 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).
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ Feature.disable(:save_issuable_health_status)
|
|||
- [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.
|
||||
- [Import issues](csv_import.md)
|
||||
- [Export issues](csv_export.md) **(STARTER)**
|
||||
- [Export issues](csv_export.md)
|
||||
- [Issues API](../../../api/issues.md)
|
||||
- Configure an [external issue tracker](../../../integration/external-issue-tracker.md)
|
||||
such as Jira, Redmine, or Bugzilla.
|
||||
|
|
|
@ -205,16 +205,6 @@ module API
|
|||
|
||||
post '/notify_post_receive' do
|
||||
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
|
||||
|
||||
post '/post_receive' do
|
||||
|
|
|
@ -1024,6 +1024,15 @@ msgstr ""
|
|||
msgid "AccessTokens|reset it"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Learn More"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Message: %{message}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|New"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account"
|
||||
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 createFlash from '~/flash';
|
||||
import { defaultTimeRange } from '~/vue_shared/constants';
|
||||
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
|
||||
|
||||
import store from '~/monitoring/stores';
|
||||
import * as types from '~/monitoring/stores/mutation_types';
|
||||
|
@ -157,17 +158,21 @@ describe('Monitoring store actions', () => {
|
|||
variables: {
|
||||
projectPath: state.projectPath,
|
||||
search: searchTerm,
|
||||
states: [ENVIRONMENT_AVAILABLE_STATE],
|
||||
},
|
||||
};
|
||||
state.environmentsSearchTerm = searchTerm;
|
||||
mockMutate.mockReturnValue(Promise.resolve());
|
||||
mockMutate.mockResolvedValue({});
|
||||
|
||||
return testAction(
|
||||
fetchEnvironmentsData,
|
||||
null,
|
||||
state,
|
||||
[],
|
||||
[{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
|
||||
[
|
||||
{ type: 'requestEnvironmentsData' },
|
||||
{ type: 'receiveEnvironmentsDataSuccess', payload: [] },
|
||||
],
|
||||
() => {
|
||||
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 }
|
||||
|
||||
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
|
||||
before_all do
|
||||
# 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')
|
||||
@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_reader(reader)
|
||||
|
||||
|
@ -999,23 +1002,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
|
|||
end
|
||||
|
||||
context 'enable ndjson import' do
|
||||
before_all do
|
||||
# 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, true
|
||||
|
||||
it_behaves_like 'project tree restorer work properly', :legacy_reader
|
||||
|
||||
it_behaves_like 'project tree restorer work properly', :ndjson_reader
|
||||
it_behaves_like 'project tree restorer work properly', :ndjson_reader, true
|
||||
end
|
||||
|
||||
context 'disable ndjson import' do
|
||||
before do
|
||||
stub_feature_flags(project_import_ndjson: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'project tree restorer work properly', :legacy_reader
|
||||
it_behaves_like 'project tree restorer work properly', :legacy_reader, false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,6 @@ describe Gitlab::ImportExport::Project::TreeSaver do
|
|||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { setup_project }
|
||||
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 }
|
||||
|
||||
|
@ -29,9 +28,17 @@ describe Gitlab::ImportExport::Project::TreeSaver do
|
|||
end
|
||||
|
||||
before_all do
|
||||
Feature.enable(:project_export_as_ndjson) if ndjson_enabled
|
||||
project.add_maintainer(user)
|
||||
project_tree_saver.save
|
||||
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)
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
after :all do
|
||||
|
|
|
@ -882,88 +882,6 @@ describe API::Internal::Base do
|
|||
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
|
||||
let(:identifier) { 'key-123' }
|
||||
let(:branch_name) { 'feature' }
|
||||
|
|
Loading…
Reference in a new issue