Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue18
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue411
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue123
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js83
-rw-r--r--app/assets/javascripts/alerts_settings/index.js35
-rw-r--r--app/assets/javascripts/alerts_settings/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue4
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue6
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_graphql.vue4
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_rest.vue4
-rw-r--r--app/assets/javascripts/releases/components/releases_sort.vue4
-rw-r--r--app/assets/javascripts/releases/components/tag_field.vue2
-rw-r--r--app/assets/javascripts/releases/components/tag_field_existing.vue2
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue6
-rw-r--r--app/assets/javascripts/releases/mount_edit.js4
-rw-r--r--app/assets/javascripts/releases/mount_index.js8
-rw-r--r--app/assets/javascripts/releases/mount_new.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js (renamed from app/assets/javascripts/releases/stores/modules/detail/actions.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js (renamed from app/assets/javascripts/releases/stores/modules/detail/getters.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/index.js (renamed from app/assets/javascripts/releases/stores/modules/detail/index.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js (renamed from app/assets/javascripts/releases/stores/modules/detail/mutation_types.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js (renamed from app/assets/javascripts/releases/stores/modules/detail/mutations.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/state.js (renamed from app/assets/javascripts/releases/stores/modules/detail/state.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/actions.js (renamed from app/assets/javascripts/releases/stores/modules/list/actions.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/index.js (renamed from app/assets/javascripts/releases/stores/modules/list/index.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/mutation_types.js (renamed from app/assets/javascripts/releases/stores/modules/list/mutation_types.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/mutations.js (renamed from app/assets/javascripts/releases/stores/modules/list/mutations.js)0
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/state.js (renamed from app/assets/javascripts/releases/stores/modules/list/state.js)0
-rw-r--r--app/assets/stylesheets/page_bundles/alert_management_settings.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss4
-rw-r--r--app/models/namespace.rb11
-rw-r--r--app/models/namespace_setting.rb2
-rw-r--r--app/models/raw_usage_data.rb4
-rw-r--r--app/services/groups/base_service.rb12
-rw-r--r--app/services/groups/create_service.rb6
-rw-r--r--app/services/groups/update_service.rb12
-rw-r--r--app/services/submit_usage_ping_service.rb8
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml4
-rw-r--r--app/views/shared/empty_states/_issues.html.haml2
-rw-r--r--changelogs/unreleased/292698-save-usage_data_id-raw_data_id-we-receive-from-versions-in-raw_usa.yml5
-rw-r--r--changelogs/unreleased/296882-standalone-shard-replica-settings.yml5
-rw-r--r--changelogs/unreleased/300827-integration-form-cleanup-part-3.yml5
-rw-r--r--changelogs/unreleased/dblessing_ignore_namespace_delayed_project_removal.yml5
-rw-r--r--changelogs/unreleased/make-ci_runner_builds_queue_on_replicas-default.yml5
-rw-r--r--changelogs/unreleased/pb-fix-duration-for-skipped-pipeline.yml5
-rw-r--r--config/feature_flags/development/ci_runner_builds_queue_on_replicas.yml2
-rw-r--r--config/feature_flags/development/other_storage_tab.yml8
-rw-r--r--config/helpers/check_frontend_integration_env.js37
-rw-r--r--db/migrate/20210219211845_add_version_usage_data_id_to_raw_usage_data.rb9
-rw-r--r--db/migrate/20210317100520_create_elastic_index_settings.rb23
-rw-r--r--db/migrate/20210324131727_migrate_elastic_index_settings.rb31
-rw-r--r--db/schema_migrations/202102192118451
-rw-r--r--db/schema_migrations/202103171005201
-rw-r--r--db/schema_migrations/202103241317271
-rw-r--r--db/structure.sql29
-rw-r--r--doc/administration/pages/index.md3
-rw-r--r--doc/api/epic_issues.md5
-rw-r--r--doc/api/packages/conan.md614
-rw-r--r--doc/api/packages/maven.md124
-rw-r--r--doc/user/packages/conan_repository/index.md3
-rw-r--r--doc/user/packages/maven_repository/index.md3
-rw-r--r--generator_templates/active_record/migration/create_table_migration.rb4
-rw-r--r--generator_templates/active_record/migration/migration.rb4
-rw-r--r--generator_templates/rails/post_deployment_migration/migration.rb4
-rw-r--r--jest.config.integration.js3
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/usage_data_non_sql_metrics.rb29
-rw-r--r--locale/gitlab.pot79
-rw-r--r--spec/features/alerts_settings/user_views_alerts_settings_spec.rb2
-rw-r--r--spec/features/projects/fork_spec.rb44
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap524
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js238
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js120
-rw-r--r--spec/frontend/jobs/components/commit_block_spec.js105
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js65
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js8
-rw-r--r--spec/frontend/releases/components/app_index_spec.js8
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js2
-rw-r--r--spec/frontend/releases/components/releases_pagination_graphql_spec.js16
-rw-r--r--spec/frontend/releases/components/releases_pagination_rest_spec.js14
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js12
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js6
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js14
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js8
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js8
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/list/helpers.js2
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js6
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/graphql.js11
-rw-r--r--spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb37
-rw-r--r--spec/migrations/migrate_elastic_index_settings_spec.rb44
-rw-r--r--spec/models/namespace_spec.rb22
-rw-r--r--spec/models/raw_usage_data_spec.rb10
-rw-r--r--spec/services/submit_usage_ping_service_spec.rb35
-rw-r--r--spec/support/shared_contexts/load_balancing_configuration_shared_context.rb21
-rw-r--r--spec/views/projects/settings/operations/show.html.haml_spec.rb2
-rw-r--r--vendor/shims/mimemagic/mimemagic.gemspec7
104 files changed, 2026 insertions, 1259 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
index 07b2e59671e..5171588eb64 100644
--- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
+++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
@@ -118,17 +118,17 @@ export default {
<template>
<div class="gl-display-table gl-w-full gl-mt-5">
<div class="gl-display-table-row">
- <h5 id="gitlabFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
+ <h5 id="gitlabFieldsHeader" class="gl-display-table-cell gl-pb-3 gl-pr-3">
{{ $options.i18n.columns.gitlabKeyTitle }}
</h5>
- <h5 class="gl-display-table-cell gl-py-3 gl-pr-3">&nbsp;</h5>
- <h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
+ <h5 class="gl-display-table-cell gl-pb-3 gl-pr-3">&nbsp;</h5>
+ <h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-pb-3 gl-pr-3">
{{ $options.i18n.columns.payloadKeyTitle }}
</h5>
<h5
v-if="hasFallbackColumn"
id="fallbackFieldsHeader"
- class="gl-display-table-cell gl-py-3 gl-pr-3"
+ class="gl-display-table-cell gl-pb-3 gl-pr-3"
>
{{ $options.i18n.columns.fallbackKeyTitle }}
<gl-icon
@@ -140,11 +140,7 @@ export default {
</h5>
</div>
- <div
- v-for="(gitlabField, index) in mappingData"
- :key="gitlabField.name"
- class="gl-display-table-row"
- >
+ <div v-for="gitlabField in mappingData" :key="gitlabField.name" class="gl-display-table-row">
<div class="gl-display-table-cell gl-py-3 gl-pr-3 gl-w-30p gl-vertical-align-middle">
<gl-form-input
aria-labelledby="gitlabFieldsHeader"
@@ -153,8 +149,8 @@ export default {
/>
</div>
- <div class="gl-display-table-cell gl-py-3 gl-pr-3">
- <div class="right-arrow" :class="{ 'gl-vertical-align-middle': index === 0 }">
+ <div class="gl-display-table-cell gl-pr-3 gl-vertical-align-middle">
+ <div class="right-arrow">
<i class="right-arrow-head"></i>
</div>
</div>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index a5e17d80f86..a071e011ec3 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -139,7 +139,7 @@ export default {
<template>
<div class="incident-management-list">
- <h5 class="gl-font-lg">{{ $options.i18n.title }}</h5>
+ <h5 class="gl-font-lg gl-mt-5">{{ $options.i18n.title }}</h5>
<gl-table
class="integration-list"
:items="integrations"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 5d9513e5b53..bf92da66b27 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -14,7 +14,7 @@ import {
GlTab,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
-import { isEmpty, omit } from 'lodash';
+import { isEqual, isEmpty, omit } from 'lodash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
integrationTypes,
@@ -24,8 +24,9 @@ import {
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
typeSet,
- viewCredentialsTabIndex,
i18n,
+ tabIndices,
+ testAlertModalId,
} from '../constants';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql';
@@ -40,6 +41,10 @@ export default {
typeSet,
integrationSteps,
i18n,
+ primaryProps: { text: i18n.integrationFormSteps.testPayload.savedAndTest },
+ secondaryProps: { text: i18n.integrationFormSteps.testPayload.proceedWithoutSave },
+ cancelProps: { text: i18n.integrationFormSteps.testPayload.cancel },
+ testAlertModalId,
components: {
ClipboardButton,
GlButton,
@@ -60,11 +65,8 @@ export default {
GlModal: GlModalDirective,
},
inject: {
- generic: {
- default: {},
- },
- prometheus: {
- default: {},
+ alertsUsageUrl: {
+ default: '#',
},
multiIntegrations: {
default: false,
@@ -87,6 +89,11 @@ export default {
required: false,
default: null,
},
+ tabIndex: {
+ type: Number,
+ required: false,
+ default: tabIndices.configureDetails,
+ },
},
apollo: {
currentIntegration: {
@@ -96,11 +103,10 @@ export default {
data() {
return {
integrationTypesOptions: Object.values(integrationTypes),
- selectedIntegration: integrationTypes.none.value,
- active: false,
samplePayload: {
json: null,
error: null,
+ loading: false,
},
testPayload: {
json: null,
@@ -108,18 +114,32 @@ export default {
},
resetPayloadAndMappingConfirmed: false,
mapping: [],
- parsingPayload: false,
+ integrationForm: {
+ active: false,
+ type: integrationTypes.none.value,
+ name: '',
+ token: '',
+ url: '',
+ apiUrl: '',
+ },
+ activeTabIndex: this.tabIndex,
currentIntegration: null,
parsedPayload: [],
- activeTabIndex: 0,
+ validationState: {
+ name: true,
+ apiUrl: true,
+ },
};
},
computed: {
isPrometheus() {
- return this.selectedIntegration === this.$options.typeSet.prometheus;
+ return this.integrationForm.type === typeSet.prometheus;
},
isHttp() {
- return this.selectedIntegration === this.$options.typeSet.http;
+ return this.integrationForm.type === typeSet.http;
+ },
+ isNone() {
+ return !this.isHttp && !this.isPrometheus;
},
isCreating() {
return !this.currentIntegration;
@@ -130,29 +150,6 @@ export default {
isTestPayloadValid() {
return this.testPayload.error === null;
},
- selectedIntegrationType() {
- switch (this.selectedIntegration) {
- case typeSet.http:
- return this.generic;
- case typeSet.prometheus:
- return this.prometheus;
- default:
- return {};
- }
- },
- integrationForm() {
- return {
- name: this.currentIntegration?.name || '',
- active: this.currentIntegration?.active || false,
- token:
- this.currentIntegration?.token ||
- (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.token : ''),
- url:
- this.currentIntegration?.url ||
- (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.url : ''),
- apiUrl: this.currentIntegration?.apiUrl || '',
- };
- },
testAlertPayload() {
return {
data: this.testPayload.json,
@@ -170,13 +167,7 @@ export default {
return this.hasSamplePayload && !this.resetPayloadAndMappingConfirmed;
},
canParseSamplePayload() {
- return !this.active || !this.isSampePayloadValid || !this.samplePayload.json;
- },
- isResetAuthKeyDisabled() {
- return !this.active && !this.integrationForm.token !== '';
- },
- isPayloadEditDisabled() {
- return !this.active || this.canEditPayload;
+ return this.isSampePayloadValid && this.samplePayload.json;
},
isSelectDisabled() {
return this.currentIntegration !== null || !this.canAddIntegration;
@@ -186,30 +177,101 @@ export default {
? i18n.integrationFormSteps.setupCredentials.prometheusHelp
: i18n.integrationFormSteps.setupCredentials.help;
},
+ isFormValid() {
+ return (
+ Object.values(this.validationState).every(Boolean) &&
+ !this.isNone &&
+ this.isSampePayloadValid
+ );
+ },
+ isFormDirty() {
+ const { type, active, name, apiUrl, payloadAlertFields = [], payloadAttributeMappings = [] } =
+ this.currentIntegration || {};
+ const {
+ name: formName,
+ apiUrl: formApiUrl,
+ active: formActive,
+ type: formType,
+ } = this.integrationForm;
+
+ const isDirty =
+ type !== formType ||
+ active !== formActive ||
+ name !== formName ||
+ apiUrl !== formApiUrl ||
+ !isEqual(this.parsedPayload, payloadAlertFields) ||
+ !isEqual(this.mapping, this.getCleanMapping(payloadAttributeMappings));
+
+ return isDirty;
+ },
+ canSubmitForm() {
+ return this.isFormValid && this.isFormDirty;
+ },
+ dataForSave() {
+ const { name, apiUrl, active } = this.integrationForm;
+ const customMappingVariables = {
+ payloadAttributeMappings: this.mapping,
+ payloadExample: this.samplePayload.json || '{}',
+ };
+
+ const variables = this.isHttp
+ ? { name, active, ...customMappingVariables }
+ : { apiUrl, active };
+
+ return { type: this.integrationForm.type, variables };
+ },
+ testAlertModal() {
+ return this.isFormDirty ? testAlertModalId : null;
+ },
},
watch: {
+ tabIndex(val) {
+ this.activeTabIndex = val;
+ },
currentIntegration(val) {
if (val === null) {
this.reset();
return;
}
- const { type, active, payloadExample, payloadAlertFields, payloadAttributeMappings } = val;
- this.selectedIntegration = type;
- this.active = active;
- if (type === typeSet.http && this.showMappingBuilder) {
+ this.resetPayloadAndMapping();
+ const {
+ name,
+ type,
+ active,
+ url,
+ apiUrl,
+ token,
+ payloadExample,
+ payloadAlertFields,
+ payloadAttributeMappings,
+ } = val;
+ this.integrationForm = { type, name, active, url, apiUrl, token };
+
+ if (this.showMappingBuilder) {
+ this.resetPayloadAndMappingConfirmed = false;
this.parsedPayload = payloadAlertFields;
this.samplePayload.json = this.isValidNonEmptyJSON(payloadExample) ? payloadExample : null;
- const mapping = payloadAttributeMappings.map((mappingItem) =>
- omit(mappingItem, '__typename'),
- );
- this.updateMapping(mapping);
+ this.updateMapping(this.getCleanMapping(payloadAttributeMappings));
}
- this.activeTabIndex = viewCredentialsTabIndex;
this.$el.scrollIntoView({ block: 'center' });
},
},
methods: {
+ getCleanMapping(mapping) {
+ return mapping.map((mappingItem) => omit(mappingItem, '__typename'));
+ },
+ validateName() {
+ this.validationState.name = Boolean(this.integrationForm.name?.length);
+ },
+ validateApiUrl() {
+ try {
+ const parsedUrl = new URL(this.integrationForm.apiUrl);
+ this.validationState.apiUrl = ['http:', 'https:'].includes(parsedUrl.protocol);
+ } catch (e) {
+ this.validationState.apiUrl = false;
+ }
+ },
isValidNonEmptyJSON(JSONString) {
if (JSONString) {
let parsed;
@@ -222,29 +284,24 @@ export default {
}
return false;
},
+ triggerValidation() {
+ if (this.isHttp) {
+ this.validationState.apiUrl = true;
+ this.validateName();
+ } else if (this.isPrometheus) {
+ this.validationState.name = true;
+ this.validateApiUrl();
+ }
+ },
sendTestAlert() {
this.$emit('test-alert-payload', this.testAlertPayload);
},
- submit() {
- const { name, apiUrl } = this.integrationForm;
- const customMappingVariables = {
- payloadAttributeMappings: this.mapping,
- payloadExample: this.samplePayload.json || '{}',
- };
-
- const variables =
- this.selectedIntegration === typeSet.http
- ? { name, active: this.active, ...customMappingVariables }
- : { apiUrl, active: this.active };
-
- const integrationPayload = { type: this.selectedIntegration, variables };
-
- if (this.currentIntegration) {
- return this.$emit('update-integration', integrationPayload);
- }
-
- this.reset();
- return this.$emit('create-new-integration', integrationPayload);
+ saveAndSendTestAlert() {
+ this.$emit('save-and-test-alert-payload', this.dataForSave, this.testAlertPayload);
+ },
+ submit(testAfterSubmit = false) {
+ const event = this.currentIntegration ? 'update-integration' : 'create-new-integration';
+ this.$emit(event, this.dataForSave, testAfterSubmit);
},
reset() {
this.resetFormValues();
@@ -252,14 +309,14 @@ export default {
this.$emit('clear-current-integration', { type: this.currentIntegration?.type });
},
resetFormValues() {
- this.selectedIntegration = integrationTypes.none.value;
+ this.integrationForm.type = integrationTypes.none.value;
this.integrationForm.name = '';
+ this.integrationForm.active = false;
this.integrationForm.apiUrl = '';
this.samplePayload = {
json: null,
error: null,
};
- this.active = false;
},
resetAuthKey() {
if (!this.currentIntegration) {
@@ -267,7 +324,7 @@ export default {
}
this.$emit('reset-token', {
- type: this.selectedIntegration,
+ type: this.integrationForm.type,
variables: { id: this.currentIntegration.id },
});
},
@@ -285,8 +342,8 @@ export default {
payload.error = JSON.stringify(e.message);
}
},
- parseMapping() {
- this.parsingPayload = true;
+ parseSamplePayload() {
+ this.samplePayload.loading = true;
return this.$apollo
.query({
@@ -303,7 +360,7 @@ export default {
this.resetPayloadAndMappingConfirmed = false;
this.$toast.show(
- this.$options.i18n.integrationFormSteps.setSamplePayload.payloadParsedSucessMsg,
+ this.$options.i18n.integrationFormSteps.mapFields.payloadParsedSucessMsg,
);
},
)
@@ -311,7 +368,7 @@ export default {
this.samplePayload.error = message;
})
.finally(() => {
- this.parsingPayload = false;
+ this.samplePayload.loading = false;
});
},
updateMapping(mapping) {
@@ -338,7 +395,7 @@ export default {
<template>
<gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
<gl-tabs v-model="activeTabIndex">
- <gl-tab :title="$options.i18n.integrationTabs.configureDetails">
+ <gl-tab :title="$options.i18n.integrationTabs.configureDetails" class="gl-mt-3">
<gl-form-group
v-if="isCreating"
id="integration-type"
@@ -351,10 +408,11 @@ export default {
label-for="integration-type"
>
<gl-form-select
- v-model="selectedIntegration"
+ v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
:options="integrationTypesOptions"
+ @change="triggerValidation"
/>
<alert-settings-form-help-block
@@ -369,7 +427,6 @@ export default {
<div class="gl-mt-3">
<gl-form-group
v-if="isHttp"
- id="name-integration"
:label="
getLabelWithStepNumber(
$options.integrationSteps.nameIntegration,
@@ -377,67 +434,81 @@ export default {
)
"
label-for="name-integration"
+ :invalid-feedback="$options.i18n.integrationFormSteps.nameIntegration.error"
+ :state="validationState.name"
>
<gl-form-input
+ id="name-integration"
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
+ @input="validateName"
/>
</gl-form-group>
- <gl-toggle
- v-model="active"
- :is-loading="loading"
- :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
- class="gl-my-4 gl-font-weight-normal"
- />
-
- <div v-if="isPrometheus" class="gl-my-4">
- <span class="gl-font-weight-bold">
- {{
- getLabelWithStepNumber(
- $options.integrationSteps.setPrometheusApiUrl,
- $options.i18n.integrationFormSteps.prometheusFormUrl.label,
- )
- }}
- </span>
+ <gl-form-group
+ v-if="!isNone"
+ :label="
+ getLabelWithStepNumber(
+ isHttp
+ ? $options.integrationSteps.enableHttpIntegration
+ : $options.integrationSteps.enablePrometheusIntegration,
+ $options.i18n.integrationFormSteps.enableIntegration.label,
+ )
+ "
+ >
+ <span>{{ $options.i18n.integrationFormSteps.enableIntegration.help }}</span>
+
+ <gl-toggle
+ id="enable-integration"
+ v-model="integrationForm.active"
+ :is-loading="loading"
+ :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
+ class="gl-mt-4 gl-font-weight-normal"
+ />
+ </gl-form-group>
+ <gl-form-group
+ v-if="isPrometheus"
+ class="gl-my-4"
+ :label="$options.i18n.integrationFormSteps.prometheusFormUrl.label"
+ label-for="api-url"
+ :invalid-feedback="$options.i18n.integrationFormSteps.prometheusFormUrl.error"
+ :state="validationState.apiUrl"
+ >
<gl-form-input
- id="integration-apiUrl"
+ id="api-url"
v-model="integrationForm.apiUrl"
type="text"
:placeholder="$options.placeholders.prometheus"
+ @input="validateApiUrl"
/>
-
<span class="gl-text-gray-400">
{{ $options.i18n.integrationFormSteps.prometheusFormUrl.help }}
</span>
- </div>
+ </gl-form-group>
<template v-if="showMappingBuilder">
<gl-form-group
data-testid="sample-payload-section"
:label="
getLabelWithStepNumber(
- $options.integrationSteps.setSamplePayload,
- $options.i18n.integrationFormSteps.setSamplePayload.label,
+ $options.integrationSteps.customizeMapping,
+ $options.i18n.integrationFormSteps.mapFields.label,
)
"
label-for="sample-payload"
class="gl-mb-0!"
:invalid-feedback="samplePayload.error"
>
- <alert-settings-form-help-block
- :message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelpHttp"
- :link="generic.alertsUsageUrl"
- />
+ <span>{{ $options.i18n.integrationFormSteps.mapFields.help }}</span>
<gl-form-textarea
id="sample-payload"
v-model.trim="samplePayload.json"
- :disabled="isPayloadEditDisabled"
+ :disabled="canEditPayload"
:state="isSampePayloadValid"
- :placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
+ :placeholder="$options.i18n.integrationFormSteps.mapFields.placeholder"
class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
@@ -450,71 +521,76 @@ export default {
v-if="canEditPayload"
v-gl-modal.resetPayloadModal
data-testid="payload-action-btn"
- :disabled="!active"
+ :disabled="!integrationForm.active"
class="gl-mt-3"
>
- {{ $options.i18n.integrationFormSteps.setSamplePayload.editPayload }}
+ {{ $options.i18n.integrationFormSteps.mapFields.editPayload }}
</gl-button>
<gl-button
v-else
data-testid="payload-action-btn"
:class="{ 'gl-mt-3': samplePayload.error }"
- :disabled="canParseSamplePayload"
- :loading="parsingPayload"
- @click="parseMapping"
+ :disabled="!canParseSamplePayload"
+ :loading="samplePayload.loading"
+ @click="parseSamplePayload"
>
- {{ $options.i18n.integrationFormSteps.setSamplePayload.parsePayload }}
+ {{ $options.i18n.integrationFormSteps.mapFields.parsePayload }}
</gl-button>
<gl-modal
modal-id="resetPayloadModal"
- :title="$options.i18n.integrationFormSteps.setSamplePayload.resetHeader"
- :ok-title="$options.i18n.integrationFormSteps.setSamplePayload.resetOk"
+ :title="$options.i18n.integrationFormSteps.mapFields.resetHeader"
+ :ok-title="$options.i18n.integrationFormSteps.mapFields.resetOk"
ok-variant="danger"
- @ok="resetPayloadAndMapping"
+ @ok="resetPayloadAndMappingConfirmed = true"
>
- {{ $options.i18n.integrationFormSteps.setSamplePayload.resetBody }}
+ {{ $options.i18n.integrationFormSteps.mapFields.resetBody }}
</gl-modal>
- <gl-form-group
- id="mapping-builder"
- class="gl-mt-5"
- :label="
- getLabelWithStepNumber(
- $options.integrationSteps.customizeMapping,
- $options.i18n.integrationFormSteps.mapFields.label,
- )
- "
- label-for="mapping-builder"
- >
- <span>{{ $options.i18n.integrationFormSteps.mapFields.intro }}</span>
+ <div class="gl-mt-5">
+ <span>{{ $options.i18n.integrationFormSteps.mapFields.mapIntro }}</span>
<mapping-builder
:parsed-payload="parsedPayload"
:saved-mapping="mapping"
:alert-fields="alertFields"
@onMappingUpdate="updateMapping"
/>
- </gl-form-group>
+ </div>
</template>
</div>
-
<div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button
- type="submit"
+ :disabled="!canSubmitForm"
variant="confirm"
class="js-no-auto-disable"
data-testid="integration-form-submit"
+ @click="submit(false)"
>
{{ $options.i18n.saveIntegration }}
</gl-button>
+ <gl-button
+ :disabled="!canSubmitForm"
+ variant="confirm"
+ category="secondary"
+ class="gl-ml-3 js-no-auto-disable"
+ data-testid="integration-form-test-and-submit"
+ @click="submit(true)"
+ >
+ {{ $options.i18n.saveAndTestIntegration }}
+ </gl-button>
+
<gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
$options.i18n.cancelAndClose
}}</gl-button>
</div>
</gl-tab>
- <gl-tab :title="$options.i18n.integrationTabs.viewCredentials" :disabled="isCreating">
+ <gl-tab
+ :title="$options.i18n.integrationTabs.viewCredentials"
+ :disabled="isCreating"
+ class="gl-mt-3"
+ >
<alert-settings-form-help-block
:message="viewCredentialsHelpMsg"
link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
@@ -559,13 +635,15 @@ export default {
</div>
</gl-form-group>
- <gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled" variant="danger">
- {{ $options.i18n.integrationFormSteps.setupCredentials.reset }}
- </gl-button>
+ <div class="gl-display-flex gl-justify-content-start gl-py-3">
+ <gl-button v-gl-modal.authKeyModal variant="danger">
+ {{ $options.i18n.integrationFormSteps.setupCredentials.reset }}
+ </gl-button>
- <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
- $options.i18n.cancelAndClose
- }}</gl-button>
+ <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
+ {{ $options.i18n.cancelAndClose }}
+ </gl-button>
+ </div>
<gl-modal
modal-id="authKeyModal"
@@ -578,18 +656,22 @@ export default {
</gl-modal>
</gl-tab>
- <gl-tab :title="$options.i18n.integrationTabs.sendTestAlert" :disabled="isCreating">
+ <gl-tab
+ :title="$options.i18n.integrationTabs.sendTestAlert"
+ :disabled="isCreating"
+ class="gl-mt-3"
+ >
<gl-form-group id="test-integration" :invalid-feedback="testPayload.error">
<alert-settings-form-help-block
- :message="$options.i18n.integrationFormSteps.setSamplePayload.testPayloadHelp"
- :link="generic.alertsUsageUrl"
+ :message="$options.i18n.integrationFormSteps.testPayload.help"
+ :link="alertsUsageUrl"
/>
<gl-form-textarea
id="test-payload"
v-model.trim="testPayload.json"
:state="isTestPayloadValid"
- :placeholder="$options.i18n.integrationFormSteps.setSamplePayload.placeholder"
+ :placeholder="$options.i18n.integrationFormSteps.testPayload.placeholder"
class="gl-my-3"
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
@@ -597,20 +679,35 @@ export default {
@input="validateJson(false)"
/>
</gl-form-group>
+ <div class="gl-display-flex gl-justify-content-start gl-py-3">
+ <gl-button
+ v-gl-modal="testAlertModal"
+ :disabled="!isTestPayloadValid"
+ :loading="loading"
+ data-testid="send-test-alert"
+ variant="confirm"
+ class="js-no-auto-disable"
+ @click="isFormDirty ? null : sendTestAlert()"
+ >
+ {{ $options.i18n.send }}
+ </gl-button>
- <gl-button
- :disabled="!isTestPayloadValid"
- data-testid="send-test-alert"
- variant="confirm"
- class="js-no-auto-disable"
- @click="sendTestAlert"
- >
- {{ $options.i18n.send }}
- </gl-button>
+ <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
+ {{ $options.i18n.cancelAndClose }}
+ </gl-button>
+ </div>
- <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
- $options.i18n.cancelAndClose
- }}</gl-button>
+ <gl-modal
+ :modal-id="$options.testAlertModalId"
+ :title="$options.i18n.integrationFormSteps.testPayload.modalTitle"
+ :action-primary="$options.primaryProps"
+ :action-secondary="$options.secondaryProps"
+ :action-cancel="$options.cancelProps"
+ @primary="saveAndSendTestAlert"
+ @secondary="sendTestAlert"
+ >
+ {{ $options.i18n.integrationFormSteps.testPayload.modalBody }}
+ </gl-modal>
</gl-tab>
</gl-tabs>
</gl-form>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 3ffb652e61b..f51c8d7e9f7 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -1,11 +1,11 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlAlert } from '@gitlab/ui';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
-import { s__ } from '~/locale';
-import { typeSet } from '../constants';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { typeSet, i18n, tabIndices } from '../constants';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
@@ -28,21 +28,12 @@ import {
RESET_INTEGRATION_TOKEN_ERROR,
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
+ INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
+ DEFAULT_ERROR,
} from '../utils/error_messages';
import IntegrationsList from './alerts_integrations_list.vue';
import AlertSettingsForm from './alerts_settings_form.vue';
-export const i18n = {
- changesSaved: s__(
- 'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
- ),
- integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
- alertSent: s__(
- 'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.',
- ),
- addNewIntegration: s__('AlertSettings|Add new integration'),
-};
-
export default {
typeSet,
i18n,
@@ -50,14 +41,9 @@ export default {
IntegrationsList,
AlertSettingsForm,
GlButton,
+ GlAlert,
},
inject: {
- generic: {
- default: {},
- },
- prometheus: {
- default: {},
- },
projectPath: {
default: '',
},
@@ -124,7 +110,10 @@ export default {
integrations: {},
httpIntegrations: {},
currentIntegration: null,
+ newIntegration: null,
formVisible: false,
+ showSuccessfulCreateAlert: false,
+ tabIndex: tabIndices.configureDetails,
};
},
computed: {
@@ -139,10 +128,10 @@ export default {
isHttp(type) {
return type === typeSet.http;
},
- createNewIntegration({ type, variables }) {
+ createNewIntegration({ type, variables }, testAfterSubmit) {
const { projectPath } = this;
-
const isHttp = this.isHttp(type);
+
this.isUpdating = true;
this.$apollo
.mutate({
@@ -163,16 +152,19 @@ export default {
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
- return createFlash({ message: error });
+ createFlash({ message: error });
+ return;
}
- const { integration } = httpIntegrationCreate || prometheusIntegrationCreate;
- this.editIntegration(integration);
+ const { integration } = httpIntegrationCreate || prometheusIntegrationCreate;
+ this.newIntegration = integration;
+ this.showSuccessfulCreateAlert = true;
- return createFlash({
- message: this.$options.i18n.changesSaved,
- type: FLASH_TYPES.SUCCESS,
- });
+ if (testAfterSubmit) {
+ this.viewIntegration(this.newIntegration, tabIndices.sendTestAlert);
+ } else {
+ this.setFormVisibility(false);
+ }
})
.catch(() => {
createFlash({ message: ADD_INTEGRATION_ERROR });
@@ -181,9 +173,9 @@ export default {
this.isUpdating = false;
});
},
- updateIntegration({ type, variables }) {
+ updateIntegration({ type, variables }, testAfterSubmit) {
this.isUpdating = true;
- this.$apollo
+ return this.$apollo
.mutate({
mutation: this.isHttp(type)
? updateHttpIntegrationMutation
@@ -196,12 +188,20 @@ export default {
.then(({ data: { httpIntegrationUpdate, prometheusIntegrationUpdate } = {} } = {}) => {
const error = httpIntegrationUpdate?.errors[0] || prometheusIntegrationUpdate?.errors[0];
if (error) {
- return createFlash({ message: error });
+ createFlash({ message: error });
+ return;
}
- this.clearCurrentIntegration({ type });
+ const integration =
+ httpIntegrationUpdate?.integration || prometheusIntegrationUpdate?.integration;
- return createFlash({
+ if (testAfterSubmit) {
+ this.viewIntegration(integration, tabIndices.sendTestAlert);
+ } else {
+ this.clearCurrentIntegration(type);
+ }
+
+ createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
});
@@ -261,13 +261,23 @@ export default {
currentIntegration = { ...currentIntegration, ...httpIntegrationMappingData };
}
- this.$apollo.mutate({
- mutation: this.isHttp(type)
- ? updateCurrentHttpIntegrationMutation
- : updateCurrentPrometheusIntegrationMutation,
- variables: currentIntegration,
- });
- this.setFormVisibility(true);
+ this.viewIntegration(currentIntegration, tabIndices.viewCredentials);
+ },
+ viewIntegration(integration, tabIndex) {
+ this.$apollo
+ .mutate({
+ mutation: this.isHttp(integration.type)
+ ? updateCurrentHttpIntegrationMutation
+ : updateCurrentPrometheusIntegrationMutation,
+ variables: integration,
+ })
+ .then(() => {
+ this.setFormVisibility(true);
+ this.tabIndex = tabIndex;
+ })
+ .catch(() => {
+ createFlash({ message: DEFAULT_ERROR });
+ });
},
deleteIntegration({ id, type }) {
const { projectPath } = this;
@@ -319,19 +329,44 @@ export default {
type: FLASH_TYPES.SUCCESS,
});
})
- .catch(() => {
- createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ .catch((error) => {
+ let message = INTEGRATION_PAYLOAD_TEST_ERROR;
+ if (error.response?.status === httpStatusCodes.FORBIDDEN) {
+ message = INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR;
+ }
+ createFlash({ message });
});
},
+ saveAndTestAlertPayload(integration, payload) {
+ return this.updateIntegration(integration, false).then(() => {
+ this.testAlertPayload(payload);
+ });
+ },
setFormVisibility(visible) {
this.formVisible = visible;
},
+ viewCreatedIntegration() {
+ this.viewIntegration(this.newIntegration, tabIndices.viewCredentials);
+ this.showSuccessfulCreateAlert = false;
+ this.newIntegration = null;
+ },
},
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="showSuccessfulCreateAlert"
+ class="gl-mt-n2"
+ :primary-button-text="$options.i18n.integrationCreated.btnCaption"
+ :title="$options.i18n.integrationCreated.title"
+ @primaryAction="viewCreatedIntegration"
+ @dismiss="showSuccessfulCreateAlert = false"
+ >
+ {{ $options.i18n.integrationCreated.successMsg }}
+ </gl-alert>
+
<integrations-list
:integrations="integrations.list"
:loading="loading"
@@ -353,11 +388,13 @@ export default {
:loading="isUpdating"
:can-add-integration="canAddIntegration"
:alert-fields="alertFields"
+ :tab-index="tabIndex"
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
@test-alert-payload="testAlertPayload"
+ @save-and-test-alert-payload="saveAndTestAlertPayload"
/>
</div>
</template>
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js
index ce6cf61b5dd..d64ac55ff98 100644
--- a/app/assets/javascripts/alerts_settings/constants.js
+++ b/app/assets/javascripts/alerts_settings/constants.js
@@ -17,6 +17,13 @@ export const i18n = {
label: s__('AlertSettings|Name integration'),
placeholder: s__('AlertSettings|Enter integration name'),
activeToggle: __('Active'),
+ error: __("Name can't be blank"),
+ },
+ enableIntegration: {
+ label: s__('AlertSettings|Enable integration'),
+ help: s__(
+ 'AlertSettings|A webhook URL and authorization key will be generated for the integration. Both will be visible after saving the integration in the “View credentials” tab.',
+ ),
},
setupCredentials: {
help: s__(
@@ -29,35 +36,41 @@ export const i18n = {
authorizationKey: s__('AlertSettings|Authorization key'),
reset: s__('AlertSettings|Reset Key'),
},
- setSamplePayload: {
- label: s__('AlertSettings|Sample alert payload (optional)'),
- testPayloadHelpHttp: s__(
- 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional).',
- ),
- testPayloadHelp: s__(
- 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.',
+ mapFields: {
+ label: s__('AlertSettings|Customize alert payload mapping (optional)'),
+ help: s__(
+ 'AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click the "parse payload fields" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration.',
),
placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
+ editPayload: s__('AlertSettings|Edit payload'),
+ parsePayload: s__('AlertSettings|Parse payload fields'),
+ payloadParsedSucessMsg: s__(
+ 'AlertSettings|Sample payload has been parsed. You can now map the fields.',
+ ),
resetHeader: s__('AlertSettings|Reset the mapping'),
resetBody: s__(
"AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields.",
),
resetOk: s__('AlertSettings|Proceed with editing'),
- editPayload: s__('AlertSettings|Edit payload'),
- parsePayload: s__('AlertSettings|Parse payload for custom mapping'),
- payloadParsedSucessMsg: s__(
- 'AlertSettings|Sample payload has been parsed. You can now map the fields.',
+ mapIntro: s__(
+ "AlertSettings|The default GitLab alert fields are listed below. If you choose to map your payload keys to GitLab's, please make a selection in the dropdowns below. You may also opt to leave the fields unmapped and move straight to saving your integration.",
),
},
- mapFields: {
- label: s__('AlertSettings|Customize alert payload mapping (optional)'),
- intro: s__(
- 'AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click "parse payload fields" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration.',
+ testPayload: {
+ help: s__(
+ 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.',
),
+ placeholder: s__('AlertSettings|{ "events": [{ "application": "Name of application" }] }'),
+ modalTitle: s__('AlertSettings|The form has unsaved changes'),
+ modalBody: s__('AlertSettings|The form has unsaved changes. How would you like to proceed?'),
+ savedAndTest: s__('AlertSettings|Save integration & send'),
+ proceedWithoutSave: s__('AlertSettings|Send without saving'),
+ cancel: __('Cancel'),
},
prometheusFormUrl: {
label: s__('AlertSettings|Prometheus API base URL'),
help: s__('AlertSettings|URL cannot be blank and must start with http or https'),
+ error: s__('AlertSettings|URL is invalid.'),
},
restKeyInfo: {
label: s__(
@@ -66,40 +79,52 @@ export const i18n = {
},
},
saveIntegration: s__('AlertSettings|Save integration'),
- changesSaved: s__('AlertSettings|Your integration was successfully updated.'),
+ saveAndTestIntegration: s__('AlertSettings|Save & create test alert'),
cancelAndClose: __('Cancel and close'),
- send: s__('AlertSettings|Send'),
+ send: __('Send'),
copy: __('Copy'),
+ integrationCreated: {
+ title: s__('AlertSettings|Integration successfully saved'),
+ successMsg: s__(
+ 'AlertSettings|A URL and authorization key have been created for your integration. You will need them to setup a webhook and authorize your endpoint to send alerts to GitLab.',
+ ),
+ btnCaption: s__('AlertSettings|View URL and authorization key'),
+ },
+ changesSaved: s__('AlertsIntegrations|The integration has been successfully saved.'),
+ integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
+ alertSent: s__(
+ 'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.',
+ ),
+ addNewIntegration: s__('AlertSettings|Add new integration'),
};
export const integrationSteps = {
selectType: 'SELECT_TYPE',
nameIntegration: 'NAME_INTEGRATION',
- setPrometheusApiUrl: 'SET_PROMETHEUS_API_URL',
- setSamplePayload: 'SET_SAMPLE_PAYLOAD',
+ enableHttpIntegration: 'ENABLE_HTTP_INTEGRATION',
+ enablePrometheusIntegration: 'ENABLE_PROMETHEUS_INTEGRATION',
customizeMapping: 'CUSTOMIZE_MAPPING',
};
export const createStepNumbers = {
[integrationSteps.selectType]: 1,
[integrationSteps.nameIntegration]: 2,
- [integrationSteps.setPrometheusApiUrl]: 2,
- [integrationSteps.setSamplePayload]: 3,
+ [integrationSteps.enableHttpIntegration]: 3,
+ [integrationSteps.enablePrometheusIntegration]: 2,
[integrationSteps.customizeMapping]: 4,
};
export const editStepNumbers = {
- [integrationSteps.selectType]: 1,
[integrationSteps.nameIntegration]: 1,
- [integrationSteps.setPrometheusApiUrl]: null,
- [integrationSteps.setSamplePayload]: 2,
+ [integrationSteps.enableHttpIntegration]: 2,
+ [integrationSteps.enablePrometheusIntegration]: null,
[integrationSteps.customizeMapping]: 3,
};
export const integrationTypes = {
none: { value: '', text: s__('AlertSettings|Select integration type') },
http: { value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
- prometheus: { value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
+ prometheus: { value: 'PROMETHEUS', text: s__('AlertSettings|Prometheus') },
};
export const typeSet = {
@@ -127,4 +152,10 @@ export const mappingFields = {
fallback: 'fallback',
};
-export const viewCredentialsTabIndex = 1;
+export const tabIndices = {
+ configureDetails: 0,
+ viewCredentials: 1,
+ sendTestAlert: 2,
+};
+
+export const testAlertModalId = 'confirmSendTestAlert';
diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js
index e830f53a55e..953a867b2b7 100644
--- a/app/assets/javascripts/alerts_settings/index.js
+++ b/app/assets/javascripts/alerts_settings/index.js
@@ -19,23 +19,7 @@ export default (el) => {
return null;
}
- const {
- prometheusActivated,
- prometheusUrl,
- prometheusAuthorizationKey,
- prometheusFormPath,
- prometheusResetKeyPath,
- prometheusApiUrl,
- activated: activatedStr,
- alertsSetupUrl,
- alertsUsageUrl,
- formPath,
- authorizationKey,
- url,
- projectPath,
- multiIntegrations,
- alertFields,
- } = el.dataset;
+ const { alertsUsageUrl, projectPath, multiIntegrations, alertFields } = el.dataset;
return new Vue({
el,
@@ -43,22 +27,7 @@ export default (el) => {
AlertSettingsWrapper,
},
provide: {
- prometheus: {
- active: parseBoolean(prometheusActivated),
- url: prometheusUrl,
- token: prometheusAuthorizationKey,
- prometheusFormPath,
- prometheusResetKeyPath,
- prometheusApiUrl,
- },
- generic: {
- alertsSetupUrl,
- alertsUsageUrl,
- active: parseBoolean(activatedStr),
- formPath,
- token: authorizationKey,
- url,
- },
+ alertsUsageUrl,
projectPath,
multiIntegrations: parseBoolean(multiIntegrations),
},
diff --git a/app/assets/javascripts/alerts_settings/utils/error_messages.js b/app/assets/javascripts/alerts_settings/utils/error_messages.js
index e380257f983..9a0644b4e22 100644
--- a/app/assets/javascripts/alerts_settings/utils/error_messages.js
+++ b/app/assets/javascripts/alerts_settings/utils/error_messages.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const DELETE_INTEGRATION_ERROR = s__(
'AlertsIntegrations|The integration could not be deleted. Please try again.',
@@ -19,3 +19,9 @@ export const RESET_INTEGRATION_TOKEN_ERROR = s__(
export const INTEGRATION_PAYLOAD_TEST_ERROR = s__(
'AlertsIntegrations|Integration payload is invalid.',
);
+
+export const INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR = s__(
+ 'AlertsIntegrations|The integration is currently inactive. Enable the integration to send the test alert.',
+);
+
+export const DEFAULT_ERROR = __('Something went wrong on our end.');
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index eae6b5d5419..7f25ca8a94d 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLink } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -23,9 +22,9 @@ export default {
</script>
<template>
<div>
- <span class="font-weight-bold">{{ __('Commit') }}</span>
+ <span class="gl-font-weight-bold">{{ __('Commit') }}</span>
- <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit">
+ <gl-link :href="commit.commit_path" class="gl-text-blue-600!" data-testid="commit-sha">
{{ commit.short_id }}
</gl-link>
@@ -37,8 +36,8 @@ export default {
/>
<span v-if="mergeRequest">
- in
- <gl-link :href="mergeRequest.path" class="js-link-commit link-commit"
+ {{ __('in') }}
+ <gl-link :href="mergeRequest.path" class="gl-text-blue-600!" data-testid="link-commit"
>!{{ mergeRequest.iid }}</gl-link
>
</span>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
index 816761f2ef8..c24cb07b81a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
@@ -22,6 +22,9 @@ export default {
finishedTime() {
return this.pipeline?.details?.finished_at;
},
+ skipped() {
+ return this.pipeline?.details?.status?.label === 'skipped';
+ },
durationFormatted() {
const date = new Date(this.duration * 1000);
@@ -48,16 +51,11 @@ export default {
legacyTableMobileClass() {
return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : '';
},
- singleStagePipelineManual() {
- return (
- this.pipeline.details.manual_actions.length > 0 && this.pipeline.details.stages.length === 1
- );
- },
showInProgress() {
- return !this.duration && !this.finishedTime && !this.singleStagePipelineManual;
+ return !this.duration && !this.finishedTime && !this.skipped;
},
showSkipped() {
- return !this.duration && !this.finishedTime && this.singleStagePipelineManual;
+ return !this.duration && !this.finishedTime && this.skipped;
},
},
};
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index a8c7b7c857a..93ddbde0dc4 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -22,7 +22,7 @@ export default {
TagField,
},
computed: {
- ...mapState('detail', [
+ ...mapState('editNew', [
'isFetchingRelease',
'isUpdatingRelease',
'fetchError',
@@ -36,13 +36,13 @@ export default {
'groupId',
'groupMilestonesAvailable',
]),
- ...mapGetters('detail', ['isValid', 'isExistingRelease']),
+ ...mapGetters('editNew', ['isValid', 'isExistingRelease']),
showForm() {
return Boolean(!this.isFetchingRelease && !this.fetchError && this.release);
},
releaseTitle: {
get() {
- return this.$store.state.detail.release.name;
+ return this.$store.state.editNew.release.name;
},
set(title) {
this.updateReleaseTitle(title);
@@ -50,7 +50,7 @@ export default {
},
releaseNotes: {
get() {
- return this.$store.state.detail.release.description;
+ return this.$store.state.editNew.release.description;
},
set(notes) {
this.updateReleaseNotes(notes);
@@ -58,7 +58,7 @@ export default {
},
releaseMilestones: {
get() {
- return this.$store.state.detail.release.milestones;
+ return this.$store.state.editNew.release.milestones;
},
set(milestones) {
this.updateReleaseMilestones(milestones);
@@ -93,7 +93,7 @@ export default {
this.$el.querySelector('input:enabled, button:enabled').focus();
},
methods: {
- ...mapActions('detail', [
+ ...mapActions('editNew', [
'initializeRelease',
'saveRelease',
'updateReleaseTitle',
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 32183e454c8..262b5614d65 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -20,7 +20,7 @@ export default {
ReleasesSort,
},
computed: {
- ...mapState('list', [
+ ...mapState('index', [
'documentationPath',
'illustrationPath',
'newReleasePath',
@@ -46,7 +46,7 @@ export default {
window.addEventListener('popstate', this.fetchReleases);
},
methods: {
- ...mapActions('list', {
+ ...mapActions('index', {
fetchReleasesStoreAction: 'fetchReleases',
}),
fetchReleases() {
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
index cfcb9f6978d..b9601428850 100644
--- a/app/assets/javascripts/releases/components/asset_links_form.vue
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -26,14 +26,14 @@ export default {
},
directives: { GlTooltip: GlTooltipDirective },
computed: {
- ...mapState('detail', ['release', 'releaseAssetsDocsPath']),
- ...mapGetters('detail', ['validationErrors']),
+ ...mapState('editNew', ['release', 'releaseAssetsDocsPath']),
+ ...mapGetters('editNew', ['validationErrors']),
},
created() {
this.ensureAtLeastOneLink();
},
methods: {
- ...mapActions('detail', [
+ ...mapActions('editNew', [
'addEmptyAssetLink',
'updateAssetLinkUrl',
'updateAssetLinkName',
diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
index 7d024c47fb9..13cbf95b9af 100644
--- a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
+++ b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
@@ -7,13 +7,13 @@ export default {
name: 'ReleasesPaginationGraphql',
components: { GlKeysetPagination },
computed: {
- ...mapState('list', ['graphQlPageInfo']),
+ ...mapState('index', ['graphQlPageInfo']),
showPagination() {
return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage;
},
},
methods: {
- ...mapActions('list', ['fetchReleases']),
+ ...mapActions('index', ['fetchReleases']),
onPrev(before) {
historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
this.fetchReleases({ before });
diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
index 24abb0f4498..5e97a5a0450 100644
--- a/app/assets/javascripts/releases/components/releases_pagination_rest.vue
+++ b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
@@ -7,10 +7,10 @@ export default {
name: 'ReleasesPaginationRest',
components: { TablePagination },
computed: {
- ...mapState('list', ['restPageInfo']),
+ ...mapState('index', ['restPageInfo']),
},
methods: {
- ...mapActions('list', ['fetchReleases']),
+ ...mapActions('index', ['fetchReleases']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
this.fetchReleases({ page });
diff --git a/app/assets/javascripts/releases/components/releases_sort.vue b/app/assets/javascripts/releases/components/releases_sort.vue
index c8e6e0e4996..4988904a2cd 100644
--- a/app/assets/javascripts/releases/components/releases_sort.vue
+++ b/app/assets/javascripts/releases/components/releases_sort.vue
@@ -10,7 +10,7 @@ export default {
GlSortingItem,
},
computed: {
- ...mapState('list', {
+ ...mapState('index', {
orderBy: (state) => state.sorting.orderBy,
sort: (state) => state.sorting.sort,
}),
@@ -26,7 +26,7 @@ export default {
},
},
methods: {
- ...mapActions('list', ['setSorting']),
+ ...mapActions('index', ['setSorting']),
onDirectionChange() {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
this.setSorting({ sort });
diff --git a/app/assets/javascripts/releases/components/tag_field.vue b/app/assets/javascripts/releases/components/tag_field.vue
index ed8d6e62926..f4c0fd5e9ce 100644
--- a/app/assets/javascripts/releases/components/tag_field.vue
+++ b/app/assets/javascripts/releases/components/tag_field.vue
@@ -9,7 +9,7 @@ export default {
TagFieldNew,
},
computed: {
- ...mapGetters('detail', ['isExistingRelease']),
+ ...mapGetters('editNew', ['isExistingRelease']),
},
};
</script>
diff --git a/app/assets/javascripts/releases/components/tag_field_existing.vue b/app/assets/javascripts/releases/components/tag_field_existing.vue
index 3345bbecf6e..11945fbaf3d 100644
--- a/app/assets/javascripts/releases/components/tag_field_existing.vue
+++ b/app/assets/javascripts/releases/components/tag_field_existing.vue
@@ -8,7 +8,7 @@ export default {
name: 'TagFieldExisting',
components: { GlFormGroup, GlFormInput, FormFieldContainer },
computed: {
- ...mapState('detail', ['release']),
+ ...mapState('editNew', ['release']),
inputId() {
return uniqueId('tag-name-input-');
},
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 21360a5c6cb..9df646ca798 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -27,8 +27,8 @@ export default {
};
},
computed: {
- ...mapState('detail', ['projectId', 'release', 'createFrom']),
- ...mapGetters('detail', ['validationErrors']),
+ ...mapState('editNew', ['projectId', 'release', 'createFrom']),
+ ...mapGetters('editNew', ['validationErrors']),
tagName: {
get() {
return this.release.tagName;
@@ -62,7 +62,7 @@ export default {
},
},
methods: {
- ...mapActions('detail', ['updateReleaseTagName', 'updateCreateFrom']),
+ ...mapActions('editNew', ['updateReleaseTagName', 'updateCreateFrom']),
markInputAsDirty() {
this.isInputDirty = true;
},
diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js
index 1232d55847b..fad0451ceef 100644
--- a/app/assets/javascripts/releases/mount_edit.js
+++ b/app/assets/javascripts/releases/mount_edit.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
-import createDetailModule from './stores/modules/detail';
+import createEditNewModule from './stores/modules/edit_new';
Vue.use(Vuex);
@@ -11,7 +11,7 @@ export default () => {
const store = createStore({
modules: {
- detail: createDetailModule(el.dataset),
+ editNew: createEditNewModule(el.dataset),
},
});
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index a9538cbc9e5..0b453467c13 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import ReleaseListApp from './components/app_index.vue';
+import ReleaseIndexApp from './components/app_index.vue';
import createStore from './stores';
-import createListModule from './stores/modules/list';
+import createIndexModule from './stores/modules/index';
Vue.use(Vuex);
@@ -13,7 +13,7 @@ export default () => {
el,
store: createStore({
modules: {
- list: createListModule(el.dataset),
+ index: createIndexModule(el.dataset),
},
featureFlags: {
graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
@@ -21,6 +21,6 @@ export default () => {
graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
},
}),
- render: (h) => h(ReleaseListApp),
+ render: (h) => h(ReleaseIndexApp),
});
};
diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js
index d85f4cf77d5..b358a27f06d 100644
--- a/app/assets/javascripts/releases/mount_new.js
+++ b/app/assets/javascripts/releases/mount_new.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
-import createDetailModule from './stores/modules/detail';
+import createEditNewModule from './stores/modules/edit_new';
Vue.use(Vuex);
@@ -11,7 +11,7 @@ export default () => {
const store = createStore({
modules: {
- detail: createDetailModule(el.dataset),
+ editNew: createEditNewModule(el.dataset),
},
});
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 8dc2083dd2b..8dc2083dd2b 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
diff --git a/app/assets/javascripts/releases/stores/modules/detail/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index 831037c8861..831037c8861 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
diff --git a/app/assets/javascripts/releases/stores/modules/detail/index.js b/app/assets/javascripts/releases/stores/modules/edit_new/index.js
index e1b7e69accc..e1b7e69accc 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/index.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/index.js
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index 1b2f5f33f02..1b2f5f33f02 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index cf282f9ab2c..cf282f9ab2c 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
index 315d07ac664..315d07ac664 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js
index f1add54626a..f1add54626a 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/index/actions.js
diff --git a/app/assets/javascripts/releases/stores/modules/list/index.js b/app/assets/javascripts/releases/stores/modules/index/index.js
index d5ca191153a..d5ca191153a 100644
--- a/app/assets/javascripts/releases/stores/modules/list/index.js
+++ b/app/assets/javascripts/releases/stores/modules/index/index.js
diff --git a/app/assets/javascripts/releases/stores/modules/list/mutation_types.js b/app/assets/javascripts/releases/stores/modules/index/mutation_types.js
index 669168efb88..669168efb88 100644
--- a/app/assets/javascripts/releases/stores/modules/list/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/index/mutation_types.js
diff --git a/app/assets/javascripts/releases/stores/modules/list/mutations.js b/app/assets/javascripts/releases/stores/modules/index/mutations.js
index e1aaa2e2a19..e1aaa2e2a19 100644
--- a/app/assets/javascripts/releases/stores/modules/list/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/index/mutations.js
diff --git a/app/assets/javascripts/releases/stores/modules/list/state.js b/app/assets/javascripts/releases/stores/modules/index/state.js
index 164a496d450..164a496d450 100644
--- a/app/assets/javascripts/releases/stores/modules/list/state.js
+++ b/app/assets/javascripts/releases/stores/modules/index/state.js
diff --git a/app/assets/stylesheets/page_bundles/alert_management_settings.scss b/app/assets/stylesheets/page_bundles/alert_management_settings.scss
index fb7c1602cba..ed2707ffbcd 100644
--- a/app/assets/stylesheets/page_bundles/alert_management_settings.scss
+++ b/app/assets/stylesheets/page_bundles/alert_management_settings.scss
@@ -6,7 +6,6 @@ $stroke-size: 1px;
@include gl-relative;
@include gl-w-full;
height: $stroke-size;
- @include gl-display-inline-block;
background-color: var(--gray-400, $gray-400);
min-width: $gl-spacing-scale-5;
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 307386e2c01..be709ff9d09 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -219,10 +219,6 @@
}
}
}
-
- .link-commit {
- color: var(--blue-600, $blue-600);
- }
}
.build-sidebar {
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index e6809316274..13ab542b5d9 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -15,6 +15,8 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
+ ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
+
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
# Android repo (15) + some extra backup.
@@ -84,8 +86,6 @@ class Namespace < ApplicationRecord
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
- before_save :ensure_delayed_project_removal_assigned_to_namespace_settings, if: :delayed_project_removal_changed?
-
scope :for_user, -> { where('type IS NULL') }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
@@ -408,13 +408,6 @@ class Namespace < ApplicationRecord
private
- def ensure_delayed_project_removal_assigned_to_namespace_settings
- return if Feature.disabled?(:migrate_delayed_project_removal, default_enabled: true)
-
- self.namespace_settings || build_namespace_settings
- namespace_settings.delayed_project_removal = delayed_project_removal
- end
-
def all_projects_with_pages
if all_projects.pages_metadata_not_migrated.exists?
Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation(
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 50844403d7f..04f778268d6 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -8,7 +8,7 @@ class NamespaceSetting < ApplicationRecord
before_validation :normalize_default_branch_name
- NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
+ NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal].freeze
self.primary_key = :namespace_id
diff --git a/app/models/raw_usage_data.rb b/app/models/raw_usage_data.rb
index 06cd4ad3f6c..6fe3b26b58b 100644
--- a/app/models/raw_usage_data.rb
+++ b/app/models/raw_usage_data.rb
@@ -4,7 +4,7 @@ class RawUsageData < ApplicationRecord
validates :payload, presence: true
validates :recorded_at, presence: true, uniqueness: true
- def update_sent_at!
- self.update_column(:sent_at, Time.current)
+ def update_version_metadata!(usage_data_id:)
+ self.update_columns(sent_at: Time.current, version_usage_data_id_value: usage_data_id)
end
end
diff --git a/app/services/groups/base_service.rb b/app/services/groups/base_service.rb
index 019cd047ae9..d6589970951 100644
--- a/app/services/groups/base_service.rb
+++ b/app/services/groups/base_service.rb
@@ -10,6 +10,18 @@ module Groups
private
+ def handle_namespace_settings
+ settings_params = params.slice(*::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS)
+
+ return if settings_params.empty?
+
+ ::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS.each do |nsp|
+ params.delete(nsp)
+ end
+
+ ::NamespaceSettings::UpdateService.new(current_user, group, settings_params).execute
+ end
+
def remove_unallowed_params
# overridden in EE
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 3ead2323588..da9afe86e36 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -11,7 +11,10 @@ module Groups
remove_unallowed_params
set_visibility_level
- @group = Group.new(params)
+ @group = Group.new(params.except(*::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS))
+
+ @group.build_namespace_settings
+ handle_namespace_settings
after_build_hook(@group, params)
@@ -33,7 +36,6 @@ module Groups
Group.transaction do
if @group.save
@group.add_owner(current_user)
- @group.create_namespace_settings unless @group.namespace_settings
Service.create_from_active_default_integrations(@group, :group_id)
OnboardingProgress.onboard(@group)
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 84385f5da25..ff369d01efc 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -46,18 +46,6 @@ module Groups
private
- def handle_namespace_settings
- settings_params = params.slice(*::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS)
-
- return if settings_params.empty?
-
- ::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS.each do |nsp|
- params.delete(nsp)
- end
-
- ::NamespaceSettings::UpdateService.new(current_user, group, settings_params).execute
- end
-
def valid_path_change_with_npm_packages?
return true unless group.packages_feature_enabled?
return true if params[:path].blank?
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 8ab1193b04f..d628b1ea7c7 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -35,7 +35,13 @@ class SubmitUsagePingService
raise SubmissionError.new("Unsuccessful response code: #{response.code}") unless response.success?
- raw_usage_data.update_sent_at! if raw_usage_data
+ version_usage_data_id = response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
+
+ unless version_usage_data_id.is_a?(Integer) && version_usage_data_id > 0
+ raise SubmissionError.new("Invalid usage_data_id in response: #{version_usage_data_id}")
+ end
+
+ raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
store_metrics(response)
end
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index abfac0a92ce..d07ac101139 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -6,11 +6,11 @@
%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _('Alerts')
+ = _('Alert integrations')
%button.btn.js-settings-toggle{ type: 'button' }
= _('Expand')
%p
= _('Display alerts from all your monitoring tools directly within GitLab.')
- = link_to _('More information'), help_page_path('operations/incident_management/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('operations/incident_management/integrations.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-alerts-settings{ data: alerts_settings_data }
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 8ccf14463c7..1bfb5247d04 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -43,7 +43,7 @@
.text-center
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: _('New issue'), type: :issues, with_feature_enabled: 'issues'
- - else
+ - elsif show_new_issue_link?(@project)
= link_to _('New issue'), button_path, class: 'gl-button btn btn-confirm', id: 'new_issue_link'
- if show_import_button
diff --git a/changelogs/unreleased/292698-save-usage_data_id-raw_data_id-we-receive-from-versions-in-raw_usa.yml b/changelogs/unreleased/292698-save-usage_data_id-raw_data_id-we-receive-from-versions-in-raw_usa.yml
new file mode 100644
index 00000000000..8e978beaf90
--- /dev/null
+++ b/changelogs/unreleased/292698-save-usage_data_id-raw_data_id-we-receive-from-versions-in-raw_usa.yml
@@ -0,0 +1,5 @@
+---
+title: Save usage_data_id from versions app in raw_usage_data
+merge_request: 54738
+author:
+type: added
diff --git a/changelogs/unreleased/296882-standalone-shard-replica-settings.yml b/changelogs/unreleased/296882-standalone-shard-replica-settings.yml
new file mode 100644
index 00000000000..f087684a626
--- /dev/null
+++ b/changelogs/unreleased/296882-standalone-shard-replica-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Allow setting the shard/replica separately for standalone indexes
+merge_request: 56344
+author:
+type: changed
diff --git a/changelogs/unreleased/300827-integration-form-cleanup-part-3.yml b/changelogs/unreleased/300827-integration-form-cleanup-part-3.yml
new file mode 100644
index 00000000000..e202729f4ad
--- /dev/null
+++ b/changelogs/unreleased/300827-integration-form-cleanup-part-3.yml
@@ -0,0 +1,5 @@
+---
+title: Alerts integration form UX cleanup
+merge_request: 55892
+author:
+type: changed
diff --git a/changelogs/unreleased/dblessing_ignore_namespace_delayed_project_removal.yml b/changelogs/unreleased/dblessing_ignore_namespace_delayed_project_removal.yml
new file mode 100644
index 00000000000..49ea816bdb5
--- /dev/null
+++ b/changelogs/unreleased/dblessing_ignore_namespace_delayed_project_removal.yml
@@ -0,0 +1,5 @@
+---
+title: Move usage of delayed_project_removal to namespace settings
+merge_request: 56397
+author:
+type: changed
diff --git a/changelogs/unreleased/make-ci_runner_builds_queue_on_replicas-default.yml b/changelogs/unreleased/make-ci_runner_builds_queue_on_replicas-default.yml
new file mode 100644
index 00000000000..e559339e0f7
--- /dev/null
+++ b/changelogs/unreleased/make-ci_runner_builds_queue_on_replicas-default.yml
@@ -0,0 +1,5 @@
+---
+title: Make `ci_runner_builds_queue_on_replicas` default
+merge_request: 57484
+author:
+type: performance
diff --git a/changelogs/unreleased/pb-fix-duration-for-skipped-pipeline.yml b/changelogs/unreleased/pb-fix-duration-for-skipped-pipeline.yml
new file mode 100644
index 00000000000..d155a0c4905
--- /dev/null
+++ b/changelogs/unreleased/pb-fix-duration-for-skipped-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Show skipped duration state for all skipped pipelines
+merge_request: 57242
+author:
+type: changed
diff --git a/config/feature_flags/development/ci_runner_builds_queue_on_replicas.yml b/config/feature_flags/development/ci_runner_builds_queue_on_replicas.yml
index 2c8269ed030..7e930ce5a10 100644
--- a/config/feature_flags/development/ci_runner_builds_queue_on_replicas.yml
+++ b/config/feature_flags/development/ci_runner_builds_queue_on_replicas.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325723
milestone: '13.10'
type: development
group: group::continuous integration
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/other_storage_tab.yml b/config/feature_flags/development/other_storage_tab.yml
new file mode 100644
index 00000000000..8ce4848f98b
--- /dev/null
+++ b/config/feature_flags/development/other_storage_tab.yml
@@ -0,0 +1,8 @@
+---
+name: other_storage_tab
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57121
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325967
+milestone: '13.11'
+type: development
+group: group::fulfillment
+default_enabled: false
diff --git a/config/helpers/check_frontend_integration_env.js b/config/helpers/check_frontend_integration_env.js
new file mode 100644
index 00000000000..38393c89445
--- /dev/null
+++ b/config/helpers/check_frontend_integration_env.js
@@ -0,0 +1,37 @@
+const fs = require('fs');
+const isESLint = require('./is_eslint');
+
+const GRAPHQL_SCHEMA_PATH = 'tmp/tests/graphql/gitlab_schema.graphql';
+const GRAPHQL_SCHEMA_JOB = 'bundle exec rake gitlab:graphql:schema:dump';
+
+const shouldIgnoreWarnings = JSON.parse(process.env.GL_IGNORE_WARNINGS || '0');
+
+const failCheck = (message) => {
+ console.error(message);
+
+ if (!shouldIgnoreWarnings) {
+ process.exit(1);
+ }
+};
+
+const checkGraphqlSchema = () => {
+ if (!fs.existsSync(GRAPHQL_SCHEMA_PATH)) {
+ const message = `
+ERROR: Expected to find "${GRAPHQL_SCHEMA_PATH}" but file does not exist. Try running:
+
+ ${GRAPHQL_SCHEMA_JOB}
+`;
+
+ failCheck(message);
+ }
+};
+
+const check = () => {
+ if (isESLint(module)) {
+ return;
+ }
+
+ checkGraphqlSchema();
+};
+
+module.exports = check;
diff --git a/db/migrate/20210219211845_add_version_usage_data_id_to_raw_usage_data.rb b/db/migrate/20210219211845_add_version_usage_data_id_to_raw_usage_data.rb
new file mode 100644
index 00000000000..1b49fdd98bd
--- /dev/null
+++ b/db/migrate/20210219211845_add_version_usage_data_id_to_raw_usage_data.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddVersionUsageDataIdToRawUsageData < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :raw_usage_data, :version_usage_data_id_value, :bigint
+ end
+end
diff --git a/db/migrate/20210317100520_create_elastic_index_settings.rb b/db/migrate/20210317100520_create_elastic_index_settings.rb
new file mode 100644
index 00000000000..61c1cbb3518
--- /dev/null
+++ b/db/migrate/20210317100520_create_elastic_index_settings.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class CreateElasticIndexSettings < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table_with_constraints :elastic_index_settings do |t|
+ t.timestamps_with_timezone null: false
+ t.integer :number_of_replicas, null: false, default: 1, limit: 2
+ t.integer :number_of_shards, null: false, default: 5, limit: 2
+ t.text :alias_name, null: false
+
+ t.text_limit :alias_name, 255
+ t.index :alias_name, unique: true
+ end
+ end
+
+ def down
+ drop_table :elastic_index_settings
+ end
+end
diff --git a/db/migrate/20210324131727_migrate_elastic_index_settings.rb b/db/migrate/20210324131727_migrate_elastic_index_settings.rb
new file mode 100644
index 00000000000..4dcfc6cf952
--- /dev/null
+++ b/db/migrate/20210324131727_migrate_elastic_index_settings.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class MigrateElasticIndexSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ ALIAS_NAME = [Rails.application.class.module_parent_name.downcase, Rails.env].join('-')
+
+ class ElasticIndexSetting < ActiveRecord::Base
+ end
+
+ class ApplicationSetting < ActiveRecord::Base
+ end
+
+ def up
+ setting = ApplicationSetting.first
+ number_of_replicas = setting&.elasticsearch_replicas || 1
+ number_of_shards = setting&.elasticsearch_shards || 5
+
+ return if ElasticIndexSetting.exists?(alias_name: ALIAS_NAME)
+
+ ElasticIndexSetting.create!(
+ alias_name: ALIAS_NAME,
+ number_of_replicas: number_of_replicas,
+ number_of_shards: number_of_shards
+ )
+ end
+
+ def down
+ ElasticIndexSetting.where(alias_name: ALIAS_NAME).delete_all
+ end
+end
diff --git a/db/schema_migrations/20210219211845 b/db/schema_migrations/20210219211845
new file mode 100644
index 00000000000..ad45eee91b5
--- /dev/null
+++ b/db/schema_migrations/20210219211845
@@ -0,0 +1 @@
+b58f2853d7a2d9a821198f69c5913d290404a4961410dd66d256eefc7ecf1026 \ No newline at end of file
diff --git a/db/schema_migrations/20210317100520 b/db/schema_migrations/20210317100520
new file mode 100644
index 00000000000..f75c67143c2
--- /dev/null
+++ b/db/schema_migrations/20210317100520
@@ -0,0 +1 @@
+54c701451c305ffdead2a9019cf07adae835c5873025caa1f32169f5ae83bf5d \ No newline at end of file
diff --git a/db/schema_migrations/20210324131727 b/db/schema_migrations/20210324131727
new file mode 100644
index 00000000000..85ea4aad1ee
--- /dev/null
+++ b/db/schema_migrations/20210324131727
@@ -0,0 +1 @@
+e0fab4d950a5be032f823160b1805c44262f9e3d233dc76cd108483a5b92896b \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index dd8c2a2d0f6..78f959a71f1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12270,6 +12270,25 @@ CREATE SEQUENCE draft_notes_id_seq
ALTER SEQUENCE draft_notes_id_seq OWNED BY draft_notes.id;
+CREATE TABLE elastic_index_settings (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ number_of_replicas smallint DEFAULT 1 NOT NULL,
+ number_of_shards smallint DEFAULT 5 NOT NULL,
+ alias_name text NOT NULL,
+ CONSTRAINT check_c30005c325 CHECK ((char_length(alias_name) <= 255))
+);
+
+CREATE SEQUENCE elastic_index_settings_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE elastic_index_settings_id_seq OWNED BY elastic_index_settings.id;
+
CREATE TABLE elastic_reindexing_subtasks (
id bigint NOT NULL,
elastic_reindexing_task_id bigint NOT NULL,
@@ -16913,7 +16932,8 @@ CREATE TABLE raw_usage_data (
updated_at timestamp with time zone NOT NULL,
recorded_at timestamp with time zone NOT NULL,
sent_at timestamp with time zone,
- payload jsonb NOT NULL
+ payload jsonb NOT NULL,
+ version_usage_data_id_value bigint
);
CREATE SEQUENCE raw_usage_data_id_seq
@@ -19276,6 +19296,8 @@ ALTER TABLE ONLY dora_daily_metrics ALTER COLUMN id SET DEFAULT nextval('dora_da
ALTER TABLE ONLY draft_notes ALTER COLUMN id SET DEFAULT nextval('draft_notes_id_seq'::regclass);
+ALTER TABLE ONLY elastic_index_settings ALTER COLUMN id SET DEFAULT nextval('elastic_index_settings_id_seq'::regclass);
+
ALTER TABLE ONLY elastic_reindexing_subtasks ALTER COLUMN id SET DEFAULT nextval('elastic_reindexing_subtasks_id_seq'::regclass);
ALTER TABLE ONLY elastic_reindexing_tasks ALTER COLUMN id SET DEFAULT nextval('elastic_reindexing_tasks_id_seq'::regclass);
@@ -20511,6 +20533,9 @@ ALTER TABLE ONLY dora_daily_metrics
ALTER TABLE ONLY draft_notes
ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY elastic_index_settings
+ ADD CONSTRAINT elastic_index_settings_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY elastic_reindexing_subtasks
ADD CONSTRAINT elastic_reindexing_subtasks_pkey PRIMARY KEY (id);
@@ -22470,6 +22495,8 @@ CREATE INDEX index_draft_notes_on_discussion_id ON draft_notes USING btree (disc
CREATE INDEX index_draft_notes_on_merge_request_id ON draft_notes USING btree (merge_request_id);
+CREATE UNIQUE INDEX index_elastic_index_settings_on_alias_name ON elastic_index_settings USING btree (alias_name);
+
CREATE INDEX index_elastic_reindexing_subtasks_on_elastic_reindexing_task_id ON elastic_reindexing_subtasks USING btree (elastic_reindexing_task_id);
CREATE UNIQUE INDEX index_elastic_reindexing_tasks_on_in_progress ON elastic_reindexing_tasks USING btree (in_progress) WHERE in_progress;
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 8181c13aca0..7407babfe9b 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -647,6 +647,9 @@ The default is 100MB.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16610) in GitLab 12.7.
+NOTE:
+Only GitLab admin users will be able to view and override the **Maximum size of Pages** setting.
+
To override the global maximum pages size for a specific project:
1. Go to your project's **Settings > Pages** page.
diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md
index 17115f65b90..c4a8e2d40cc 100644
--- a/doc/api/epic_issues.md
+++ b/doc/api/epic_issues.md
@@ -14,6 +14,11 @@ results in a `404` status code.
Epics are available only in GitLab [Premium and higher](https://about.gitlab.com/pricing/).
If the Epics feature is not available, a `403` status code is returned.
+## Epic Issues pagination
+
+API results [are paginated](README.md#pagination). Requests that return
+multiple issues default to returning 20 results at a time.
+
## List issues for an epic
Gets all issues that are assigned to an epic and the authenticated user has access to.
diff --git a/doc/api/packages/conan.md b/doc/api/packages/conan.md
new file mode 100644
index 00000000000..88ed2524173
--- /dev/null
+++ b/doc/api/packages/conan.md
@@ -0,0 +1,614 @@
+---
+stage: Package
+group: Package
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Conan API
+
+This is the API documentation for [Conan Packages](../../user/packages/conan_repository/index.md).
+
+WARNING:
+This API is used by the [Conan package manager client](https://docs.conan.io/en/latest/)
+and is generally not meant for manual consumption.
+
+For instructions on how to upload and install Conan packages from the GitLab
+package registry, see the [Conan package registry documentation](../../user/packages/conan_repository/index.md).
+
+NOTE:
+These endpoints do not adhere to the standard API authentication methods.
+See each route for details on how credentials are expected to be passed.
+
+## Route prefix
+
+There are two sets of identical routes that each make requests in different scopes:
+
+- Use the instance-level prefix to make requests in the entire GitLab instance's scope.
+- Use the project-level prefix to make requests in a single project's scope.
+
+The examples in this document all use the instance-level prefix.
+
+### Instance-level
+
+```plaintext
+/packages/conan/v1
+```
+
+When using the instance-level routes, be aware that there is a [naming
+restriction](../../user/packages/conan_repository/index.md#package-recipe-naming-convention-for-instance-remotes)
+for Conan recipes.
+
+### Project-level
+
+```plaintext
+ /projects/:id/packages/conan/v1`
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | string | yes | The project ID or full project path. |
+
+## Ping
+
+> Introduced in GitLab 12.2.
+
+Ping the GitLab Conan repository to verify availability:
+
+```plaintext
+GET <route-prefix>/ping
+```
+
+```shell
+curl "https://gitlab.example.com/api/v4/packages/conan/v1/ping"
+```
+
+Example response:
+
+```json
+""
+```
+
+## Search
+
+> Introduced in GitLab 12.4.
+
+Search the instance for Conan packages by name:
+
+```plaintext
+GET <route-prefix>/conans/search
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `q` | string | yes | Search query. You can use `*` as a wildcard. |
+
+```shell
+curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/packages/conan/v1/conans/search?q=Hello*"
+```
+
+Example response:
+
+```json
+{
+ "results": [
+ "Hello/0.1@foo+conan_test_prod/beta",
+ "Hello/0.1@foo+conan_test_prod/stable",
+ "Hello/0.2@foo+conan_test_prod/beta",
+ "Hello/0.3@foo+conan_test_prod/beta",
+ "Hello/0.1@foo+conan-reference-test/stable",
+ "HelloWorld/0.1@baz+conan-reference-test/beta"
+ "hello-world/0.4@buz+conan-test/alpha"
+ ]
+}
+```
+
+## Authenticate
+
+> Introduced in GitLab 12.2.
+
+Returns a JWT to be used for Conan requests in a Bearer header:
+
+```shell
+"Authorization: Bearer <token>
+```
+
+The Conan package manager client automatically uses this token.
+
+```plaintext
+GET <route-prefix>/users/authenticate
+```
+
+```shell
+curl --user <username>:<personal_access_token> "https://gitlab.example.com/packages/conan/v1/users/authenticate
+```
+
+Example response:
+
+```shell
+eyJhbGciOiJIUzI1NiIiheR5cCI6IkpXVCJ9.eyJhY2Nlc3NfdG9rZW4iOjMyMTQyMzAsqaVzZXJfaWQiOjQwNTkyNTQsImp0aSI6IjdlNzBiZTNjLWFlNWQtNDEyOC1hMmIyLWZiOThhZWM0MWM2OSIsImlhd3r1MTYxNjYyMzQzNSwibmJmIjoxNjE2NjIzNDMwLCJleHAiOjE2MTY2MjcwMzV9.QF0Q3ZIB2GW5zNKyMSIe0HIFOITjEsZEioR-27Rtu7E
+```
+
+## Check Credentials
+
+> Introduced in GitLab 12.4.
+
+Checks the validity of Basic Auth credentials or a Conan JWT generated from [`/authenticate`](#authenticate).
+
+```plaintext
+GET <route-prefix>/users/check_credentials
+```
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/users/check_credentials
+```
+
+Example response:
+
+```shell
+ok
+```
+
+## Recipe Snapshot
+
+> Introduced in GitLab 12.5.
+
+This returns the snapshot of the recipe files for the specified Conan recipe. The snapshot is a list
+of filenames with their associated md5 hash.
+
+```plaintext
+GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable"
+```
+
+Example response:
+
+```json
+{
+ "conan_sources.tgz": "eadf19b33f4c3c7e113faabf26e76277",
+ "conanfile.py": "25e55b96a28f81a14ba8e8a8c99eeace",
+ "conanmanifest.txt": "5b6fd77a2ba14303ce4cdb08c87e82ab"
+}
+```
+
+## Package Snapshot
+
+> Introduced in GitLab 12.5.
+
+This returns the snapshot of the package files for the specified Conan recipe with the specified
+Conan reference. The snapshot is a list of filenames with their associated md5 hash.
+
+```plaintext
+GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f"
+```
+
+Example response:
+
+```json
+{
+ "conan_package.tgz": "749b29bdf72587081ca03ec033ee59dc",
+ "conaninfo.txt": "32859d737fe84e6a7ccfa4d64dc0d1f2",
+ "conanmanifest.txt": "a86b398e813bd9aa111485a9054a2301"
+}
+```
+
+## Recipe Manifest
+
+> Introduced in GitLab 12.5.
+
+The manifest is a list of recipe filenames with their associated download URLs.
+
+```plaintext
+GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/digest
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/digest"
+```
+
+Example response:
+
+```json
+{
+ "conan_sources.tgz": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conan_sources.tgz",
+ "conanfile.py": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py",
+ "conanmanifest.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanmanifest.txt"
+}
+```
+
+The URLs in the response have the same route prefix used to request them. If you request them with
+the project-level route, the returned URLs contain `/projects/:id`.
+
+## Package Manifest
+
+> Introduced in GitLab 12.5.
+
+The manifest is a list of package filenames with their associated download URLs.
+
+```plaintext
+GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference/digest
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/digest"
+```
+
+Example response:
+
+```json
+{
+ "conan_package.tgz": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conan_package.tgz",
+ "conaninfo.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt",
+ "conanmanifest.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conanmanifest.txt"
+}
+```
+
+The URLs in the response have the same route prefix used to request them. If you request them with
+the project-level route, the returned URLs contain `/projects/:id`.
+
+## Recipe Download URLs
+
+> Introduced in GitLab 12.5.
+
+Returns a list of recipe filenames with their associated download URLs.
+This is the same payload as the [recipe manifest](#recipe-manifest) endpoint.
+
+```plaintext
+GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/download_urls
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/digest"
+```
+
+Example response:
+
+```json
+{
+ "conan_sources.tgz": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conan_sources.tgz",
+ "conanfile.py": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py",
+ "conanmanifest.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanmanifest.txt"
+}
+```
+
+The URLs in the response have the same route prefix used to request them. If you request them with
+the project-level route, the returned URLs contain `/projects/:id`.
+
+## Package Download URLs
+
+> Introduced in GitLab 12.5.
+
+Returns a list of package filenames with their associated download URLs.
+This is the same payload as the [package manifest](#package-manifest) endpoint.
+
+```plaintext
+GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/download_urls"
+```
+
+Example response:
+
+```json
+{
+ "conan_package.tgz": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conan_package.tgz",
+ "conaninfo.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt",
+ "conanmanifest.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conanmanifest.txt"
+}
+```
+
+The URLs in the response have the same route prefix used to request them. If you request them with
+the project-level route, the returned URLs contain `/projects/:id`.
+
+## Recipe Upload URLs
+
+> Introduced in GitLab 12.5.
+
+Given a list of recipe filenames and file sizes, a list of URLs to upload each file is returned.
+
+```plaintext
+POST <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/upload_urls
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+
+Example request JSON payload:
+
+```json
+{
+ "conanfile.py": 410,
+ "conanmanifest.txt": 130
+}
+```
+
+```shell
+curl --request POST \
+ --header "Authorization: Bearer <authenticate_token>" \
+ --header "Content-Type: application/json" \
+ --data '{"conanfile.py":410,"conanmanifest.txt":130}' \
+ "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/upload_urls"
+```
+
+Example response:
+
+```json
+{
+ "conanfile.py": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py",
+ "conanmanifest.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanmanifest.txt"
+}
+```
+
+The URLs in the response have the same route prefix used to request them. If you request them with
+the project-level route, the returned URLs contain `/projects/:id`.
+
+## Package Upload URLs
+
+> Introduced in GitLab 12.5.
+
+Given a list of package filenames and file sizes, a list of URLs to upload each file is returned.
+
+```plaintext
+POST <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
+
+Example request JSON payload:
+
+```json
+{
+ "conan_package.tgz": 5412,
+ "conanmanifest.txt": 130,
+ "conaninfo.txt": 210
+ }
+```
+
+```shell
+curl --request POST \
+ --header "Authorization: Bearer <authenticate_token>" \
+ --header "Content-Type: application/json" \
+ --data '{"conan_package.tgz":5412,"conanmanifest.txt":130,"conaninfo.txt":210}'
+ "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/upload_urls"
+```
+
+Example response:
+
+```json
+{
+ "conan_package.tgz": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/package/103f6067a947f366ef91fc1b7da351c588d1827f/0/conan_package.tgz",
+ "conanmanifest.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/package/103f6067a947f366ef91fc1b7da351c588d1827f/0/conanmanifest.txt",
+ "conaninfo.txt": "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/package/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt"
+}
+```
+
+The URLs in the response have the same route prefix used to request them. If you request them with
+the project-level route, the returned URLs contain `/projects/:id`.
+
+## Download a Recipe file
+
+> Introduced in GitLab 12.6.
+
+Download a recipe file to the package registry. You must use a download URL that the
+[recipe download URLs endpoint](#recipe-download-urls)
+returned.
+
+```shell
+GET packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/export/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `recipe_revision` | string | yes | Revision of the recipe. GitLab does not yet support Conan revisions, so the default value of `0` is always used. |
+| `file_name` | string | yes | The name and file extension of the requested file. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py"
+```
+
+You can also write the output to a file by using:
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py" >> conanfile.py
+```
+
+This example writes to `conanfile.py` in the current directory.
+
+## Upload a Recipe file
+
+> Introduced in GitLab 12.6.
+
+Upload a recipe file to the package registry. You must use an upload URL that the
+[recipe upload URLs endpoint](#recipe-upload-urls)
+returned.
+
+```shell
+GET packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/export/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `recipe_revision` | string | yes | Revision of the recipe. GitLab does not yet support Conan revisions, so the default value of `0` is always used. |
+| `file_name` | string | yes | The name and file extension of the requested file. |
+
+Provide the file context in the request body:
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" \
+ --upload-file path/to/conanfile.py \
+ "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py"
+```
+
+## Download a Package file
+
+> Introduced in GitLab 12.6.
+
+Download a package file to the package registry. You must use a download URL that the
+[package download URLs endpoint](#package-download-urls)
+returned.
+
+```shell
+GET packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `recipe_revision` | string | yes | Revision of the recipe. GitLab does not yet support Conan revisions, so the default value of `0` is always used. |
+| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
+| `package_revision` | string | yes | Revision of the package. GitLab does not yet support Conan revisions, so the default value of `0` is always used. |
+| `file_name` | string | yes | The name and file extension of the requested file. |
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt"
+```
+
+You can also write the output to a file by using:
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt" >> conaninfo.txt
+```
+
+This example writes to `conaninfo.txt` in the current directory.
+
+## Upload a Package file
+
+> Introduced in GitLab 12.6.
+
+Upload a package file to the package registry. You must use an upload URL that the
+[package upload URLs endpoint](#package-upload-urls)
+returned.
+
+```shell
+GET packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+| `recipe_revision` | string | yes | Revision of the recipe. GitLab does not yet support Conan revisions, so the default value of `0` is always used. |
+| `conan_package_reference` | string | yes | Reference hash of a Conan package. Conan generates this value. |
+| `package_revision` | string | yes | Revision of the package. GitLab does not yet support Conan revisions, so the default value of `0` is always used. |
+| `file_name` | string | yes | The name and file extension of the requested file. |
+
+Provide the file context in the request body:
+
+```shell
+curl --header "Authorization: Bearer <authenticate_token>" \
+ --upload-file path/to/conaninfo.txt \
+ "https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/packages/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt"
+```
+
+## Delete a Package (delete a Conan recipe)
+
+> Introduced in GitLab 12.5.
+
+Delete the Conan recipe and package files from the registry:
+
+```plaintext
+DELETE <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `package_name` | string | yes | Name of a package. |
+| `package_version` | string | yes | Version of a package. |
+| `package_username` | string | yes | Conan username of a package. This is the `+`-separated full path of your project. |
+| `package_channel` | string | yes | Channel of a package. |
+
+```shell
+curl --request DELETE --header "Authorization: Bearer <authenticate_token>" "https://gitlab.example.com/api/v4/packages/conan/v1/conans/my-package/1.0/my-group+my-project/stable"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "project_id": 123,
+ "created_at": "2020-08-19T13:17:28.655Z",
+ "updated_at": "2020-08-19T13:17:28.655Z",
+ "name": "my-package",
+ "version": "1.0",
+ "package_type": "conan",
+ "creator_id": null,
+ "status": "default"
+}
+```
diff --git a/doc/api/packages/maven.md b/doc/api/packages/maven.md
new file mode 100644
index 00000000000..d03c9be3060
--- /dev/null
+++ b/doc/api/packages/maven.md
@@ -0,0 +1,124 @@
+---
+stage: Package
+group: Package
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Maven API
+
+This is the API documentation for [Maven Packages](../../user/packages/maven_repository/index.md).
+
+WARNING:
+This API is used by the [Maven package manager client](https://maven.apache.org/)
+and is generally not meant for manual consumption.
+
+For instructions on how to upload and install Maven packages from the GitLab
+package registry, see the [Maven package registry documentation](../../user/packages/maven_repository/index.md).
+
+NOTE:
+These endpoints do not adhere to the standard API authentication methods.
+See [Maven package registry documentation](../../user/packages/maven_repository/index.md)
+for details on which headers and token types are supported.
+
+## Download a package file at the instance-level
+
+> Introduced in GitLab 11.6.
+
+Download a Maven package file:
+
+```plaintext
+GET packages/maven/*path/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `path` | string | yes | The Maven package path, in the format `<groupId>/<artifactId>/<version>`. Replace any `.` in the `groupId` with `/`. |
+| `file_name` | string | yes | The name of the Maven package file. |
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.jar"
+```
+
+To write the output to file:
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.jar" >> mypkg-1.0-SNAPSHOT.jar
+```
+
+This writes the downloaded file to `mypkg-1.0-SNAPSHOT.jar` in the current directory.
+
+## Download a package file at the group-level
+
+> Introduced in GitLab 11.7.
+
+Download a Maven package file:
+
+```plaintext
+GET groups/:id/-/packages/maven/*path/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `path` | string | yes | The Maven package path, in the format `<groupId>/<artifactId>/<version>`. Replace any `.` in the `groupId` with `/`. |
+| `file_name` | string | yes | The name of the Maven package file. |
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/groups/1/-/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.jar"
+```
+
+To write the output to file:
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/groups/1/-/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.jar" >> mypkg-1.0-SNAPSHOT.jar
+```
+
+This writes the downloaded file to `mypkg-1.0-SNAPSHOT.jar` in the current directory.
+
+## Download a package file at the project-level
+
+> Introduced in GitLab 11.3.
+
+Download a Maven package file:
+
+```plaintext
+GET projects/:id/packages/maven/*path/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `path` | string | yes | The Maven package path, in the format `<groupId>/<artifactId>/<version>`. Replace any `.` in the `groupId` with `/`. |
+| `file_name` | string | yes | The name of the Maven package file. |
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.jar"
+```
+
+To write the output to file:
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.jar" >> mypkg-1.0-SNAPSHOT.jar
+```
+
+This writes the downloaded file to `mypkg-1.0-SNAPSHOT.jar` in the current directory.
+
+## Upload a package file
+
+> Introduced in GitLab 11.3.
+
+Upload a Maven package file:
+
+```plaintext
+PUT projects/:id/packages/maven/*path/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `path` | string | yes | The Maven package path, in the format `<groupId>/<artifactId>/<version>`. Replace any `.` in the `groupId` with `/`. |
+| `file_name` | string | yes | The name of the Maven package file. |
+
+```shell
+curl --request PUT \
+ --upload-file path/to/mypkg-1.0-SNAPSHOT.pom \
+ --header "Private-Token: <personal_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/packages/maven/foo/bar/baz/mypkg-1.0-SNAPSHOT.pom"
+```
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index af1f2f57e0e..a519f07da77 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -18,6 +18,9 @@ remote and authenticate with it.
Then you can run `conan` commands and publish your package to the
Package Registry.
+For documentation of the specific API endpoints that the Conan package manager
+client uses, see the [Conan API documentation](../../../api/packages/conan.md).
+
## Build a Conan package
This section explains how to install Conan and build a package for your C/C++
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 17a0e4a5784..7104f4a02ed 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -12,6 +12,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Publish [Maven](https://maven.apache.org) artifacts in your project's Package Registry.
Then, install the packages whenever you need to use them as a dependency.
+For documentation of the specific API endpoints that the Maven package manager
+client uses, see the [Maven API documentation](../../../api/packages/maven.md).
+
## Build a Maven package
This section explains how to install Maven and build a package.
diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb
index 91df1b0d98f..e8e46202c44 100644
--- a/generator_templates/active_record/migration/create_table_migration.rb
+++ b/generator_templates/active_record/migration/create_table_migration.rb
@@ -14,8 +14,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
# migration requires downtime.
# DOWNTIME_REASON = ''
- # When using the methods "add_concurrent_index", "remove_concurrent_index" or
- # "add_column_with_default" you must disable the use of transactions
+ # When using the methods "add_concurrent_index" or "remove_concurrent_index"
+ # you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb
index 2a57edba65e..1a1ab7eba94 100644
--- a/generator_templates/active_record/migration/migration.rb
+++ b/generator_templates/active_record/migration/migration.rb
@@ -15,8 +15,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
# migration requires downtime.
# DOWNTIME_REASON = ''
- # When using the methods "add_concurrent_index", "remove_concurrent_index" or
- # "add_column_with_default" you must disable the use of transactions
+ # When using the methods "add_concurrent_index" or "remove_concurrent_index"
+ # you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb
index b36cf877f0e..c76d318c5d6 100644
--- a/generator_templates/rails/post_deployment_migration/migration.rb
+++ b/generator_templates/rails/post_deployment_migration/migration.rb
@@ -9,8 +9,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
DOWNTIME = false
- # When using the methods "add_concurrent_index", "remove_concurrent_index" or
- # "add_column_with_default" you must disable the use of transactions
+ # When using the methods "add_concurrent_index" or "remove_concurrent_index"
+ # you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
diff --git a/jest.config.integration.js b/jest.config.integration.js
index d85e14fe218..92296fb751e 100644
--- a/jest.config.integration.js
+++ b/jest.config.integration.js
@@ -1,5 +1,8 @@
+const checkEnvironment = require('./config/helpers/check_frontend_integration_env');
const baseConfig = require('./jest.config.base');
+checkEnvironment();
+
module.exports = {
...baseConfig('spec/frontend_integration', {
moduleNameMapper: {
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index ef5221a8042..3d24b4d53a4 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -107,6 +107,8 @@ module Gitlab
entry.data = data.join
entry unless entry.oid.blank?
+ rescue GRPC::NotFound
+ nil
end
def tree_entries(repository, revision, path, recursive)
diff --git a/lib/gitlab/usage_data_non_sql_metrics.rb b/lib/gitlab/usage_data_non_sql_metrics.rb
new file mode 100644
index 00000000000..6bd74a2a993
--- /dev/null
+++ b/lib/gitlab/usage_data_non_sql_metrics.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class UsageDataNonSqlMetrics < UsageData
+ SQL_METRIC_DEFAULT = -3
+
+ class << self
+ def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
+ SQL_METRIC_DEFAULT
+ end
+
+ def distinct_count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
+ SQL_METRIC_DEFAULT
+ end
+
+ def estimate_batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil)
+ SQL_METRIC_DEFAULT
+ end
+
+ def sum(relation, column, batch_size: nil, start: nil, finish: nil)
+ SQL_METRIC_DEFAULT
+ end
+
+ def histogram(relation, column, buckets:, bucket_size: buckets.size)
+ SQL_METRIC_DEFAULT
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ad5990fb869..478c35c81e0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2731,6 +2731,9 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert integrations"
+msgstr ""
+
msgid "AlertManagement|Acknowledged"
msgstr ""
@@ -2893,6 +2896,12 @@ msgstr ""
msgid "AlertMappingBuilder|Title is a required field for alerts in GitLab. Should the payload field you specified not be available, specifiy which field we should use instead. "
msgstr ""
+msgid "AlertSettings|A URL and authorization key have been created for your integration. You will need them to setup a webhook and authorize your endpoint to send alerts to GitLab."
+msgstr ""
+
+msgid "AlertSettings|A webhook URL and authorization key will be generated for the integration. Both will be visible after saving the integration in the “View credentials” tab."
+msgstr ""
+
msgid "AlertSettings|Add new integration"
msgstr ""
@@ -2911,10 +2920,10 @@ msgstr ""
msgid "AlertSettings|Edit payload"
msgstr ""
-msgid "AlertSettings|Enter integration name"
+msgid "AlertSettings|Enable integration"
msgstr ""
-msgid "AlertSettings|External Prometheus"
+msgid "AlertSettings|Enter integration name"
msgstr ""
msgid "AlertSettings|HTTP Endpoint"
@@ -2923,25 +2932,28 @@ msgstr ""
msgid "AlertSettings|If you edit the payload, the stored mapping will be reset, and you'll need to re-map the fields."
msgstr ""
-msgid "AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click \"parse payload fields\" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration."
+msgid "AlertSettings|If you intend to create a custom mapping, provide an example payload from your monitoring tool and click the \"parse payload fields\" button to continue. The sample payload is required for completing the custom mapping; if you want to skip the mapping step, progress straight to saving your integration."
msgstr ""
msgid "AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations."
msgstr ""
+msgid "AlertSettings|Integration successfully saved"
+msgstr ""
+
msgid "AlertSettings|Name integration"
msgstr ""
-msgid "AlertSettings|Parse payload for custom mapping"
+msgid "AlertSettings|Parse payload fields"
msgstr ""
msgid "AlertSettings|Proceed with editing"
msgstr ""
-msgid "AlertSettings|Prometheus API base URL"
+msgid "AlertSettings|Prometheus"
msgstr ""
-msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to create a custom mapping (optional)."
+msgid "AlertSettings|Prometheus API base URL"
msgstr ""
msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point."
@@ -2956,33 +2968,51 @@ msgstr ""
msgid "AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in."
msgstr ""
-msgid "AlertSettings|Sample alert payload (optional)"
+msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
msgstr ""
-msgid "AlertSettings|Sample payload has been parsed. You can now map the fields."
+msgid "AlertSettings|Save & create test alert"
msgstr ""
msgid "AlertSettings|Save integration"
msgstr ""
-msgid "AlertSettings|Select integration type"
+msgid "AlertSettings|Save integration & send"
msgstr ""
-msgid "AlertSettings|Send"
+msgid "AlertSettings|Select integration type"
msgstr ""
msgid "AlertSettings|Send test alert"
msgstr ""
+msgid "AlertSettings|Send without saving"
+msgstr ""
+
+msgid "AlertSettings|The default GitLab alert fields are listed below. If you choose to map your payload keys to GitLab's, please make a selection in the dropdowns below. You may also opt to leave the fields unmapped and move straight to saving your integration."
+msgstr ""
+
+msgid "AlertSettings|The form has unsaved changes"
+msgstr ""
+
+msgid "AlertSettings|The form has unsaved changes. How would you like to proceed?"
+msgstr ""
+
msgid "AlertSettings|URL cannot be blank and must start with http or https"
msgstr ""
+msgid "AlertSettings|URL is invalid."
+msgstr ""
+
msgid "AlertSettings|Utilize the URL and authorization key below to authorize Prometheus to send alerts to GitLab. Review the Prometheus documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
msgid "AlertSettings|Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
+msgid "AlertSettings|View URL and authorization key"
+msgstr ""
+
msgid "AlertSettings|View credentials"
msgstr ""
@@ -2992,9 +3022,6 @@ msgstr ""
msgid "AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated."
msgstr ""
-msgid "AlertSettings|Your integration was successfully updated."
-msgstr ""
-
msgid "AlertSettings|{ \"events\": [{ \"application\": \"Name of application\" }] }"
msgstr ""
@@ -3031,7 +3058,10 @@ msgstr ""
msgid "AlertsIntegrations|The integration has been successfully removed."
msgstr ""
-msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
+msgid "AlertsIntegrations|The integration has been successfully saved."
+msgstr ""
+
+msgid "AlertsIntegrations|The integration is currently inactive. Enable the integration to send the test alert."
msgstr ""
msgid "AlertsIntegrations|The integration token could not be reset. Please try again."
@@ -13930,6 +13960,12 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
+msgid "Geo|Nothing to synchronize"
+msgstr ""
+
+msgid "Geo|Number of %{title}"
+msgstr ""
+
msgid "Geo|Pending synchronization"
msgstr ""
@@ -13945,9 +13981,6 @@ msgstr ""
msgid "Geo|Primary site"
msgstr ""
-msgid "Geo|Progress Bar Placeholder"
-msgstr ""
-
msgid "Geo|Project"
msgstr ""
@@ -13960,6 +13993,9 @@ msgstr ""
msgid "Geo|Projects in certain storage shards"
msgstr ""
+msgid "Geo|Queued"
+msgstr ""
+
msgid "Geo|Redownload"
msgstr ""
@@ -20127,6 +20163,9 @@ msgstr ""
msgid "Name"
msgstr ""
+msgid "Name can't be blank"
+msgstr ""
+
msgid "Name has already been taken"
msgstr ""
@@ -27529,6 +27568,9 @@ msgstr ""
msgid "SelfMonitoring|Self monitoring project has been successfully deleted."
msgstr ""
+msgid "Send"
+msgstr ""
+
msgid "Send a single email notification to Owners and Maintainers for new alerts."
msgstr ""
@@ -32801,6 +32843,9 @@ msgstr ""
msgid "UsageQuota|Learn more about usage quotas"
msgstr ""
+msgid "UsageQuota|Other Storage"
+msgstr ""
+
msgid "UsageQuota|Packages"
msgstr ""
diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
index 60f2f776595..6675abd6b42 100644
--- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
+++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'Alert integrations settings form', :js do
it 'shows the alerts setting form title' do
page.within('#js-alert-management-settings') do
- expect(find('h4')).to have_content('Alerts')
+ expect(find('h4')).to have_content('Alert integrations')
end
end
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 7abbd207b24..81751d2123a 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -118,6 +118,50 @@ RSpec.describe 'Project fork' do
it_behaves_like 'fork button on project page'
it_behaves_like 'create fork page', 'Fork project'
+ context 'fork form', :js do
+ let(:group) { create(:group) }
+ let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+
+ def submit_form
+ select(group.name)
+ click_button 'Fork project'
+ end
+
+ it 'forks the project', :sidekiq_might_not_need_inline do
+ visit new_project_fork_path(project)
+ submit_form
+
+ expect(page).to have_content 'Forked from'
+ end
+
+ it 'shows the new forked project on the forks page' do
+ visit new_project_fork_path(project)
+ submit_form
+ wait_for_requests
+
+ visit project_forks_path(project)
+
+ page.within('.js-projects-list-holder') do
+ expect(page).to have_content("#{group.name} / #{project.name}")
+ end
+ end
+
+ it 'shows the filled in info forked project on the forks page' do
+ fork_name = 'some-name'
+ visit new_project_fork_path(project)
+ fill_in('fork-name', with: fork_name, fill_options: { clear: :backspace })
+ fill_in('fork-slug', with: fork_name, fill_options: { clear: :backspace })
+ submit_form
+ wait_for_requests
+
+ visit project_forks_path(project)
+
+ page.within('.js-projects-list-holder') do
+ expect(page).to have_content("#{group.name} / #{fork_name}")
+ end
+ end
+ end
+
context 'with fork_project_form feature flag disabled' do
before do
stub_feature_flags(fork_project_form: false)
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index 31b61781f45..019de35be0a 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe 'User searches project settings', :js do
visit project_settings_operations_path(project)
end
- it_behaves_like 'can search settings', 'Alerts', 'Error tracking'
+ it_behaves_like 'can search settings', 'Alert integrations', 'Error tracking'
end
context 'in Pages page' do
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
deleted file mode 100644
index 1f8429af7dd..00000000000
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
+++ /dev/null
@@ -1,524 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
-<form
- class="gl-mt-6"
->
- <div
- class="tabs gl-tabs"
- id="__BVID__6"
- >
- <!---->
- <div
- class=""
- >
- <ul
- class="nav gl-tabs-nav"
- id="__BVID__6__BV_tab_controls_"
- role="tablist"
- >
- <!---->
- <li
- class="nav-item"
- role="presentation"
- >
- <a
- aria-controls="__BVID__8"
- aria-posinset="1"
- aria-selected="true"
- aria-setsize="3"
- class="nav-link active gl-tab-nav-item gl-tab-nav-item-active gl-tab-nav-item-active-indigo"
- href="#"
- id="__BVID__8___BV_tab_button__"
- role="tab"
- target="_self"
- >
- Configure details
- </a>
- </li>
- <li
- class="nav-item"
- role="presentation"
- >
- <a
- aria-controls="__BVID__19"
- aria-disabled="true"
- aria-posinset="2"
- aria-selected="false"
- aria-setsize="3"
- class="nav-link disabled disabled gl-tab-nav-item"
- href="#"
- id="__BVID__19___BV_tab_button__"
- role="tab"
- tabindex="-1"
- target="_self"
- >
- View credentials
- </a>
- </li>
- <li
- class="nav-item"
- role="presentation"
- >
- <a
- aria-controls="__BVID__41"
- aria-disabled="true"
- aria-posinset="3"
- aria-selected="false"
- aria-setsize="3"
- class="nav-link disabled disabled gl-tab-nav-item"
- href="#"
- id="__BVID__41___BV_tab_button__"
- role="tab"
- tabindex="-1"
- target="_self"
- >
- Send test alert
- </a>
- </li>
- <!---->
- </ul>
- </div>
- <div
- class="tab-content gl-tab-content"
- id="__BVID__6__BV_tab_container_"
- >
- <transition-stub
- css="true"
- enteractiveclass=""
- enterclass=""
- entertoclass="show"
- leaveactiveclass=""
- leaveclass="show"
- leavetoclass=""
- mode="out-in"
- name=""
- >
- <div
- aria-hidden="false"
- aria-labelledby="__BVID__8___BV_tab_button__"
- class="tab-pane active"
- id="__BVID__8"
- role="tabpanel"
- style=""
- >
- <div
- class="form-group gl-form-group"
- id="integration-type"
- role="group"
- >
- <label
- class="d-block col-form-label"
- for="integration-type"
- id="integration-type__BV_label_"
- >
- 1.Select integration type
- </label>
- <div
- class="bv-no-focus-ring"
- >
- <select
- class="gl-form-select gl-max-w-full custom-select"
- id="__BVID__13"
- >
- <option
- value=""
- >
- Select integration type
- </option>
- <option
- value="HTTP"
- >
- HTTP Endpoint
- </option>
- <option
- value="PROMETHEUS"
- >
- External Prometheus
- </option>
- </select>
-
- <!---->
- <!---->
- <!---->
- <!---->
- </div>
- </div>
-
- <div
- class="gl-mt-3"
- >
- <!---->
-
- <label
- class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal"
- >
- <span
- class="gl-toggle-wrapper"
- >
- <span
- class="gl-toggle-label"
- data-testid="toggle-label"
- >
- Active
- </span>
-
- <!---->
-
- <button
- aria-label="Active"
- class="gl-toggle"
- role="switch"
- type="button"
- >
- <span
- class="toggle-icon"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="close-icon"
- >
- <use
- href="#close"
- />
- </svg>
- </span>
- </button>
- </span>
-
- <!---->
- </label>
-
- <!---->
-
- <!---->
- </div>
-
- <div
- class="gl-display-flex gl-justify-content-start gl-py-3"
- >
- <button
- class="btn js-no-auto-disable btn-confirm btn-md gl-button"
- data-testid="integration-form-submit"
- type="submit"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Save integration
-
- </span>
- </button>
-
- <button
- class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
- type="reset"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
- Cancel and close
- </span>
- </button>
- </div>
- </div>
- </transition-stub>
-
- <transition-stub
- css="true"
- enteractiveclass=""
- enterclass=""
- entertoclass="show"
- leaveactiveclass=""
- leaveclass="show"
- leavetoclass=""
- mode="out-in"
- name=""
- >
- <div
- aria-hidden="true"
- aria-labelledby="__BVID__19___BV_tab_button__"
- class="tab-pane disabled"
- id="__BVID__19"
- role="tabpanel"
- style="display: none;"
- >
- <span>
- Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the
- <a
- class="gl-link gl-display-inline-block"
- href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
- rel="noopener noreferrer"
- target="_blank"
- >
- GitLab documentation
- </a>
- to learn more about configuring your endpoint.
- </span>
-
- <fieldset
- class="form-group gl-form-group"
- id="integration-webhook"
- >
- <!---->
- <div
- class="bv-no-focus-ring"
- role="group"
- tabindex="-1"
- >
- <div
- class="gl-my-4"
- >
- <span
- class="gl-font-weight-bold"
- >
-
- Webhook URL
-
- </span>
-
- <div
- id="url"
- readonly="readonly"
- >
- <div
- class="input-group"
- role="group"
- >
- <!---->
- <!---->
-
- <input
- class="gl-form-input form-control"
- id="url"
- readonly="readonly"
- type="text"
- />
-
- <div
- class="input-group-append"
- >
- <button
- aria-label="Copy this value"
- class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
- data-clipboard-text=""
- title="Copy"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="copy-to-clipboard-icon"
- >
- <use
- href="#copy-to-clipboard"
- />
- </svg>
-
- <!---->
- </button>
- </div>
- <!---->
- </div>
- </div>
- </div>
-
- <div
- class="gl-my-4"
- >
- <span
- class="gl-font-weight-bold"
- >
-
- Authorization key
-
- </span>
-
- <div
- class="gl-mb-3"
- id="authorization-key"
- readonly="readonly"
- >
- <div
- class="input-group"
- role="group"
- >
- <!---->
- <!---->
-
- <input
- class="gl-form-input form-control"
- id="authorization-key"
- readonly="readonly"
- type="text"
- />
-
- <div
- class="input-group-append"
- >
- <button
- aria-label="Copy this value"
- class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
- data-clipboard-text=""
- title="Copy"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="copy-to-clipboard-icon"
- >
- <use
- href="#copy-to-clipboard"
- />
- </svg>
-
- <!---->
- </button>
- </div>
- <!---->
- </div>
- </div>
- </div>
- <!---->
- <!---->
- <!---->
- </div>
- </fieldset>
-
- <button
- class="btn btn-danger btn-md disabled gl-button"
- disabled="disabled"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Reset Key
-
- </span>
- </button>
-
- <button
- class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
- type="reset"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
- Cancel and close
- </span>
- </button>
-
- <!---->
- </div>
- </transition-stub>
-
- <transition-stub
- css="true"
- enteractiveclass=""
- enterclass=""
- entertoclass="show"
- leaveactiveclass=""
- leaveclass="show"
- leavetoclass=""
- mode="out-in"
- name=""
- >
- <div
- aria-hidden="true"
- aria-labelledby="__BVID__41___BV_tab_button__"
- class="tab-pane disabled"
- id="__BVID__41"
- role="tabpanel"
- style="display: none;"
- >
- <fieldset
- class="form-group gl-form-group"
- id="test-integration"
- >
- <!---->
- <div
- class="bv-no-focus-ring"
- role="group"
- tabindex="-1"
- >
- <span>
- Provide an example payload from the monitoring tool you intend to integrate with. This will allow you to send an alert to an active GitLab alerting point.
- </span>
-
- <textarea
- class="gl-form-input gl-form-textarea gl-my-3 form-control is-valid"
- id="test-payload"
- placeholder="{ \\"events\\": [{ \\"application\\": \\"Name of application\\" }] }"
- style="resize: none; overflow-y: scroll;"
- wrap="soft"
- />
- <!---->
- <!---->
- <!---->
- </div>
- </fieldset>
-
- <button
- class="btn js-no-auto-disable btn-confirm btn-md gl-button"
- data-testid="send-test-alert"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Send
-
- </span>
- </button>
-
- <button
- class="btn gl-ml-3 js-no-auto-disable btn-default btn-md gl-button"
- type="reset"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
- Cancel and close
- </span>
- </button>
- </div>
- </transition-stub>
- <!---->
- </div>
- </div>
-</form>
-`;
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index d2dcff14432..00f2fa60360 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -1,5 +1,7 @@
import { GlForm, GlFormSelect, GlFormInput, GlToggle, GlFormTextarea, GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
@@ -13,43 +15,44 @@ describe('AlertsSettingsForm', () => {
const mockToastShow = jest.fn();
const createComponent = ({ data = {}, props = {}, multiIntegrations = true } = {}) => {
- wrapper = mount(AlertsSettingsForm, {
- data() {
- return { ...data };
- },
- propsData: {
- loading: false,
- canAddIntegration: true,
- ...props,
- },
- provide: {
- ...defaultAlertSettingsConfig,
- multiIntegrations,
- },
- mocks: {
- $apollo: {
- query: jest.fn(),
+ wrapper = extendedWrapper(
+ mount(AlertsSettingsForm, {
+ data() {
+ return { ...data };
},
- $toast: {
- show: mockToastShow,
+ propsData: {
+ loading: false,
+ canAddIntegration: true,
+ ...props,
},
- },
- });
+ provide: {
+ ...defaultAlertSettingsConfig,
+ multiIntegrations,
+ },
+ mocks: {
+ $apollo: {
+ query: jest.fn(),
+ },
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ }),
+ );
};
const findForm = () => wrapper.findComponent(GlForm);
const findSelect = () => wrapper.findComponent(GlFormSelect);
const findFormFields = () => wrapper.findAllComponents(GlFormInput);
const findFormToggle = () => wrapper.findComponent(GlToggle);
- const findSamplePayloadSection = () => wrapper.find('[data-testid="sample-payload-section"]');
- const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
+ const findSamplePayloadSection = () => wrapper.findByTestId('sample-payload-section');
const findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
- const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
- const findMultiSupportText = () =>
- wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
- const findJsonTestSubmit = () => wrapper.find(`[data-testid="send-test-alert"]`);
+
+ const findSubmitButton = () => wrapper.findByTestId('integration-form-submit');
+ const findMultiSupportText = () => wrapper.findByTestId('multi-integrations-not-supported');
+ const findJsonTestSubmit = () => wrapper.findByTestId('send-test-alert');
const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`);
- const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`);
+ const findActionBtn = () => wrapper.findByTestId('payload-action-btn');
const findTabs = () => wrapper.findAllComponents(GlTab);
afterEach(() => {
@@ -74,10 +77,6 @@ describe('AlertsSettingsForm', () => {
createComponent();
});
- it('renders the initial template', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('render the initial form with only an integration type dropdown', () => {
expect(findForm().exists()).toBe(true);
expect(findSelect().exists()).toBe(true);
@@ -151,29 +150,28 @@ describe('AlertsSettingsForm', () => {
findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
findForm().trigger('submit');
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- {
- type: typeSet.http,
- variables: {
- name: integrationName,
- active: true,
- payloadAttributeMappings: sampleMapping,
- payloadExample: '{}',
- },
+ expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({
+ type: typeSet.http,
+ variables: {
+ name: integrationName,
+ active: true,
+ payloadAttributeMappings: sampleMapping,
+ payloadExample: '{}',
},
- ]);
+ });
});
it('update', () => {
createComponent({
data: {
- selectedIntegration: typeSet.http,
- currentIntegration: { id: '1', name: 'Test integration pre' },
+ integrationForm: { id: '1', name: 'Test integration pre', type: typeSet.http },
+ currentIntegration: { id: '1' },
},
props: {
loading: false,
},
});
+
const updatedIntegrationName = 'Test integration post';
enableIntegration(0, updatedIntegrationName);
@@ -181,21 +179,16 @@ describe('AlertsSettingsForm', () => {
expect(submitBtn.exists()).toBe(true);
expect(submitBtn.text()).toBe('Save integration');
- findForm().trigger('submit');
-
- expect(wrapper.emitted('update-integration')[0]).toEqual(
- expect.arrayContaining([
- {
- type: typeSet.http,
- variables: {
- name: updatedIntegrationName,
- active: true,
- payloadAttributeMappings: [],
- payloadExample: '{}',
- },
- },
- ]),
- );
+ submitBtn.trigger('click');
+ expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({
+ type: typeSet.http,
+ variables: {
+ name: updatedIntegrationName,
+ active: true,
+ payloadAttributeMappings: [],
+ payloadExample: '{}',
+ },
+ });
});
});
@@ -211,16 +204,17 @@ describe('AlertsSettingsForm', () => {
findForm().trigger('submit');
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl, active: true } },
- ]);
+ expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({
+ type: typeSet.prometheus,
+ variables: { apiUrl, active: true },
+ });
});
it('update', () => {
createComponent({
data: {
- selectedIntegration: typeSet.prometheus,
- currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
+ integrationForm: { id: '1', apiUrl: 'https://test-pre.com', type: typeSet.prometheus },
+ currentIntegration: { id: '1' },
},
props: {
loading: false,
@@ -236,9 +230,10 @@ describe('AlertsSettingsForm', () => {
findForm().trigger('submit');
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl, active: true } },
- ]);
+ expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({
+ type: typeSet.prometheus,
+ variables: { apiUrl, active: true },
+ });
});
});
});
@@ -247,7 +242,6 @@ describe('AlertsSettingsForm', () => {
beforeEach(() => {
createComponent({
data: {
- selectedIntegration: typeSet.http,
currentIntegration: { id: '1', name: 'Test' },
active: true,
},
@@ -262,7 +256,7 @@ describe('AlertsSettingsForm', () => {
await findJsonTextArea().setValue('Invalid JSON');
jest.runAllTimers();
- await wrapper.vm.$nextTick();
+ await nextTick();
const jsonTestSubmit = findJsonTestSubmit();
expect(jsonTestSubmit.exists()).toBe(true);
@@ -275,7 +269,7 @@ describe('AlertsSettingsForm', () => {
await findJsonTextArea().setValue('{ "value": "value" }');
jest.runAllTimers();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findJsonTestSubmit().props('disabled')).toBe(false);
});
});
@@ -283,14 +277,13 @@ describe('AlertsSettingsForm', () => {
describe('Test payload section for HTTP integration', () => {
const validSamplePayload = JSON.stringify(alertFields);
const emptySamplePayload = '{}';
-
beforeEach(() => {
createComponent({
+ multiIntegrations: true,
data: {
+ integrationForm: { type: typeSet.http },
currentIntegration: {
- type: typeSet.http,
- payloadExample: validSamplePayload,
- payloadAttributeMappings: [],
+ payloadExample: emptySamplePayload,
},
active: false,
resetPayloadAndMappingConfirmed: false,
@@ -300,25 +293,25 @@ describe('AlertsSettingsForm', () => {
});
describe.each`
- active | resetPayloadAndMappingConfirmed | disabled
- ${true} | ${true} | ${undefined}
- ${false} | ${true} | ${'disabled'}
- ${true} | ${false} | ${'disabled'}
- ${false} | ${false} | ${'disabled'}
- `('', ({ active, resetPayloadAndMappingConfirmed, disabled }) => {
+ payload | resetPayloadAndMappingConfirmed | disabled
+ ${validSamplePayload} | ${true} | ${undefined}
+ ${emptySamplePayload} | ${true} | ${undefined}
+ ${validSamplePayload} | ${false} | ${'disabled'}
+ ${emptySamplePayload} | ${false} | ${undefined}
+ `('', ({ payload, resetPayloadAndMappingConfirmed, disabled }) => {
const payloadResetMsg = resetPayloadAndMappingConfirmed
? 'was confirmed'
: 'was not confirmed';
const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled';
- const activeState = active ? 'active' : 'not active';
+ const validPayloadMsg = payload === emptySamplePayload ? 'not valid' : 'valid';
- it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => {
+ it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and payload is ${validPayloadMsg}`, async () => {
wrapper.setData({
- selectedIntegration: typeSet.http,
- active,
+ currentIntegration: { payloadExample: payload },
resetPayloadAndMappingConfirmed,
});
- await wrapper.vm.$nextTick();
+
+ await nextTick();
expect(findSamplePayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(
disabled,
);
@@ -329,9 +322,9 @@ describe('AlertsSettingsForm', () => {
describe.each`
resetPayloadAndMappingConfirmed | payloadExample | caption
${false} | ${validSamplePayload} | ${'Edit payload'}
- ${true} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
- ${true} | ${validSamplePayload} | ${'Parse payload for custom mapping'}
- ${false} | ${emptySamplePayload} | ${'Parse payload for custom mapping'}
+ ${true} | ${emptySamplePayload} | ${'Parse payload fields'}
+ ${true} | ${validSamplePayload} | ${'Parse payload fields'}
+ ${false} | ${emptySamplePayload} | ${'Parse payload fields'}
`('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
const payloadResetMsg = resetPayloadAndMappingConfirmed
@@ -340,16 +333,12 @@ describe('AlertsSettingsForm', () => {
it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
wrapper.setData({
- selectedIntegration: typeSet.http,
currentIntegration: {
payloadExample,
- type: typeSet.http,
- active: true,
- payloadAttributeMappings: [],
},
resetPayloadAndMappingConfirmed,
});
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findActionBtn().text()).toBe(caption);
});
});
@@ -358,7 +347,6 @@ describe('AlertsSettingsForm', () => {
describe('Parsing payload', () => {
beforeEach(() => {
wrapper.setData({
- selectedIntegration: typeSet.http,
resetPayloadAndMappingConfirmed: true,
});
});
@@ -398,11 +386,12 @@ describe('AlertsSettingsForm', () => {
${true} | ${false} | ${1} | ${false}
${false} | ${true} | ${1} | ${false}
`('', ({ alertFieldsProvided, multiIntegrations, integrationOption, visible }) => {
- const visibleMsg = visible ? 'is rendered' : 'is not rendered';
- const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided';
+ const visibleMsg = visible ? 'rendered' : 'not rendered';
+ const alertFieldsMsg = alertFieldsProvided ? 'provided' : 'not provided';
const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
+ const multiIntegrationsEnabled = multiIntegrations ? 'enabled' : 'not enabled';
- it(`${visibleMsg} when integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => {
+ it(`is ${visibleMsg} when multiIntegrations are ${multiIntegrationsEnabled}, integration type is ${integrationType} and alert fields are ${alertFieldsMsg}`, async () => {
createComponent({
multiIntegrations,
props: {
@@ -411,8 +400,63 @@ describe('AlertsSettingsForm', () => {
});
await selectOptionAtIndex(integrationOption);
- expect(findMappingBuilderSection().exists()).toBe(visible);
+ expect(findMappingBuilder().exists()).toBe(visible);
});
});
});
+
+ describe('Form validation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should not be able to submit when no integration type is selected', () => {
+ selectOptionAtIndex(0);
+
+ expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ });
+
+ it('should not be able to submit when HTTP integration form is invalid', () => {
+ selectOptionAtIndex(1);
+
+ expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ });
+
+ it('should be able to submit when HTTP integration form is valid', async () => {
+ await selectOptionAtIndex(1);
+ await findFormFields().at(0).setValue('Name');
+ expect(findSubmitButton().attributes('disabled')).toBe(undefined);
+ });
+
+ it('should not be able to submit when Prometheus integration form is invalid', () => {
+ selectOptionAtIndex(2);
+
+ expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ });
+
+ it('should be able to submit when Prometheus integration form is valid', async () => {
+ await selectOptionAtIndex(2);
+ await findFormFields().at(0).setValue('http://valid.url');
+ expect(findSubmitButton().attributes('disabled')).toBe(undefined);
+ });
+
+ it('should be able to submit when form is dirty', async () => {
+ wrapper.setData({
+ currentIntegration: { type: typeSet.http, name: 'Existing integration' },
+ });
+ await nextTick();
+ await findFormFields().at(0).setValue('Updated name');
+
+ expect(findSubmitButton().attributes('disabled')).toBe(undefined);
+ });
+
+ it('should not be able to submit when form is pristine', async () => {
+ wrapper.setData({
+ currentIntegration: { type: typeSet.http, name: 'Existing integration' },
+ });
+ await nextTick();
+
+ expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ });
+ });
});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 77fac6dd022..dd8ce838dfd 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -1,18 +1,18 @@
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
-import AlertsSettingsWrapper, {
- i18n,
-} from '~/alerts_settings/components/alerts_settings_wrapper.vue';
-import { typeSet } from '~/alerts_settings/constants';
+import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
+import { typeSet, i18n } from '~/alerts_settings/constants';
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
@@ -27,10 +27,12 @@ import {
RESET_INTEGRATION_TOKEN_ERROR,
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
+ INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
import createFlash, { FLASH_TYPES } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
import {
createHttpVariables,
updateHttpVariables,
@@ -81,8 +83,9 @@ describe('AlertsSettingsWrapper', () => {
const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon);
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
- const findAddIntegrationBtn = () => wrapper.find('[data-testid="add-integration-btn"]');
+ const findAddIntegrationBtn = () => wrapper.findByTestId('add-integration-btn');
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
+ const findAlert = () => wrapper.findComponent(GlAlert);
async function destroyHttpIntegration(localWrapper) {
await jest.runOnlyPendingTimers();
@@ -94,32 +97,34 @@ describe('AlertsSettingsWrapper', () => {
}
async function awaitApolloDomMock() {
- await wrapper.vm.$nextTick(); // kick off the DOM update
+ await nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
- await wrapper.vm.$nextTick(); // kick off the DOM update for flash
+ await nextTick(); // kick off the DOM update for flash
}
const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
- wrapper = mount(AlertsSettingsWrapper, {
- data() {
- return { ...data };
- },
- provide: {
- ...defaultAlertSettingsConfig,
- ...provide,
- },
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- query: jest.fn(),
- queries: {
- integrations: {
- loading,
+ wrapper = extendedWrapper(
+ mount(AlertsSettingsWrapper, {
+ data() {
+ return { ...data };
+ },
+ provide: {
+ ...defaultAlertSettingsConfig,
+ ...provide,
+ },
+ mocks: {
+ $apollo: {
+ mutate: jest.fn(),
+ query: jest.fn(),
+ queries: {
+ integrations: {
+ loading,
+ },
},
},
},
- },
- });
+ }),
+ );
};
function createComponentWithApollo({
@@ -200,20 +205,29 @@ describe('AlertsSettingsWrapper', () => {
loading: false,
});
});
- it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
+
+ describe('Create', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { httpIntegrationCreate: { integration: { id: '1' }, errors: [] } },
+ });
+ findAlertsSettingsForm().vm.$emit('create-new-integration', {
+ type: typeSet.http,
+ variables: createHttpVariables,
+ });
});
- findAlertsSettingsForm().vm.$emit('create-new-integration', {
- type: typeSet.http,
- variables: createHttpVariables,
+
+ it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: createHttpIntegrationMutation,
+ update: expect.anything(),
+ variables: createHttpVariables,
+ });
});
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: createHttpIntegrationMutation,
- update: expect.anything(),
- variables: createHttpVariables,
+ it('shows success alert', () => {
+ expect(findAlert().exists()).toBe(true);
});
});
@@ -334,13 +348,29 @@ describe('AlertsSettingsWrapper', () => {
expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
});
- it('shows an error alert when integration test payload fails ', async () => {
- const mock = new AxiosMockAdapter(axios);
- mock.onPost(/(.*)/).replyOnce(403);
- return wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
+ describe('Test alert failure', () => {
+ let mock;
+ beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+ });
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows an error alert when integration test payload is invalid ', async () => {
+ mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
+ await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createFlash).toHaveBeenCalledTimes(1);
- mock.restore();
+ });
+
+ it('shows an error alert when integration is not activated ', async () => {
+ mock.onPost(/(.*)/).replyOnce(httpStatusCodes.FORBIDDEN);
+ await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
+ expect(createFlash).toHaveBeenCalledWith({
+ message: INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
+ });
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
@@ -354,7 +384,7 @@ describe('AlertsSettingsWrapper', () => {
loading: false,
});
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValueOnce({});
findIntegrationsList().vm.$emit('edit-integration', updateHttpVariables);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateCurrentHttpIntegrationMutation,
@@ -372,7 +402,7 @@ describe('AlertsSettingsWrapper', () => {
loading: false,
});
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
findIntegrationsList().vm.$emit('edit-integration', updatePrometheusVariables);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateCurrentPrometheusIntegrationMutation,
@@ -414,7 +444,7 @@ describe('AlertsSettingsWrapper', () => {
createComponentWithApollo();
await jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findIntegrations()).toHaveLength(4);
});
@@ -426,7 +456,7 @@ describe('AlertsSettingsWrapper', () => {
expect(destroyIntegrationHandler).toHaveBeenCalled();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findIntegrations()).toHaveLength(3);
});
diff --git a/spec/frontend/jobs/components/commit_block_spec.js b/spec/frontend/jobs/components/commit_block_spec.js
index 13261317b48..8a6d48cecb8 100644
--- a/spec/frontend/jobs/components/commit_block_spec.js
+++ b/spec/frontend/jobs/components/commit_block_spec.js
@@ -1,89 +1,70 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/jobs/components/commit_block.vue';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CommitBlock from '~/jobs/components/commit_block.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('Commit block', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
- const props = {
- commit: {
- short_id: '1f0fb84f',
- id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
- commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
- title: 'Update README.md',
- },
- mergeRequest: {
- iid: '!21244',
- path: 'merge_requests/21244',
- },
- isLastBlock: true,
+ const commit = {
+ short_id: '1f0fb84f',
+ id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
+ commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
+ title: 'Update README.md',
+ };
+
+ const mergeRequest = {
+ iid: '!21244',
+ path: 'merge_requests/21244',
+ };
+
+ const findCommitSha = () => wrapper.findByTestId('commit-sha');
+ const findLinkSha = () => wrapper.findByTestId('link-commit');
+
+ const mountComponent = (props) => {
+ wrapper = extendedWrapper(
+ shallowMount(CommitBlock, {
+ propsData: {
+ commit,
+ ...props,
+ },
+ }),
+ );
};
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('pipeline short sha', () => {
+ describe('without merge request', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- ...props,
- });
+ mountComponent();
});
it('renders pipeline short sha link', () => {
- expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(
- props.commit.commit_path,
- );
-
- expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(
- props.commit.short_id,
- );
+ expect(findCommitSha().attributes('href')).toBe(commit.commit_path);
+ expect(findCommitSha().text()).toBe(commit.short_id);
});
it('renders clipboard button', () => {
- expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(
- props.commit.id,
- );
+ expect(wrapper.findComponent(ClipboardButton).attributes('text')).toBe(commit.id);
});
- });
-
- describe('with merge request', () => {
- it('renders merge request link and reference', () => {
- vm = mountComponent(Component, {
- ...props,
- });
-
- expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(
- props.mergeRequest.path,
- );
- expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(
- `!${props.mergeRequest.iid}`,
- );
+ it('renders git commit title', () => {
+ expect(wrapper.text()).toContain(commit.title);
});
- });
- describe('without merge request', () => {
it('does not render merge request', () => {
- const copyProps = { ...props };
- delete copyProps.mergeRequest;
-
- vm = mountComponent(Component, {
- ...copyProps,
- });
-
- expect(vm.$el.querySelector('.js-link-commit')).toBeNull();
+ expect(findLinkSha().exists()).toBe(false);
});
});
- describe('git commit title', () => {
- it('renders git commit title', () => {
- vm = mountComponent(Component, {
- ...props,
- });
+ describe('with merge request', () => {
+ it('renders merge request link and reference', () => {
+ mountComponent({ mergeRequest });
- expect(vm.$el.textContent).toContain(props.commit.title);
+ expect(findLinkSha().attributes('href')).toBe(mergeRequest.path);
+ expect(findLinkSha().text()).toBe(`!${mergeRequest.iid}`);
});
});
});
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index 412fb085e0b..5c174c436fe 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -5,41 +5,6 @@ import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => {
let wrapper;
- const mutlipleStages = {
- manual_actions: [
- {
- name: 'deploy_job',
- path: '/root/one-stage-manual/-/jobs/1930/play',
- playable: true,
- scheduled: false,
- },
- ],
- stages: [
- {
- name: 'deploy',
- },
- {
- name: 'qa',
- },
- ],
- };
-
- const singleStageManual = {
- manual_actions: [
- {
- name: 'deploy_job',
- path: '/root/one-stage-manual/-/jobs/1930/play',
- playable: true,
- scheduled: false,
- },
- ],
- stages: [
- {
- name: 'deploy',
- },
- ],
- };
-
const createComponent = (props = {}) => {
wrapper = shallowMount(TimeAgo, {
propsData: {
@@ -82,7 +47,7 @@ describe('Timeago component', () => {
describe('without duration', () => {
beforeEach(() => {
- createComponent({ ...singleStageManual, duration: 0, finished_at: '' });
+ createComponent({ duration: 0, finished_at: '' });
});
it('should not render duration and timer svg', () => {
@@ -107,7 +72,7 @@ describe('Timeago component', () => {
describe('without finishedTime', () => {
beforeEach(() => {
- createComponent({ ...singleStageManual, duration: 0, finished_at: '' });
+ createComponent({ duration: 0, finished_at: '' });
});
it('should not render time and calendar icon', () => {
@@ -126,7 +91,6 @@ describe('Timeago component', () => {
'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
({ durationTime, finishedAtTime, shouldShow }) => {
createComponent({
- ...mutlipleStages,
duration: durationTime,
finished_at: finishedAtTime,
});
@@ -138,24 +102,13 @@ describe('Timeago component', () => {
});
describe('skipped', () => {
- it.each`
- durationTime | finishedAtTime | shouldShow
- ${10} | ${'2017-04-26T12:40:23.277Z'} | ${false}
- ${10} | ${''} | ${false}
- ${0} | ${'2017-04-26T12:40:23.277Z'} | ${false}
- ${0} | ${''} | ${true}
- `(
- 'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
- ({ durationTime, finishedAtTime, shouldShow }) => {
- createComponent({
- ...singleStageManual,
- duration: durationTime,
- finished_at: finishedAtTime,
- });
+ it('should show skipped if pipeline was skipped', () => {
+ createComponent({
+ status: { label: 'skipped' },
+ });
- expect(findSkipped().exists()).toBe(shouldShow);
- expect(findInProgress().exists()).toBe(false);
- },
- );
+ expect(findSkipped().exists()).toBe(true);
+ expect(findInProgress().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 1e55ab8f9e4..106e6a3a012 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -50,7 +50,7 @@ describe('Release edit/new component', () => {
merge(
{
modules: {
- detail: {
+ editNew: {
namespaced: true,
actions,
state,
@@ -168,7 +168,7 @@ describe('Release edit/new component', () => {
await factory({
store: {
modules: {
- detail: {
+ editNew: {
getters: {
isExistingRelease: () => false,
},
@@ -207,7 +207,7 @@ describe('Release edit/new component', () => {
await factory({
store: {
modules: {
- detail: {
+ editNew: {
getters: {
isValid: () => true,
},
@@ -227,7 +227,7 @@ describe('Release edit/new component', () => {
await factory({
store: {
modules: {
- detail: {
+ editNew: {
getters: {
isValid: () => false,
},
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 2b5270e29d6..7955b079cbc 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -8,7 +8,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleasesApp from '~/releases/components/app_index.vue';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import createStore from '~/releases/stores';
-import createListModule from '~/releases/stores/modules/list';
+import createIndexModule from '~/releases/stores/modules/index';
import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
jest.mock('~/lib/utils/common_utils', () => ({
@@ -41,15 +41,15 @@ describe('Releases App ', () => {
};
const createComponent = (stateUpdates = {}) => {
- const listModule = createListModule({
+ const indexModule = createIndexModule({
...defaultInitialState,
...stateUpdates,
});
- fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases');
+ fetchReleaseSpy = jest.spyOn(indexModule.actions, 'fetchReleases');
const store = createStore({
- modules: { list: listModule },
+ modules: { index: indexModule },
featureFlags: {
graphqlReleaseData: true,
graphqlReleasesPage: false,
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index bbaa4e9dc94..460007e48ef 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -44,7 +44,7 @@ describe('Release edit component', () => {
const store = new Vuex.Store({
modules: {
- detail: {
+ editNew: {
namespaced: true,
actions,
state,
diff --git a/spec/frontend/releases/components/releases_pagination_graphql_spec.js b/spec/frontend/releases/components/releases_pagination_graphql_spec.js
index de80d82e93c..5b2dd4bc784 100644
--- a/spec/frontend/releases/components/releases_pagination_graphql_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_graphql_spec.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import { historyPushState } from '~/lib/utils/common_utils';
import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
import createStore from '~/releases/stores';
-import createListModule from '~/releases/stores/modules/list';
+import createIndexModule from '~/releases/stores/modules/index';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
@@ -15,7 +15,7 @@ localVue.use(Vuex);
describe('~/releases/components/releases_pagination_graphql.vue', () => {
let wrapper;
- let listModule;
+ let indexModule;
const cursors = {
startCursor: 'startCursor',
@@ -25,16 +25,16 @@ describe('~/releases/components/releases_pagination_graphql.vue', () => {
const projectPath = 'my/project';
const createComponent = (pageInfo) => {
- listModule = createListModule({ projectPath });
+ indexModule = createIndexModule({ projectPath });
- listModule.state.graphQlPageInfo = pageInfo;
+ indexModule.state.graphQlPageInfo = pageInfo;
- listModule.actions.fetchReleases = jest.fn();
+ indexModule.actions.fetchReleases = jest.fn();
wrapper = mount(ReleasesPaginationGraphql, {
store: createStore({
modules: {
- list: listModule,
+ index: indexModule,
},
featureFlags: {},
}),
@@ -142,7 +142,7 @@ describe('~/releases/components/releases_pagination_graphql.vue', () => {
});
it('calls fetchReleases with the correct after cursor', () => {
- expect(listModule.actions.fetchReleases.mock.calls).toEqual([
+ expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
[expect.anything(), { after: cursors.endCursor }],
]);
});
@@ -160,7 +160,7 @@ describe('~/releases/components/releases_pagination_graphql.vue', () => {
});
it('calls fetchReleases with the correct before cursor', () => {
- expect(listModule.actions.fetchReleases.mock.calls).toEqual([
+ expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
[expect.anything(), { before: cursors.startCursor }],
]);
});
diff --git a/spec/frontend/releases/components/releases_pagination_rest_spec.js b/spec/frontend/releases/components/releases_pagination_rest_spec.js
index 6f2690f5322..7d45176967b 100644
--- a/spec/frontend/releases/components/releases_pagination_rest_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_rest_spec.js
@@ -4,7 +4,7 @@ import Vuex from 'vuex';
import * as commonUtils from '~/lib/utils/common_utils';
import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue';
import createStore from '~/releases/stores';
-import createListModule from '~/releases/stores/modules/list';
+import createIndexModule from '~/releases/stores/modules/index';
commonUtils.historyPushState = jest.fn();
@@ -13,21 +13,21 @@ localVue.use(Vuex);
describe('~/releases/components/releases_pagination_rest.vue', () => {
let wrapper;
- let listModule;
+ let indexModule;
const projectId = 19;
const createComponent = (pageInfo) => {
- listModule = createListModule({ projectId });
+ indexModule = createIndexModule({ projectId });
- listModule.state.restPageInfo = pageInfo;
+ indexModule.state.restPageInfo = pageInfo;
- listModule.actions.fetchReleases = jest.fn();
+ indexModule.actions.fetchReleases = jest.fn();
wrapper = mount(ReleasesPaginationRest, {
store: createStore({
modules: {
- list: listModule,
+ index: indexModule,
},
featureFlags: {},
}),
@@ -58,7 +58,7 @@ describe('~/releases/components/releases_pagination_rest.vue', () => {
});
it('calls fetchReleases with the correct page', () => {
- expect(listModule.actions.fetchReleases.mock.calls).toEqual([
+ expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
[expect.anything(), { page: newPage }],
]);
});
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index f17c6678592..b16f80b9c73 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import createStore from '~/releases/stores';
-import createListModule from '~/releases/stores/modules/list';
+import createIndexModule from '~/releases/stores/modules/index';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -11,15 +11,15 @@ localVue.use(Vuex);
describe('~/releases/components/releases_sort.vue', () => {
let wrapper;
let store;
- let listModule;
+ let indexModule;
const projectId = 8;
const createComponent = () => {
- listModule = createListModule({ projectId });
+ indexModule = createIndexModule({ projectId });
store = createStore({
modules: {
- list: listModule,
+ index: indexModule,
},
});
@@ -52,7 +52,7 @@ describe('~/releases/components/releases_sort.vue', () => {
it('on sort change set sorting in vuex and emit event', () => {
findReleasesSorting().vm.$emit('sortDirectionChange');
- expect(store.dispatch).toHaveBeenCalledWith('list/setSorting', { sort: 'asc' });
+ expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { sort: 'asc' });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
@@ -60,7 +60,7 @@ describe('~/releases/components/releases_sort.vue', () => {
const item = findSortingItems().at(0);
const { orderBy } = wrapper.vm.sortOptions[0];
item.vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('list/setSorting', { orderBy });
+ expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index cef7a0272a6..294538086b4 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
-import createDetailModule from '~/releases/stores/modules/detail';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
const TEST_TAG_NAME = 'test-tag-name';
@@ -27,13 +27,13 @@ describe('releases/components/tag_field_existing', () => {
beforeEach(() => {
store = createStore({
modules: {
- detail: createDetailModule({
+ editNew: createEditNewModule({
tagName: TEST_TAG_NAME,
}),
},
});
- store.state.detail.release = {
+ store.state.editNew.release = {
tagName: TEST_TAG_NAME,
};
});
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 387217c2a8e..f1608ca31b4 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -3,7 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
-import createDetailModule from '~/releases/stores/modules/detail';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_PROJECT_ID = '1234';
@@ -44,15 +44,15 @@ describe('releases/components/tag_field_new', () => {
beforeEach(() => {
store = createStore({
modules: {
- detail: createDetailModule({
+ editNew: createEditNewModule({
projectId: TEST_PROJECT_ID,
}),
},
});
- store.state.detail.createFrom = TEST_CREATE_FROM;
+ store.state.editNew.createFrom = TEST_CREATE_FROM;
- store.state.detail.release = {
+ store.state.editNew.release = {
tagName: TEST_TAG_NAME,
assets: {
links: [],
@@ -89,7 +89,7 @@ describe('releases/components/tag_field_new', () => {
});
it("updates the store's release.tagName property", () => {
- expect(store.state.detail.release.tagName).toBe(NONEXISTENT_TAG_NAME);
+ expect(store.state.editNew.release.tagName).toBe(NONEXISTENT_TAG_NAME);
});
it('hides the "Create from" field', () => {
@@ -107,7 +107,7 @@ describe('releases/components/tag_field_new', () => {
});
it("updates the store's release.tagName property", () => {
- expect(store.state.detail.release.tagName).toBe(updatedTagName);
+ expect(store.state.editNew.release.tagName).toBe(updatedTagName);
});
it('shows the "Create from" field', () => {
@@ -178,7 +178,7 @@ describe('releases/components/tag_field_new', () => {
await wrapper.vm.$nextTick();
- expect(store.state.detail.createFrom).toBe(updatedCreateFrom);
+ expect(store.state.editNew.createFrom).toBe(updatedCreateFrom);
});
});
});
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
index 2cf5944f9e6..db08f874959 100644
--- a/spec/frontend/releases/components/tag_field_spec.js
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -3,7 +3,7 @@ import TagField from '~/releases/components/tag_field.vue';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
-import createDetailModule from '~/releases/stores/modules/detail';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
describe('releases/components/tag_field', () => {
let store;
@@ -12,11 +12,11 @@ describe('releases/components/tag_field', () => {
const createComponent = ({ tagName }) => {
store = createStore({
modules: {
- detail: createDetailModule({}),
+ editNew: createEditNewModule({}),
},
});
- store.state.detail.tagName = tagName;
+ store.state.editNew.tagName = tagName;
wrapper = shallowMount(TagField, { store });
};
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 0f81869e3f9..b116d601ca4 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -9,9 +9,9 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatus from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { ASSET_LINK_TYPE } from '~/releases/constants';
-import * as actions from '~/releases/stores/modules/detail/actions';
-import * as types from '~/releases/stores/modules/detail/mutation_types';
-import createState from '~/releases/stores/modules/detail/state';
+import * as actions from '~/releases/stores/modules/edit_new/actions';
+import * as types from '~/releases/stores/modules/edit_new/mutation_types';
+import createState from '~/releases/stores/modules/edit_new/state';
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
jest.mock('~/flash');
@@ -23,7 +23,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
const originalRelease = getJSONFixture('api/releases/release.json');
-describe('Release detail actions', () => {
+describe('Release edit/new actions', () => {
let state;
let release;
let mock;
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 2d9f35428f2..1449c064d77 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -1,6 +1,6 @@
-import * as getters from '~/releases/stores/modules/detail/getters';
+import * as getters from '~/releases/stores/modules/edit_new/getters';
-describe('Release detail getters', () => {
+describe('Release edit/new getters', () => {
describe('isExistingRelease', () => {
it('returns true if the release is an existing release that already exists in the database', () => {
const state = { tagName: 'test-tag-name' };
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index cdf26bfa834..20ae332e500 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -1,13 +1,13 @@
import { getJSONFixture } from 'helpers/fixtures';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
-import * as types from '~/releases/stores/modules/detail/mutation_types';
-import mutations from '~/releases/stores/modules/detail/mutations';
-import createState from '~/releases/stores/modules/detail/state';
+import * as types from '~/releases/stores/modules/edit_new/mutation_types';
+import mutations from '~/releases/stores/modules/edit_new/mutations';
+import createState from '~/releases/stores/modules/edit_new/state';
const originalRelease = getJSONFixture('api/releases/release.json');
-describe('Release detail mutations', () => {
+describe('Release edit/new mutations', () => {
let state;
let release;
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
index 309f7387929..4dc996174bc 100644
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/list/actions_spec.js
@@ -15,9 +15,9 @@ import {
fetchReleasesRest,
receiveReleasesError,
setSorting,
-} from '~/releases/stores/modules/list/actions';
-import * as types from '~/releases/stores/modules/list/mutation_types';
-import createState from '~/releases/stores/modules/list/state';
+} from '~/releases/stores/modules/index/actions';
+import * as types from '~/releases/stores/modules/index/mutation_types';
+import createState from '~/releases/stores/modules/index/state';
import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
diff --git a/spec/frontend/releases/stores/modules/list/helpers.js b/spec/frontend/releases/stores/modules/list/helpers.js
index 3913eba31b8..6669f44aa95 100644
--- a/spec/frontend/releases/stores/modules/list/helpers.js
+++ b/spec/frontend/releases/stores/modules/list/helpers.js
@@ -1,4 +1,4 @@
-import state from '~/releases/stores/modules/list/state';
+import state from '~/releases/stores/modules/index/state';
export const resetStore = (store) => {
store.replaceState(state());
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
index ea6a4ada16a..8b35ba5d7ac 100644
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js
@@ -1,8 +1,8 @@
import { getJSONFixture } from 'helpers/fixtures';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import * as types from '~/releases/stores/modules/list/mutation_types';
-import mutations from '~/releases/stores/modules/list/mutations';
-import createState from '~/releases/stores/modules/list/state';
+import * as types from '~/releases/stores/modules/index/mutation_types';
+import mutations from '~/releases/stores/modules/index/mutations';
+import createState from '~/releases/stores/modules/index/state';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
diff --git a/spec/frontend_integration/test_helpers/mock_server/graphql.js b/spec/frontend_integration/test_helpers/mock_server/graphql.js
index e2658852599..27396842523 100644
--- a/spec/frontend_integration/test_helpers/mock_server/graphql.js
+++ b/spec/frontend_integration/test_helpers/mock_server/graphql.js
@@ -1,13 +1,11 @@
import { buildSchema, graphql } from 'graphql';
+import { memoize } from 'lodash';
-/* eslint-disable import/no-unresolved */
-// This rule is disabled for the following line.
// The graphql schema is dynamically generated in CI
// during the `graphql-schema-dump` job.
-import gitlabSchemaStr from '../../../../tmp/tests/graphql/gitlab_schema.graphql';
-/* eslint-enable import/no-unresolved */
+// eslint-disable-next-line global-require, import/no-unresolved
+const getGraphqlSchema = () => require('../../../../tmp/tests/graphql/gitlab_schema.graphql');
-const graphqlSchema = buildSchema(gitlabSchemaStr.loc.source.body);
const graphqlResolvers = {
project({ fullPath }, schema) {
const result = schema.projects.findBy({ path_with_namespace: fullPath });
@@ -21,6 +19,7 @@ const graphqlResolvers = {
};
},
};
+const buildGraphqlSchema = memoize(() => buildSchema(getGraphqlSchema().loc.source.body));
export const graphqlQuery = (query, variables, schema) =>
- graphql(graphqlSchema, query, graphqlResolvers, schema, variables);
+ graphql(buildGraphqlSchema(), query, graphqlResolvers, schema, variables);
diff --git a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
new file mode 100644
index 00000000000..0b1424133b1
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataNonSqlMetrics do
+ let(:default_count) { Gitlab::UsageDataNonSqlMetrics::SQL_METRIC_DEFAULT }
+
+ describe '.count' do
+ it 'returns default value for count' do
+ expect(described_class.count(User)).to eq(default_count)
+ end
+ end
+
+ describe '.distinct_count' do
+ it 'returns default value for distinct count' do
+ expect(described_class.distinct_count(User)).to eq(default_count)
+ end
+ end
+
+ describe '.estimate_batch_distinct_count' do
+ it 'returns default value for estimate_batch_distinct_count' do
+ expect(described_class.estimate_batch_distinct_count(User)).to eq(default_count)
+ end
+ end
+
+ describe '.sum' do
+ it 'returns default value for sum' do
+ expect(described_class.sum(JiraImportState.finished, :imported_issues_count)).to eq(default_count)
+ end
+ end
+
+ describe '.histogram' do
+ it 'returns default value for histogram' do
+ expect(described_class.histogram(JiraImportState.finished, :imported_issues_count, buckets: [], bucket_size: 0)).to eq(default_count)
+ end
+ end
+end
diff --git a/spec/migrations/migrate_elastic_index_settings_spec.rb b/spec/migrations/migrate_elastic_index_settings_spec.rb
new file mode 100644
index 00000000000..41483773903
--- /dev/null
+++ b/spec/migrations/migrate_elastic_index_settings_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'migrate', '20210324131727_migrate_elastic_index_settings.rb')
+
+RSpec.describe MigrateElasticIndexSettings do
+ let(:elastic_index_settings) { table(:elastic_index_settings) }
+ let(:application_settings) { table(:application_settings) }
+
+ context 'with application_settings present' do
+ before do
+ application_settings.create!(elasticsearch_replicas: 2, elasticsearch_shards: 15)
+ end
+
+ it 'migrates settings' do
+ migrate!
+
+ settings = elastic_index_settings.all
+
+ expect(settings.size).to eq 1
+
+ setting = settings.first
+
+ expect(setting.number_of_replicas).to eq(2)
+ expect(setting.number_of_shards).to eq(15)
+ end
+ end
+
+ context 'without application_settings present' do
+ it 'migrates settings' do
+ migrate!
+
+ settings = elastic_index_settings.all
+
+ expect(settings.size).to eq 1
+
+ setting = elastic_index_settings.first
+
+ expect(setting.number_of_replicas).to eq(1)
+ expect(setting.number_of_shards).to eq(5)
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 88e18fd1608..2063de691a3 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -184,28 +184,6 @@ RSpec.describe Namespace do
end
end
- describe 'callbacks' do
- describe 'before_save :ensure_delayed_project_removal_assigned_to_namespace_settings' do
- it 'sets the matching value in namespace_settings' do
- expect { namespace.update!(delayed_project_removal: true) }.to change {
- namespace.namespace_settings.delayed_project_removal
- }.from(false).to(true)
- end
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(migrate_delayed_project_removal: false)
- end
-
- it 'does not set the matching value in namespace_settings' do
- expect { namespace.update!(delayed_project_removal: true) }.not_to change {
- namespace.namespace_settings.delayed_project_removal
- }
- end
- end
- end
- end
-
describe '#visibility_level_field' do
it { expect(namespace.visibility_level_field).to eq(:visibility_level) }
end
diff --git a/spec/models/raw_usage_data_spec.rb b/spec/models/raw_usage_data_spec.rb
index 7acfb8c19af..6ff4c6eb19b 100644
--- a/spec/models/raw_usage_data_spec.rb
+++ b/spec/models/raw_usage_data_spec.rb
@@ -13,14 +13,20 @@ RSpec.describe RawUsageData do
it { is_expected.to validate_uniqueness_of(:recorded_at) }
end
- describe '#update_sent_at!' do
+ describe '#update_version_metadata!' do
let(:raw_usage_data) { create(:raw_usage_data) }
it 'updates sent_at' do
- raw_usage_data.update_sent_at!
+ raw_usage_data.update_version_metadata!(usage_data_id: 123)
expect(raw_usage_data.sent_at).not_to be_nil
end
+
+ it 'updates version_usage_data_id_value' do
+ raw_usage_data.update_version_metadata!(usage_data_id: 123)
+
+ expect(raw_usage_data.version_usage_data_id_value).not_to be_nil
+ end
end
end
end
diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb
index 24afa83ef2c..53cc33afcff 100644
--- a/spec/services/submit_usage_ping_service_spec.rb
+++ b/spec/services/submit_usage_ping_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe SubmitUsagePingService do
include StubRequests
include UsageDataHelpers
+ let(:usage_data_id) { 31643 }
let(:score_params) do
{
score: {
@@ -40,6 +41,8 @@ RSpec.describe SubmitUsagePingService do
leader_service_desk_issues: 15.8,
instance_service_desk_issues: 15.1,
+ usage_data_id: usage_data_id,
+
non_existing_column: 'value'
}
}
@@ -47,7 +50,6 @@ RSpec.describe SubmitUsagePingService do
let(:with_dev_ops_score_params) { { dev_ops_score: score_params[:score] } }
let(:with_conv_index_params) { { conv_index: score_params[:score] } }
- let(:without_dev_ops_score_params) { { dev_ops_score: {} } }
shared_examples 'does not run' do
it do
@@ -103,7 +105,7 @@ RSpec.describe SubmitUsagePingService do
end
it 'sends a POST request' do
- response = stub_response(body: without_dev_ops_score_params)
+ response = stub_response(body: with_dev_ops_score_params)
subject.execute
@@ -111,7 +113,7 @@ RSpec.describe SubmitUsagePingService do
end
it 'forces a refresh of usage data statistics before submitting' do
- stub_response(body: without_dev_ops_score_params)
+ stub_response(body: with_dev_ops_score_params)
expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_call_original
@@ -124,6 +126,33 @@ RSpec.describe SubmitUsagePingService do
end
it_behaves_like 'saves DevOps report data from the response'
+
+ it 'saves usage_data_id to version_usage_data_id_value' do
+ recorded_at = Time.current
+ usage_data = { uuid: 'uuid', recorded_at: recorded_at }
+
+ expect(Gitlab::UsageData).to receive(:data).with(force_refresh: true).and_return(usage_data)
+
+ subject.execute
+
+ raw_usage_data = RawUsageData.find_by(recorded_at: recorded_at)
+
+ expect(raw_usage_data.version_usage_data_id_value).to eq(31643)
+ end
+ end
+
+ context 'when version app usage_data_id is invalid' do
+ let(:usage_data_id) { -1000 }
+
+ before do
+ stub_response(body: with_conv_index_params)
+ end
+
+ it 'raises an exception' do
+ expect { subject.execute }.to raise_error(described_class::SubmissionError) do |error|
+ expect(error.message).to include('Invalid usage_data_id in response: -1000')
+ end
+ end
end
context 'when DevOps report data is passed' do
diff --git a/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb b/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb
new file mode 100644
index 00000000000..8e27d7987e8
--- /dev/null
+++ b/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'clear DB Load Balancing configuration' do
+ def clear_load_balancing_configuration
+ proxy = ::Gitlab::Database::LoadBalancing.instance_variable_get(:@proxy)
+ proxy.load_balancer.release_host if proxy
+ ::Gitlab::Database::LoadBalancing.instance_variable_set(:@proxy, nil)
+
+ ::Gitlab::Database::LoadBalancing.remove_instance_variable(:@feature_available) if ::Gitlab::Database::LoadBalancing.instance_variable_defined?(:@feature_available)
+
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ around do |example|
+ clear_load_balancing_configuration
+
+ example.run
+
+ clear_load_balancing_configuration
+ end
+end
diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb
index b2dd3556098..36c26b59d75 100644
--- a/spec/views/projects/settings/operations/show.html.haml_spec.rb
+++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'projects/settings/operations/show' do
it 'renders the Operations Settings page' do
render
- expect(rendered).to have_content _('Alerts')
+ expect(rendered).to have_content _('Alert integrations')
expect(rendered).to have_content _('Display alerts from all your monitoring tools directly within GitLab.')
end
end
diff --git a/vendor/shims/mimemagic/mimemagic.gemspec b/vendor/shims/mimemagic/mimemagic.gemspec
index f05f8d5bb41..86f7f824923 100644
--- a/vendor/shims/mimemagic/mimemagic.gemspec
+++ b/vendor/shims/mimemagic/mimemagic.gemspec
@@ -12,10 +12,7 @@ Gem::Specification.new do |spec|
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
- # Specify which files should be added to the gem when it is released.
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
- end
+ spec.files = %w[lib/mimemagic.rb lib/mimemagic/version.rb]
+
spec.require_paths = ["lib"]
end