diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-08 21:09:50 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-08 21:09:50 +0000 |
commit | 76358aee81a471a5e71eaf3e8c2d91b7c9a0a5a9 (patch) | |
tree | df9ba3dcc09eb404de31e0d79cb8f0b77812e655 | |
parent | 80e9fdc9682cfbcfb9202a2733605a6a6bd23f05 (diff) | |
download | gitlab-ce-76358aee81a471a5e71eaf3e8c2d91b7c9a0a5a9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
38 files changed, 1368 insertions, 297 deletions
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue new file mode 100644 index 00000000000..e5c0d1e4970 --- /dev/null +++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue @@ -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> diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue new file mode 100644 index 00000000000..f5207b47f69 --- /dev/null +++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue @@ -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> diff --git a/app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue b/app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue new file mode 100644 index 00000000000..34e4aeb290f --- /dev/null +++ b/app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue @@ -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> diff --git a/app/assets/javascripts/custom_metrics/constants.js b/app/assets/javascripts/custom_metrics/constants.js new file mode 100644 index 00000000000..2526445fdf9 --- /dev/null +++ b/app/assets/javascripts/custom_metrics/constants.js @@ -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)); +}; diff --git a/app/assets/javascripts/custom_metrics/index.js b/app/assets/javascripts/custom_metrics/index.js new file mode 100644 index 00000000000..4c279daf5f0 --- /dev/null +++ b/app/assets/javascripts/custom_metrics/index.js @@ -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, + }, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 6c158ad8990..f839e9acf04 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -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'); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index a66555838ba..1811a942beb 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -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 diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index b2911bcae8f..e092c0ccae0 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -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'; diff --git a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql index fd3a4348509..17cd1b2c342 100644 --- a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql +++ b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql @@ -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 diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index acc09fa6305..8427a72a68e 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -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 => diff --git a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue new file mode 100644 index 00000000000..6aae9195be1 --- /dev/null +++ b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue @@ -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> diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js index 8b5af263d50..e106e60951b 100644 --- a/app/assets/javascripts/reports/components/issue_body.js +++ b/app/assets/javascripts/reports/components/issue_body.js @@ -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, }; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 875de57cfcc..ed5c133950d 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -42,16 +42,6 @@ html [type="button"], } h1, -h2, -h3, -h4, -h5, -h6 { - color: $gl-text-color; - font-weight: 600; -} - -h1, .h1, h2, .h2, diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b2bbc09664c..69aed2fc20a 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -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; } diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index 7538459c97b..ef75dabbda4 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -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, diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 0d7e2a7bd38..2cd685ddcd4 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -10,7 +10,7 @@ module GroupsHelper ] end - def group_nav_link_paths + def group_settings_nav_link_paths %w[ groups#projects groups#edit diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb index b0e6464f5b1..369afba2bae 100644 --- a/app/uploaders/import_export_uploader.rb +++ b/app/uploaders/import_export_uploader.rb @@ -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 diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 89bcccb6185..8115c713a4f 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -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') diff --git a/changelogs/unreleased/36162-monitoring-dashboard-should-list-only-active-environments-in-dropdo.yml b/changelogs/unreleased/36162-monitoring-dashboard-should-list-only-active-environments-in-dropdo.yml new file mode 100644 index 00000000000..8553149f60b --- /dev/null +++ b/changelogs/unreleased/36162-monitoring-dashboard-should-list-only-active-environments-in-dropdo.yml @@ -0,0 +1,5 @@ +--- +title: Show only active environments in monitoring dropdown +merge_request: 28456 +author: +type: changed diff --git a/changelogs/unreleased/dpisek-use-right-font-weight-for-hN-classes.yml b/changelogs/unreleased/dpisek-use-right-font-weight-for-hN-classes.yml new file mode 100644 index 00000000000..300b4da1c6d --- /dev/null +++ b/changelogs/unreleased/dpisek-use-right-font-weight-for-hN-classes.yml @@ -0,0 +1,6 @@ +--- +title: Align color and font-weight styles of heading elements and their typography + classes +merge_request: 28422 +author: +type: other diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 71d483128b9..585c4903692 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -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. diff --git a/doc/administration/scaling/index.md b/doc/administration/scaling/index.md index 1ef9eca2497..23a96e11805 100644 --- a/doc/administration/scaling/index.md +++ b/doc/administration/scaling/index.md @@ -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. diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d363a4387d9..2f4408aaa6c 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -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 diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 70726f51d32..e2868d648de 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -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: diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 96b051c1673..485e9d8213d 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -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. diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index 308a7b2cb4a..4fcb5064c8c 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -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. diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md index e4dff12b004..42bc52a9201 100644 --- a/doc/user/group/saml_sso/scim_setup.md +++ b/doc/user/group/saml_sso/scim_setup.md @@ -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. diff --git a/doc/user/project/issues/csv_export.md b/doc/user/project/issues/csv_export.md index c5b048c1d69..53af878cbd6 100644 --- a/doc/user/project/issues/csv_export.md +++ b/doc/user/project/issues/csv_export.md @@ -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). diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 4da65793dbb..3c480490e02 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -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. diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 1e528d7a691..a926706f513 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 49f9e66db64..862aca39951 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js new file mode 100644 index 00000000000..61cbef0c557 --- /dev/null +++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js @@ -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); + }); + }); +}); diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js new file mode 100644 index 00000000000..384d6699150 --- /dev/null +++ b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js @@ -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'); + }); + }); +}); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index 7c559aed2c5..b37c10791bf 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -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); }, diff --git a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js new file mode 100644 index 00000000000..794deca42ac --- /dev/null +++ b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js @@ -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); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index 96aed774cfc..c8229eeee94 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -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 + it_behaves_like 'project tree restorer work properly', :legacy_reader, true - 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 diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index d859af5df02..ded57b1d576 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -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 diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index f84336b64c2..0629d51154b 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -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' } |