Commit 76358aee authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 80e9fdc9
<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>
<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>
<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>
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));
};
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';
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 =>
......
<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>
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,
};
......@@ -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')
......
---
title: Show only active environments in monitoring dropdown
merge_request: 28456
author:
type: changed
---
title: Align color and font-weight styles of heading elements and their typography
classes
merge_request: 28422
author:
type: other
This diff is collapsed.
......@@ -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.
......
# 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 ""
......
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);
});
});
});
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);
},
......
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
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
......@@ -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' }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment