diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-04 15:09:14 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-04 15:09:14 +0300 |
commit | 8a9cbfa9c56792d8e338c289eb803fb0ebde2083 (patch) | |
tree | fa5ff83fa919ae9a1c37cd8146b78a4a04c9ab7c | |
parent | fc7ce8aea5b2eca1aaf510667c79b3e7e72f2f3f (diff) |
Add latest changes from gitlab-org/gitlab@master
72 files changed, 1114 insertions, 460 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index cc65727e94d..13448a918b8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,7 +16,7 @@ inherit_mode: - Include AllCops: - TargetRubyVersion: 2.6 + TargetRubyVersion: 2.7 TargetRailsVersion: 6.0 Exclude: - 'vendor/**/*' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a1f7d87c397..c313015a314 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -5783f980c3c83022dd5a0173186fba4158948062 +78487b6231f7f0b0ae2c6db34f1495adc47268e8 diff --git a/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue b/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue deleted file mode 100644 index ee2fe00fe02..00000000000 --- a/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue +++ /dev/null @@ -1,13 +0,0 @@ -<script> -import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue'; - -export default { - name: 'DevopsAdoptionApp', - components: { - DevopsAdoptionEmptyState, - }, -}; -</script> -<template> - <devops-adoption-empty-state /> -</template> diff --git a/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue b/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue deleted file mode 100644 index 0fff9beb435..00000000000 --- a/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue +++ /dev/null @@ -1,25 +0,0 @@ -<script> -import { GlEmptyState, GlButton } from '@gitlab/ui'; -import { DEVOPS_ADOPTION_STRINGS } from '../constants'; - -export default { - name: 'DevopsAdoptionEmptyState', - inject: ['emptyStateSvgPath'], - components: { - GlEmptyState, - GlButton, - }, - i18n: DEVOPS_ADOPTION_STRINGS.emptyState, -}; -</script> -<template> - <gl-empty-state - :title="$options.i18n.title" - :description="$options.i18n.description" - :svg-path="emptyStateSvgPath" - > - <template #actions> - <gl-button variant="info">{{ $options.i18n.button }}</gl-button> - </template> - </gl-empty-state> -</template> diff --git a/app/assets/javascripts/admin/dev_ops_report/constants.js b/app/assets/javascripts/admin/dev_ops_report/constants.js deleted file mode 100644 index 4f3b7879332..00000000000 --- a/app/assets/javascripts/admin/dev_ops_report/constants.js +++ /dev/null @@ -1,11 +0,0 @@ -import { s__ } from '~/locale'; - -export const DEVOPS_ADOPTION_STRINGS = { - emptyState: { - title: s__('DevopsAdoption|Add a segment to get started'), - description: s__( - 'DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team.', - ), - button: s__('DevopsAdoption|Add new segment'), - }, -}; diff --git a/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js b/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js index 45901a5634f..ae73033079d 100644 --- a/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js +++ b/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js @@ -1,20 +1,2 @@ -import Vue from 'vue'; -import DevopsAdoptionApp from './components/devops_adoption_app.vue'; - -export default () => { - const el = document.querySelector('.js-devops-adoption'); - - if (!el) return false; - - const { emptyStateSvgPath } = el.dataset; - - return new Vue({ - el, - provide: { - emptyStateSvgPath, - }, - render(h) { - return h(DevopsAdoptionApp); - }, - }); -}; +// EE-specific feature. Find the implementation in the `ee/`-folder +export default () => {}; diff --git a/app/assets/javascripts/alerts_settings/components/alert_settings_form_help_block.vue b/app/assets/javascripts/alerts_settings/components/alert_settings_form_help_block.vue new file mode 100644 index 00000000000..674e3696eb5 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/components/alert_settings_form_help_block.vue @@ -0,0 +1,32 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; + +export default { + components: { + GlLink, + GlSprintf, + }, + props: { + message: { + type: String, + required: true, + }, + link: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <span class="gl-text-gray-500"> + <gl-sprintf :message="message"> + <template #link="{ content }"> + <gl-link class="gl-display-inline-block" :href="link" target="_blank">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </span> +</template> 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 432271d2075..1c22f035a15 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -81,7 +81,6 @@ export default { <div class="incident-management-list"> <h5 class="gl-font-lg">{{ $options.i18n.title }}</h5> <gl-table - :empty-text="$options.i18n.emptyState" :items="integrations" :fields="$options.fields" :busy="loading" @@ -115,6 +114,14 @@ export default { <template #table-busy> <gl-loading-icon size="lg" color="dark" class="mt-3" /> </template> + + <template #empty> + <div + class="gl-border-t-solid gl-border-b-solid gl-border-1 gl-border gl-border-gray-100 mt-n3" + > + <p class="gl-text-gray-400 gl-py-3 gl-my-3">{{ $options.i18n.emptyState }}</p> + </div> + </template> </gl-table> </div> </template> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue index 1490723ff76..84ee2fb524d 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue @@ -6,63 +6,166 @@ import { GlFormGroup, GlFormSelect, GlFormInput, - GlLink, - GlSprintf, + GlFormInputGroup, + GlFormTextarea, + GlModal, + GlModalDirective, + GlToggle, } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; -import { integrationTypes } from '../constants'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue'; +import { + integrationTypesNew, + JSON_VALIDATE_DELAY, + targetPrometheusUrlPlaceholder, + typeSet, +} from '../constants'; export default { + targetPrometheusUrlPlaceholder, + JSON_VALIDATE_DELAY, + typeSet, i18n: { - integrationsInfo: s__( - 'AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}', - ), integrationFormSteps: { - step1: { title: s__('AlertSettings|1. Select integration type') }, - step2: { title: s__('AlertSettings|2. Name integration') }, + step1: { + label: s__('AlertSettings|1. Select integration type'), + help: s__('AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}'), + }, + step2: { + label: s__('AlertSettings|2. Name integration'), + placeholder: s__('AlertSettings|Enter integration name'), + }, + step3: { + label: s__('AlertSettings|3. Set up webhook'), + help: s__( + 'AlertSettings|Utilize the URL and authorization key below to authorize an external service to send Alerts to GitLab. Review your chosen services documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.', + ), + info: s__('AlertSettings|Authorization key'), + reset: s__('AlertSettings|Reset Key'), + }, + step4: { + label: s__('AlertSettings|4. Test integration(optional)'), + help: s__( + 'AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with to send a test alert to the %{linkStart}alerts page%{linkEnd}. This payload can be used to test the integration using the "save and test payload" button.', + ), + placeholder: s__('AlertSettings|Enter test alert JSON....'), + }, step5: { - title: s__('AlertSettings|5. Map fields (optional)'), + label: s__('AlertSettings|5. Map fields (optional)'), intro: s__( 'AlertSettings|The default GitLab alert keys are listed below. In the event an exact match could be found in the sample payload provided, that key will be mapped automatically. In all other cases, please define which payload key should map to the specified GitLab key. Any payload keys not shown in this list will not display in the alert list, but will display on the alert details page.', ), }, + prometheusFormUrl: { + label: s__('AlertSettings|Prometheus API base URL'), + help: s__('AlertSettings|URL cannot be blank and must start with http or https'), + }, }, }, components: { + ClipboardButton, GlButton, GlCollapse, GlForm, GlFormGroup, GlFormInput, + GlFormInputGroup, + GlFormTextarea, GlFormSelect, - GlLink, - GlSprintf, + GlModal, + GlToggle, + AlertSettingsFormHelpBlock, + }, + directives: { + 'gl-modal': GlModalDirective, + }, + inject: { + generic: { + default: {}, + }, + prometheus: { + default: {}, + }, }, mixins: [glFeatureFlagsMixin()], + props: { + loading: { + type: Boolean, + required: true, + }, + }, data() { return { - selectedIntegration: integrationTypes[0].value, - options: integrationTypes, + selectedIntegration: integrationTypesNew[0].value, + options: integrationTypesNew, formVisible: false, - form: { + integrationForm: { name: '', + integrationTestPayload: { + json: null, + error: null, + }, + active: false, + authKey: '', + url: '', + apiUrl: '', }, }; }, + computed: { + jsonIsValid() { + return this.integrationForm.integrationTestPayload.error === null; + }, + selectedIntegrationType() { + switch (this.selectedIntegration) { + case this.$options.typeSet.http: + return this.generic; + case this.$options.typeSet.prometheus: + return this.prometheus; + default: + return {}; + } + }, + }, methods: { onIntegrationTypeSelect() { - if (this.selectedIntegration === integrationTypes[0].value) { + if (this.selectedIntegration === integrationTypesNew[0].value) { this.formVisible = false; } else { this.formVisible = true; } }, + onSubmitWithTestPayload() { + // TODO: Test payload before saving via GraphQL + this.onSubmit(); + }, onSubmit() { - // TODO Add GraphQL method + const { name, apiUrl, active } = this.integrationForm; + const variables = + this.selectedIntegration === this.$options.typeSet.http + ? { name, active } + : { apiUrl, active }; + this.$emit('on-create-new-integration', { type: this.selectedIntegration, variables }); }, onReset() { - this.form.name = ''; + // TODO: Reset form values + }, + onResetAuthKey() { + // TODO: Handle reset auth key via GraphQL + }, + validateJson() { + this.integrationForm.integrationTestPayload.error = null; + if (this.integrationForm.integrationTestPayload.json === '') { + return; + } + + try { + JSON.parse(this.integrationForm.integrationTestPayload.json); + } catch (e) { + this.integrationForm.integrationTestPayload.error = JSON.stringify(e.message); + } }, }, }; @@ -74,7 +177,7 @@ export default { <gl-form-group id="integration-type" - :label="$options.i18n.integrationFormSteps.step1.title" + :label="$options.i18n.integrationFormSteps.step1.label" label-for="integration-type" > <gl-form-select @@ -82,54 +185,158 @@ export default { :options="options" @change="onIntegrationTypeSelect" /> - <span class="gl-text-gray-500"> - <gl-sprintf :message="$options.i18n.integrationsInfo"> - <template #link="{ content }"> - <gl-link - class="gl-display-inline-block" - href="https://gitlab.com/groups/gitlab-org/-/epics/4390" - target="_blank" - >{{ content }}</gl-link - > - </template> - </gl-sprintf> - </span> + + <alert-settings-form-help-block + :message="$options.i18n.integrationFormSteps.step1.help" + link="https://gitlab.com/groups/gitlab-org/-/epics/4390" + /> </gl-form-group> <gl-collapse v-model="formVisible" class="gl-mt-3"> <gl-form-group id="name-integration" - :label="$options.i18n.integrationFormSteps.step2.title" + :label="$options.i18n.integrationFormSteps.step2.label" label-for="name-integration" > <gl-form-input - v-model="form.name" + v-model="integrationForm.name" type="text" - :placeholder="s__('AlertSettings|Enter integration name')" + :placeholder="$options.i18n.integrationFormSteps.step2.placeholder" + /> + </gl-form-group> + <gl-form-group + id="integration-webhook" + :label="$options.i18n.integrationFormSteps.step3.label" + label-for="integration-webhook" + > + <alert-settings-form-help-block + :message="$options.i18n.integrationFormSteps.step3.help" + link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html" + /> + + <gl-toggle + v-model="integrationForm.active" + :is-loading="loading" + :label="__('Active')" + class="gl-my-4 gl-font-weight-normal" + /> + + <div v-if="selectedIntegration === $options.typeSet.prometheus" class="gl-my-4"> + <span> + {{ $options.i18n.integrationFormSteps.prometheusFormUrl.label }} + </span> + + <gl-form-input + id="integration-apiUrl" + v-model="integrationForm.apiUrl" + type="text" + :placeholder="$options.targetPrometheusUrlPlaceholder" + /> + + <span class="gl-text-gray-400"> + {{ $options.i18n.integrationFormSteps.prometheusFormUrl.help }} + </span> + </div> + + <div class="gl-my-4"> + <span> + {{ s__('AlertSettings|Webhook URL') }} + </span> + + <gl-form-input-group id="url" readonly :value="selectedIntegrationType.url"> + <template #append> + <clipboard-button + :text="selectedIntegrationType.url || ''" + :title="__('Copy')" + class="gl-m-0!" + /> + </template> + </gl-form-input-group> + </div> + + <div class="gl-my-4"> + <span> + {{ $options.i18n.integrationFormSteps.step3.info }} + </span> + + <gl-form-input-group + id="authorization-key" + class="gl-mb-2" + readonly + :value="selectedIntegrationType.authKey" + > + <template #append> + <clipboard-button + :text="selectedIntegrationType.authKey || ''" + :title="__('Copy')" + class="gl-m-0!" + /> + </template> + </gl-form-input-group> + + <gl-button v-gl-modal.authKeyModal :disabled="!integrationForm.active" class="gl-mt-3">{{ + $options.i18n.integrationFormSteps.step3.reset + }}</gl-button> + <gl-modal + modal-id="authKeyModal" + :title="$options.i18n.integrationFormSteps.step3.reset" + :ok-title="$options.i18n.integrationFormSteps.step3.reset" + ok-variant="danger" + @ok="() => {}" + > + {{ $options.i18n.integrationFormSteps.step3.reset }} + </gl-modal> + </div> + </gl-form-group> + <gl-form-group + id="test-integration" + :label="$options.i18n.integrationFormSteps.step4.label" + label-for="test-integration" + :invalid-feedback="integrationForm.integrationTestPayload.error" + > + <alert-settings-form-help-block + :message="$options.i18n.integrationFormSteps.step4.help" + :link="generic.alertsUsageUrl" + /> + + <gl-form-textarea + id="test-integration" + v-model.trim="integrationForm.integrationTestPayload.json" + :disabled="!integrationForm.active" + :state="jsonIsValid" + :placeholder="$options.i18n.integrationFormSteps.step4.placeholder" + class="gl-my-4" + :debounce="$options.JSON_VALIDATE_DELAY" + rows="6" + max-rows="10" + @input="validateJson" /> </gl-form-group> <gl-form-group v-if="glFeatures.multipleHttpIntegrationsCustomMapping" id="mapping-builder" - :label="$options.i18n.integrationFormSteps.step5.title" + :label="$options.i18n.integrationFormSteps.step5.label" label-for="mapping-builder" > <span class="gl-text-gray-500">{{ $options.i18n.integrationFormSteps.step5.intro }}</span> <!--mapping builder will be added here--> </gl-form-group> - <div class="gl-display-flex gl-justify-content-end"> <gl-button type="reset" class="gl-mr-3 js-no-auto-disable">{{ __('Cancel') }}</gl-button> <gl-button - type="submit" category="secondary" variant="success" class="gl-mr-1 js-no-auto-disable" - >{{ __('Save and test payload') }}</gl-button + @click="onSubmitWithTestPayload" + >{{ s__('AlertSettings|Save and test payload') }}</gl-button + > + <gl-button + type="submit" + variant="success" + class="js-no-auto-disable" + data-testid="integration-form-submit" + >{{ s__('AlertSettings|Save integration') }}</gl-button > - <gl-button type="submit" variant="success" class="js-no-auto-disable">{{ - s__('AlertSettings|Save integration') - }}</gl-button> </div> </gl-collapse> </gl-form> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue index ab6d5526154..7d5ea9aa8c4 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue @@ -56,7 +56,7 @@ export default { data() { return { loading: false, - selectedIntegration: integrationTypes[1].value, + selectedIntegration: integrationTypes[0].value, options: integrationTypes, active: false, authKey: '', @@ -88,34 +88,34 @@ export default { ]; }, isPrometheus() { - return this.selectedIntegration === 'prometheus'; + return this.selectedIntegration === 'PROMETHEUS'; }, isOpsgenie() { - return this.selectedIntegration === 'opsgenie'; + return this.selectedIntegration === 'OPSGENIE'; }, selectedIntegrationType() { switch (this.selectedIntegration) { - case 'generic': { + case 'HTTP': { return { url: this.generic.url, - authKey: this.generic.authorizationKey, - activated: this.generic.activated, + authKey: this.generic.authKey, + active: this.generic.active, resetKey: this.resetKey.bind(this), }; } - case 'prometheus': { + case 'PROMETHEUS': { return { - url: this.prometheus.prometheusUrl, - authKey: this.prometheus.authorizationKey, - activated: this.prometheus.activated, - resetKey: this.resetKey.bind(this, 'prometheus'), + url: this.prometheus.url, + authKey: this.prometheus.authKey, + active: this.prometheus.active, + resetKey: this.resetKey.bind(this, 'PROMETHEUS'), targetUrl: this.prometheus.prometheusApiUrl, }; } - case 'opsgenie': { + case 'OPSGENIE': { return { targetUrl: this.opsgenie.opsgenieMvcTargetUrl, - activated: this.opsgenie.activated, + active: this.opsgenie.active, }; } default: { @@ -161,16 +161,12 @@ export default { }, }, mounted() { - if ( - this.prometheus.activated || - this.generic.activated || - !this.opsgenie.opsgenieMvcIsAvailable - ) { + if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) { this.removeOpsGenieOption(); - } else if (this.opsgenie.activated) { + } else if (this.opsgenie.active) { this.setOpsgenieAsDefault(); } - this.active = this.selectedIntegrationType.activated; + this.active = this.selectedIntegrationType.active; this.authKey = this.selectedIntegrationType.authKey ?? ''; }, methods: { @@ -183,19 +179,19 @@ export default { }, setOpsgenieAsDefault() { this.options = this.options.map(el => { - if (el.value !== 'opsgenie') { + if (el.value !== 'OPSGENIE') { return { ...el, disabled: true }; } return { ...el, disabled: false }; }); - this.selectedIntegration = this.options.find(({ value }) => value === 'opsgenie').value; + this.selectedIntegration = this.options.find(({ value }) => value === 'OPSGENIE').value; if (this.targetUrl === null) { this.targetUrl = this.selectedIntegrationType.targetUrl; } }, removeOpsGenieOption() { this.options = this.options.map(el => { - if (el.value !== 'opsgenie') { + if (el.value !== 'OPSGENIE') { return { ...el, disabled: false }; } return { ...el, disabled: true }; @@ -204,7 +200,7 @@ export default { resetFormValues() { this.testAlert.json = null; this.targetUrl = this.selectedIntegrationType.targetUrl; - this.active = this.selectedIntegrationType.activated; + this.active = this.selectedIntegrationType.active; }, dismissFeedback() { this.serverError = null; @@ -212,7 +208,7 @@ export default { this.isFeedbackDismissed = false; }, resetKey(key) { - const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey(); + const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey(); return fn .then(({ data: { token } }) => { @@ -242,9 +238,10 @@ export default { }, toggleActivated(value) { this.loading = true; + const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath; return service .updateGenericActive({ - endpoint: this[this.selectedIntegration].formPath, + endpoint: path, params: this.isOpsgenie ? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } } : { service: { active: value } }, @@ -345,7 +342,7 @@ export default { if (this.canSaveForm) { this.canSaveForm = false; - this.active = this.selectedIntegrationType.activated; + this.active = this.selectedIntegrationType.active; } }, }, @@ -402,9 +399,9 @@ export default { </gl-sprintf> </span> </gl-form-group> - <gl-form-group :label="$options.i18n.activeLabel" label-for="activated"> + <gl-form-group :label="$options.i18n.activeLabel" label-for="active"> <toggle-button - id="activated" + id="active" :disabled-input="loading" :is-loading="loading" :value="active" 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 1576faf13a9..7fe0a4ad2cb 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -1,13 +1,24 @@ <script> +import produce from 'immer'; import { s__ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { fetchPolicies } from '~/lib/graphql'; +import createFlash, { FLASH_TYPES } from '~/flash'; import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql'; +import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql'; +import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql'; import IntegrationsList from './alerts_integrations_list.vue'; import SettingsFormOld from './alerts_settings_form_old.vue'; import SettingsFormNew from './alerts_settings_form_new.vue'; +import { typeSet } from '../constants'; export default { + typeSet, + i18n: { + changesSaved: s__( + 'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.', + ), + }, components: { IntegrationsList, SettingsFormOld, @@ -49,6 +60,7 @@ export default { data() { return { errored: false, + isUpdating: false, integrations: {}, }; }, @@ -61,16 +73,85 @@ export default { { name: s__('AlertSettings|HTTP endpoint'), type: s__('AlertsIntegrations|HTTP endpoint'), - active: this.generic.activated, + active: this.generic.active, }, { name: s__('AlertSettings|External Prometheus'), type: s__('AlertsIntegrations|Prometheus'), - active: this.prometheus.activated, + active: this.prometheus.active, }, ]; }, }, + methods: { + onCreateNewIntegration({ type, variables }) { + this.isUpdating = true; + this.$apollo + .mutate({ + mutation: + type === this.$options.typeSet.http + ? createHttpIntegrationMutation + : createPrometheusIntegrationMutation, + variables: { + ...variables, + projectPath: this.projectPath, + }, + update: this.updateIntegrations, + }) + .then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => { + const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0]; + if (error) { + return createFlash({ message: error }); + } + return createFlash({ + message: this.$options.i18n.changesSaved, + type: FLASH_TYPES.SUCCESS, + }); + }) + .catch(err => { + this.errored = true; + createFlash({ message: err }); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + updateIntegrations( + store, + { + data: { httpIntegrationCreate, prometheusIntegrationCreate }, + }, + ) { + const integration = + httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration; + if (!integration) { + return; + } + + const sourceData = store.readQuery({ + query: getIntegrationsQuery, + variables: { + projectPath: this.projectPath, + }, + }); + + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.project.alertManagementIntegrations.nodes = [ + integration, + ...draftData.project.alertManagementIntegrations.nodes, + ]; + }); + + store.writeQuery({ + query: getIntegrationsQuery, + variables: { + projectPath: this.projectPath, + }, + data, + }); + }, + }, }; </script> @@ -80,7 +161,11 @@ export default { :integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld" :loading="loading" /> - <settings-form-new v-if="glFeatures.httpIntegrationsList" /> + <settings-form-new + v-if="glFeatures.httpIntegrationsList" + :loading="loading" + @on-create-new-integration="onCreateNewIntegration" + /> <settings-form-old v-else /> </div> </template> diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index 74e39534eb5..7e0422c100d 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -1,5 +1,6 @@ import { s__ } from '~/locale'; +// TODO: Remove this as part of the form old removal export const i18n = { usageSection: s__( 'AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.', @@ -39,13 +40,23 @@ export const i18n = { integration: s__('AlertSettings|Integration'), }; +// TODO: Delete as part of old form removal in 13.6 export const integrationTypes = [ + { value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') }, + { value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') }, + { value: 'OPSGENIE', text: s__('AlertSettings|Opsgenie') }, +]; + +export const integrationTypesNew = [ { value: '', text: s__('AlertSettings|Select integration type') }, - { value: 'generic', text: s__('AlertSettings|HTTP Endpoint') }, - { value: 'prometheus', text: s__('AlertSettings|External Prometheus') }, - { value: 'opsgenie', text: s__('AlertSettings|Opsgenie') }, + ...integrationTypes, ]; +export const typeSet = { + http: 'HTTP', + prometheus: 'PROMETHEUS', +}; + export const JSON_VALIDATE_DELAY = 250; export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/'; diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql new file mode 100644 index 00000000000..d1dacbad40a --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/integration_item.fragment.graphql" + +mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) { + httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) { + errors + integration { + ...IntegrationItem + } + } +} diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql new file mode 100644 index 00000000000..bb22795ddd5 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql @@ -0,0 +1,12 @@ +#import "../fragments/integration_item.fragment.graphql" + +mutation createPrometheusIntegration($projectPath: ID!, $apiUrl: String!, $active: Boolean!) { + prometheusIntegrationCreate( + input: { projectPath: $projectPath, apiUrl: $apiUrl, active: $active } + ) { + errors + integration { + ...IntegrationItem + } + } +} diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index 8f22b8d1dcd..611148cd103 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -48,9 +48,9 @@ export default el => { el, provide: { prometheus: { - activated: parseBoolean(prometheusActivated), - prometheusUrl, - authorizationKey: prometheusAuthorizationKey, + active: parseBoolean(prometheusActivated), + url: prometheusUrl, + authKey: prometheusAuthorizationKey, prometheusFormPath, prometheusResetKeyPath, prometheusApiUrl, @@ -58,14 +58,14 @@ export default el => { generic: { alertsSetupUrl, alertsUsageUrl, - activated: parseBoolean(activatedStr), + active: parseBoolean(activatedStr), formPath, - authorizationKey, + authKey: authorizationKey, url, }, opsgenie: { formPath: opsgenieMvcFormPath, - activated: parseBoolean(opsgenieMvcEnabled), + active: parseBoolean(opsgenieMvcEnabled), opsgenieMvcTargetUrl, opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable), }, diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 0c6acbf4e45..95d1b226381 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -70,6 +70,7 @@ const Api = { featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists', featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', billableGroupMembersPath: '/api/:version/groups/:id/billable_members', + containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/', group(groupId, callback = () => {}) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -106,6 +107,11 @@ const Api = { return axios.delete(url); }, + containerRegistryDetails(registryId, options = {}) { + const url = Api.buildUrl(this.containerRegistryDetailsPath).replace(':id', registryId); + return axios.get(url, options); + }, + groupMembers(id, options) { const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id)); diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index e4f79e76cad..5a3d836a158 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d .then(({ data }) => { const lines = data.map((line, index) => prepareLineForRenamedFile({ - diffViewType: state.diffViewType, + diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType, line, diffFile, index, @@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d viewer: { ...diffFile.alternate_viewer, automaticallyCollapsed: false, + manuallyCollapsed: false, }, }); commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines }); diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 2aeecf6e9f5..096c4f69439 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -378,8 +378,13 @@ export default { }, [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { const file = state.diffFiles.find(f => f.file_path === filePath); - const currentDiffLinesKey = - state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines'; + let currentDiffLinesKey; + + if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') { + currentDiffLinesKey = 'highlighted_diff_lines'; + } else { + currentDiffLinesKey = 'parallel_diff_lines'; + } file[currentDiffLinesKey] = lines; }, diff --git a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue index 273bd47cfc1..6a57e9a8759 100644 --- a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue +++ b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue @@ -27,7 +27,7 @@ export default { rolloutUserListLabel: s__('FeatureFlag|User List'), rolloutUserListDescription: s__('FeatureFlag|Select a user list'), rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'), - defaultDropdownText: s__('FeatureFlags|Select a user list'), + defaultDropdownText: s__('FeatureFlags|No user list selected'), }, computed: { ...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']), @@ -36,7 +36,7 @@ export default { return this.strategy?.userList?.id ?? ''; }, dropdownText() { - return this.strategy?.userList?.name ?? this.$options.defaultDropdownText; + return this.strategy?.userList?.name ?? this.$options.translations.defaultDropdownText; }, }, mounted() { diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js index 325b74a414d..220fc049562 100644 --- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js +++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js @@ -1,5 +1,5 @@ +import initDevopAdoption from 'ee_else_ce/admin/dev_ops_report/devops_adoption'; import initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state'; -import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption'; initDevOpsScoreEmptyState(); initDevopAdoption(); diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index 1dc5882d415..306e6903a4f 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__( 'ContainerRegistry|Tags successfully marked for deletion.', ); +export const FETCH_IMAGE_DETAILS_ERROR_MESSAGE = s__( + 'ContainerRegistry|Something went wrong while fetching the image details.', +); + export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags'); export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}'); export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}'); diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js index 9125f573aa4..b9b0e1704d8 100644 --- a/app/assets/javascripts/registry/explorer/stores/actions.js +++ b/app/assets/javascripts/registry/explorer/stores/actions.js @@ -1,11 +1,13 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import Api from '~/api'; import * as types from './mutation_types'; import { FETCH_IMAGES_LIST_ERROR_MESSAGE, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, FETCH_TAGS_LIST_ERROR_MESSAGE, + FETCH_IMAGE_DETAILS_ERROR_MESSAGE, } from '../constants/index'; import { decodeAndParse } from '../utils'; @@ -61,6 +63,19 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params }); }; +export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => { + commit(types.SET_MAIN_LOADING, true); + return Api.containerRegistryDetails(id) + .then(({ data }) => { + commit(types.SET_IMAGE_DETAILS, data); + dispatch('requestTagsList'); + }) + .catch(() => { + createFlash(FETCH_IMAGE_DETAILS_ERROR_MESSAGE); + commit(types.SET_MAIN_LOADING, false); + }); +}; + export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => { commit(types.SET_MAIN_LOADING, true); return axios diff --git a/app/assets/javascripts/registry/explorer/stores/mutation_types.js b/app/assets/javascripts/registry/explorer/stores/mutation_types.js index f32cdf90783..5dd0cec52eb 100644 --- a/app/assets/javascripts/registry/explorer/stores/mutation_types.js +++ b/app/assets/javascripts/registry/explorer/stores/mutation_types.js @@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION'; export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS'; export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP'; +export const SET_IMAGE_DETAILS = 'SET_IMAGE_DETAILS'; diff --git a/app/assets/javascripts/registry/explorer/stores/mutations.js b/app/assets/javascripts/registry/explorer/stores/mutations.js index 706f6489287..5bdb431ad2e 100644 --- a/app/assets/javascripts/registry/explorer/stores/mutations.js +++ b/app/assets/javascripts/registry/explorer/stores/mutations.js @@ -47,4 +47,8 @@ export default { const normalizedHeaders = normalizeHeaders(headers); state.tagsPagination = parseIntPagination(normalizedHeaders); }, + + [types.SET_IMAGE_DETAILS](state, details) { + state.imageDetails = details; + }, }; diff --git a/app/assets/javascripts/registry/explorer/stores/state.js b/app/assets/javascripts/registry/explorer/stores/state.js index 694006aac81..66ee56eb47b 100644 --- a/app/assets/javascripts/registry/explorer/stores/state.js +++ b/app/assets/javascripts/registry/explorer/stores/state.js @@ -3,6 +3,7 @@ export default () => ({ showGarbageCollectionTip: false, config: {}, images: [], + imageDetails: {}, tags: [], pagination: {}, tagsPagination: {}, diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js index 44262a6cbb6..7c6348151c1 100644 --- a/app/assets/javascripts/registry/explorer/utils.js +++ b/app/assets/javascripts/registry/explorer/utils.js @@ -1 +1,9 @@ export const decodeAndParse = param => JSON.parse(window.atob(param)); + +// eslint-disable-next-line @gitlab/require-i18n-strings +export const pathGenerator = (imageDetails, ending = 'tags?format=json') => { + // this method is a temporary workaround, to be removed with graphql implementation + // https://gitlab.com/gitlab-org/gitlab/-/issues/276432 + const basePath = imageDetails.path.replace(`/${imageDetails.name}`, ''); + return `/${basePath}/registry/repository/${imageDetails.id}/${ending}`; +}; diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb index 7234cbb29cc..3e802156c8f 100644 --- a/app/helpers/operations_helper.rb +++ b/app/helpers/operations_helper.rb @@ -30,7 +30,7 @@ module OperationsHelper 'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'), 'alerts_usage_url' => project_alert_management_index_path(@project), 'disabled' => disabled.to_s, - 'project_path' => project_path(@project) + 'project_path' => @project.full_path } end diff --git a/app/views/admin/dev_ops_report/_tab.html.haml b/app/views/admin/dev_ops_report/_tab.html.haml deleted file mode 100644 index 7924feba523..00000000000 --- a/app/views/admin/dev_ops_report/_tab.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%li.nav-item.js-devops-tab-item{ role: 'presentation' } - %a.nav-link{ href: target, class: active_when(active), data: { toggle: 'tab' }, role: 'tab' } - = title diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml index 4ffae7897a3..ad9f0c6f776 100644 --- a/app/views/admin/dev_ops_report/show.html.haml +++ b/app/views/admin/dev_ops_report/show.html.haml @@ -4,17 +4,7 @@ .container .gl-mt-3 - if Feature.enabled?(:devops_adoption) - %h2 - = _('DevOps Report') - %ul.nav-links.nav-tabs.nav.js-devops-tabs{ role: 'tablist' } - = render 'tab', active: true, title: _('DevOps Score'), target: '#devops_score_pane' - = render 'tab', active: false, title: _('Adoption'), target: '#devops_adoption_pane' - - .tab-content - .tab-pane.active#devops_score_pane - = render 'report' - .tab-pane#devops_adoption_pane - .js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } } + = render_if_exists 'admin/dev_ops_report/devops_tabs' - else = render 'report' diff --git a/changelogs/unreleased/216008-ec2-for-auto-deploy.yml b/changelogs/unreleased/216008-ec2-for-auto-deploy.yml new file mode 100644 index 00000000000..3e82961daf7 --- /dev/null +++ b/changelogs/unreleased/216008-ec2-for-auto-deploy.yml @@ -0,0 +1,5 @@ +--- +title: Add EC2 to AutoDevOps template +merge_request: 45651 +author: +type: changed diff --git a/changelogs/unreleased/255889-remove-feedback-alert-ondemand-scans.yml b/changelogs/unreleased/255889-remove-feedback-alert-ondemand-scans.yml new file mode 100644 index 00000000000..c0294ce4af7 --- /dev/null +++ b/changelogs/unreleased/255889-remove-feedback-alert-ondemand-scans.yml @@ -0,0 +1,5 @@ +--- +title: Remove feedback alert from on-demand scans form +merge_request: 45217 +author: +type: changed diff --git a/changelogs/unreleased/cat-readd-logging-for-ints-273663.yml b/changelogs/unreleased/cat-readd-logging-for-ints-273663.yml new file mode 100644 index 00000000000..4bc04d600de --- /dev/null +++ b/changelogs/unreleased/cat-readd-logging-for-ints-273663.yml @@ -0,0 +1,5 @@ +--- +title: Fix logging handling for API integer params +merge_request: 46551 +author: +type: fixed diff --git a/changelogs/unreleased/defect-collapsed-files-expand-renamed-files.yml b/changelogs/unreleased/defect-collapsed-files-expand-renamed-files.yml new file mode 100644 index 00000000000..e52a4a01fb3 --- /dev/null +++ b/changelogs/unreleased/defect-collapsed-files-expand-renamed-files.yml @@ -0,0 +1,5 @@ +--- +title: Assign new incoming diff lines for renamed files to the correct view type +merge_request: 46823 +author: +type: fixed diff --git a/changelogs/unreleased/fix-api-boards-docs-ee.yml b/changelogs/unreleased/fix-api-boards-docs-ee.yml new file mode 100644 index 00000000000..e342e9bc378 --- /dev/null +++ b/changelogs/unreleased/fix-api-boards-docs-ee.yml @@ -0,0 +1,5 @@ +--- +title: Fix example responses for Group Issue Board creation API in the docs +merge_request: 46760 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/fix-empty-user-list-dropdown-text.yml b/changelogs/unreleased/fix-empty-user-list-dropdown-text.yml new file mode 100644 index 00000000000..e26d83ca7f3 --- /dev/null +++ b/changelogs/unreleased/fix-empty-user-list-dropdown-text.yml @@ -0,0 +1,5 @@ +--- +title: Show "No user list selected" in feature flags +merge_request: 46790 +author: +type: fixed diff --git a/changelogs/unreleased/georgekoltsov-ignore-failed-to-download-project-export-uploads.yml b/changelogs/unreleased/georgekoltsov-ignore-failed-to-download-project-export-uploads.yml new file mode 100644 index 00000000000..9e51c3d9272 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-ignore-failed-to-download-project-export-uploads.yml @@ -0,0 +1,6 @@ +--- +title: Fix 'File name too long' error happening during Project Export when exporting + project uploads +merge_request: 46674 +author: +type: fixed diff --git a/config/feature_flags/licensed/incident_sla.yml b/config/feature_flags/licensed/incident_sla.yml deleted file mode 100644 index e59251dd82f..00000000000 --- a/config/feature_flags/licensed/incident_sla.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: incident_sla -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43648 -rollout_issue_url: -group: group::health -type: licensed -default_enabled: true diff --git a/config/initializers/0_inject_feature_flags.rb b/config/initializers/0_inject_feature_flags.rb index 7b6e3c571b5..4de762742d1 100644 --- a/config/initializers/0_inject_feature_flags.rb +++ b/config/initializers/0_inject_feature_flags.rb @@ -14,10 +14,8 @@ if Gitlab.ee? && Gitlab.dev_or_test_env? # being unique to licensed names. These feature flags should be reworked to # be "development" with explicit check IGNORED_FEATURE_FLAGS = %i[ - ci_secrets_management feature_flags_related_issues group_wikis - incident_sla swimlanes minimal_access_role ].to_set diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md index 4ff373ce583..6158400f882 100644 --- a/doc/api/group_boards.md +++ b/doc/api/group_boards.md @@ -265,44 +265,17 @@ Example response: { "id": 1, "name": "newboard", + "project": null, + "lists" : [], "group": { "id": 5, "name": "Documentcloud", "web_url": "http://example.com/groups/documentcloud" }, - "milestone": { - "id": 12 - "title": "10.0" - }, - "lists" : [ - { - "id" : 1, - "label" : { - "name" : "Testing", - "color" : "#F0AD4E", - "description" : null - }, - "position" : 1 - }, - { - "id" : 2, - "label" : { - "name" : "Ready", - "color" : "#FF0000", - "description" : null - }, - "position" : 2 - }, - { - "id" : 3, - "label" : { - "name" : "Production", - "color" : "#FF5F00", - "description" : null - }, - "position" : 3 - } - ] + "milestone": null, + "assignee" : null, + "labels" : [], + "weight" : null } ``` diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md index 1be40a0add0..e0cd1f73ffc 100644 --- a/doc/ci/cloud_deployment/index.md +++ b/doc/ci/cloud_deployment/index.md @@ -282,6 +282,32 @@ When running your project pipeline at this point: on the related JSON object's content. The deployment job finishes whenever the deployment to EC2 is done or has failed. +#### Custom build job for Auto DevOps + +To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to +AWS EC2, you must specify a job for the `build` stage. + +To do so, you must reference the `Auto-DevOps.gitlab-ci.yml` template and include a job named +`build_artifact` in your `.gitlab-ci.yml` file. For example: + +```yaml +# .gitlab-ci.yml + +include: + - template: Auto-DevOps.gitlab-ci.yml + +variables: + - AUTO_DEVOPS_PLATFORM_TARGET: EC2 + +build_artifact: + stage: build + script: + - <your build script goes here> + artifacts: + paths: + - <built artifact> +``` + ### Deploy to Amazon EKS - [How to deploy your application to a GitLab-managed Amazon EKS cluster with Auto DevOps](https://about.gitlab.com/blog/2020/05/05/deploying-application-eks/) diff --git a/doc/development/README.md b/doc/development/README.md index 58c35e0c8e6..fde9823d2cb 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -133,6 +133,7 @@ from: - [Approval Rules](approval_rules.md) - [Feature categorization](feature_categorization/index.md) - [Wikis development guide](wikis.md) +- [Newlines style guide](newlines_styleguide.md) ## Performance guides diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index dc7debdce64..6e44e139e1e 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -132,8 +132,23 @@ Non-nullable fields should only be used when a field is required, very unlikely to become optional in the future, and very easy to calculate. An example would be `id` fields. +A non-nullable GraphQL schema field is an object type followed by the exclamation point (bang) `!`. Here's an example from the `gitlab_schema.graphql` file: + +```graphql + id: ProjectID! +``` + +Here's an example of a non-nullable GraphQL array: + +```graphql + + errors: [String!]! +``` + Further reading: +- [GraphQL Best Practices Guide](https://graphql.org/learn/best-practices/#nullability). +- GraphQL documentation on [Object types and fields](https://graphql.org/learn/schema/#object-types-and-fields). - [GraphQL Best Practices Guide](https://graphql.org/learn/best-practices/#nullability) - [Using nullability in GraphQL](https://www.apollographql.com/blog/using-nullability-in-graphql-2254f84c4ed7) diff --git a/doc/topics/git/lfs/index.md b/doc/topics/git/lfs/index.md index 7235ba07d0a..80014358230 100644 --- a/doc/topics/git/lfs/index.md +++ b/doc/topics/git/lfs/index.md @@ -114,8 +114,11 @@ See the documentation on [File Locking](../../../user/project/file_lock.md). ## LFS objects in project archives > - Support for including Git LFS blobs inside [project source downloads](../../../user/project/repository/index.md) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5. -> - It's [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. -> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-lfs-objects-in-project-archives). **(CORE ONLY)** +> - It was [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. +> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/268409) on GitLab 13.6. +> - It's enabled on GitLab.com. +> - It's recommended for production use. +> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-lfs-objects-in-project-archives). CAUTION: **Warning:** This feature might not be available to you. Check the **version history** note above for details. @@ -139,10 +142,10 @@ Technical details about how this works can be found in the [development document ### Enable or disable LFS objects in project archives -_LFS objects in project archives_ is under development and not ready for production use. It is -deployed behind a feature flag that is **disabled by default**. +_LFS objects in project archives_ is under development but ready for production use. +It is deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) -can enable it. +can opt to disable it. To enable it: diff --git a/doc/user/group/index.md b/doc/user/group/index.md index e9c684ab606..bb331e02771 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -530,7 +530,10 @@ To remove a group and its contents: This action either: - Removes the group, and also queues a background job to delete all projects in that group. -- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay). +- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, this action adds a background job to mark a group for deletion. By default, the job schedules the deletion 7 days in the future. You can modify this waiting period through the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay). + +Since [GitLab 13.6](https://gitlab.com/gitlab-org/gitlab/-/issues/39504), if the user who sets up the deletion leaves or is otherwise removed from the group before the +actual deletion happens, the job is cancelled, and the group is no longer scheduled for deletion. ### Restore a group **(PREMIUM)** diff --git a/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml b/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml index 82b2f5c035e..453803a6f7e 100644 --- a/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml @@ -4,6 +4,7 @@ stages: - review - deploy - production + - cleanup variables: AUTO_DEVOPS_PLATFORM_TARGET: ECS diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 940291bd7b4..a13f2046291 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -163,6 +163,7 @@ include: - template: Jobs/Code-Intelligence.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml - template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml + - template: Jobs/Deploy/EC2.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/EC2.gitlab-ci.yml - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 0c3598a61a7..1c25d9d583b 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -16,4 +16,14 @@ build: fi - /build/build.sh rules: + - if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"' + when: never - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + +build_artifact: + stage: build + script: + - printf "To build your project, please create a build_artifact job into your .gitlab-ci.yml file.\nMore information at https://docs.gitlab.com/ee/ci/cloud_deployment\n" + - exit 1 + rules: + - if: '$AUTO_DEVOPS_PLATFORM_TARGET == "EC2"' diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb index ac149cadb5b..a0dccbcdab3 100644 --- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -41,6 +41,8 @@ module Gitlab data.map! { |v| utf8_encode_values(v) } when String encode_utf8(data) + when Integer + data end end end diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb index 26e7d2cf765..428bcbe8dc5 100644 --- a/lib/gitlab/import_export/uploads_manager.rb +++ b/lib/gitlab/import_export/uploads_manager.rb @@ -86,6 +86,10 @@ module Gitlab mkdir_p(File.join(uploads_export_path, secret)) download_or_copy_upload(upload, upload_path) + rescue Errno::ENAMETOOLONG => e + # Do not fail entire project export if downloaded file has filename that exceeds 255 characters. + # Ignore raised exception, skip such upload, log the error and keep going with the export instead. + Gitlab::ErrorTracking.log_exception(e, project_id: @project.id) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0b8daf42cf5..bdb68abf10a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2494,6 +2494,12 @@ msgstr "" msgid "AlertSettings|2. Name integration" msgstr "" +msgid "AlertSettings|3. Set up webhook" +msgstr "" + +msgid "AlertSettings|4. Test integration(optional)" +msgstr "" + msgid "AlertSettings|5. Map fields (optional)" msgstr "" @@ -2548,6 +2554,15 @@ msgstr "" msgid "AlertSettings|Opsgenie" msgstr "" +msgid "AlertSettings|Prometheus API base URL" +msgstr "" + +msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with to send a test alert to the %{linkStart}alerts page%{linkEnd}. This payload can be used to test the integration using the \"save and test payload\" button." +msgstr "" + +msgid "AlertSettings|Reset Key" +msgstr "" + msgid "AlertSettings|Reset key" msgstr "" @@ -2557,6 +2572,9 @@ msgstr "" msgid "AlertSettings|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint." msgstr "" +msgid "AlertSettings|Save and test payload" +msgstr "" + msgid "AlertSettings|Save integration" msgstr "" @@ -2584,6 +2602,9 @@ msgstr "" msgid "AlertSettings|URL cannot be blank and must start with http or https" msgstr "" +msgid "AlertSettings|Utilize the URL and authorization key below to authorize an external service to send Alerts to GitLab. Review your chosen services documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint." +msgstr "" + msgid "AlertSettings|Webhook URL" msgstr "" @@ -2623,6 +2644,9 @@ msgstr "" msgid "AlertsIntegrations|Prometheus" msgstr "" +msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list." +msgstr "" + msgid "Algorithm" msgstr "" @@ -7214,6 +7238,9 @@ msgstr "" msgid "ContainerRegistry|Something went wrong while fetching the cleanup policy." msgstr "" +msgid "ContainerRegistry|Something went wrong while fetching the image details." +msgstr "" + msgid "ContainerRegistry|Something went wrong while fetching the repository list." msgstr "" @@ -11473,6 +11500,9 @@ msgstr "" msgid "FeatureFlags|New user list" msgstr "" +msgid "FeatureFlags|No user list selected" +msgstr "" + msgid "FeatureFlags|Percent of users" msgstr "" @@ -11497,9 +11527,6 @@ msgstr "" msgid "FeatureFlags|Rollout Strategy" msgstr "" -msgid "FeatureFlags|Select a user list" -msgstr "" - msgid "FeatureFlags|Set the Unleash client application name to the name of the environment your application runs in. This value is used to match environment scopes. See the %{linkStart}example client configuration%{linkEnd}." msgstr "" @@ -23249,9 +23276,6 @@ msgstr "" msgid "Save Push Rules" msgstr "" -msgid "Save and test payload" -msgstr "" - msgid "Save anyway" msgstr "" diff --git a/package.json b/package.json index 5f602289209..a7709be064d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.175.0", - "@gitlab/ui": "21.44.0", + "@gitlab/ui": "22.0.3", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-3", "@rails/ujs": "^6.0.3-2", diff --git a/spec/features/admin/admin_dev_ops_report_spec.rb b/spec/features/admin/admin_dev_ops_report_spec.rb index cc98a275b74..3b2c9d75870 100644 --- a/spec/features/admin/admin_dev_ops_report_spec.rb +++ b/spec/features/admin/admin_dev_ops_report_spec.rb @@ -3,10 +3,6 @@ require 'spec_helper' RSpec.describe 'DevOps Report page', :js do - tabs_selector = '.js-devops-tabs' - tab_item_selector = '.js-devops-tab-item' - active_tab_selector = '.nav-link.active' - before do sign_in(create(:admin)) end @@ -16,12 +12,6 @@ RSpec.describe 'DevOps Report page', :js do stub_feature_flags(devops_adoption: false) end - it 'does not show the tabbed layout' do - visit admin_dev_ops_report_path - - expect(page).not_to have_selector tabs_selector - end - it 'has dismissable intro callout' do visit admin_dev_ops_report_path @@ -73,92 +63,4 @@ RSpec.describe 'DevOps Report page', :js do end end end - - context 'with devops_adoption feature flag enabled' do - it 'shows the tabbed layout' do - visit admin_dev_ops_report_path - - expect(page).to have_selector tabs_selector - end - - it 'shows the correct tabs' do - visit admin_dev_ops_report_path - - within tabs_selector do - expect(page.all(:css, tab_item_selector).length).to be(2) - expect(page).to have_text 'DevOps Score Adoption' - end - end - - it 'defaults to the DevOps Score tab' do - visit admin_dev_ops_report_path - - within tabs_selector do - expect(page).to have_selector active_tab_selector, text: 'DevOps Score' - end - end - - it 'displays the Adoption tab content when selected' do - visit admin_dev_ops_report_path - - click_link 'Adoption' - - within tabs_selector do - expect(page).to have_selector active_tab_selector, text: 'Adoption' - end - end - - context 'the devops score tab' do - it 'has dismissable intro callout' do - visit admin_dev_ops_report_path - - expect(page).to have_content 'Introducing Your DevOps Report' - - find('.js-close-callout').click - - expect(page).not_to have_content 'Introducing Your DevOps Report' - end - - context 'when usage ping is disabled' do - before do - stub_application_setting(usage_ping_enabled: false) - end - - it 'shows empty state' do - visit admin_dev_ops_report_path - - expect(page).to have_selector(".js-empty-state") - end - - it 'hides the intro callout' do - visit admin_dev_ops_report_path - - expect(page).not_to have_content 'Introducing Your DevOps Report' - end - end - - context 'when there is no data to display' do - it 'shows empty state' do - stub_application_setting(usage_ping_enabled: true) - - visit admin_dev_ops_report_path - - expect(page).to have_content('Data is still calculating') - end - end - - context 'when there is data to display' do - it 'shows numbers for each metric' do - stub_application_setting(usage_ping_enabled: true) - create(:dev_ops_report_metric) - - visit admin_dev_ops_report_path - - expect(page).to have_content( - 'Issues created per active user 1.2 You 9.3 Lead 13.3%' - ) - end - end - end - end end diff --git a/spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js b/spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js deleted file mode 100644 index 978a358af43..00000000000 --- a/spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import DevopsAdoptionApp from '~/admin/dev_ops_report/components/devops_adoption_app.vue'; -import DevopsAdoptionEmptyState from '~/admin/dev_ops_report/components/devops_adoption_empty_state.vue'; - -describe('DevopsAdoptionApp', () => { - let wrapper; - - const createComponent = () => { - return shallowMount(DevopsAdoptionApp); - }; - - beforeEach(() => { - wrapper = createComponent(); - }); - - describe('default behaviour', () => { - it('displays the empty state', () => { - expect(wrapper.find(DevopsAdoptionEmptyState).exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js b/spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js deleted file mode 100644 index 91e99e6dffa..00000000000 --- a/spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlEmptyState, GlButton } from '@gitlab/ui'; -import DevopsAdoptionEmptyState from '~/admin/dev_ops_report/components/devops_adoption_empty_state.vue'; -import { DEVOPS_ADOPTION_STRINGS } from '~/admin/dev_ops_report/constants'; - -const emptyStateSvgPath = 'illustrations/monitoring/getting_started.svg'; - -describe('DevopsAdoptionEmptyState', () => { - let wrapper; - - const createComponent = (options = {}) => { - const { stubs = {} } = options; - return shallowMount(DevopsAdoptionEmptyState, { - provide: { - emptyStateSvgPath, - }, - stubs, - }); - }; - - const findEmptyState = () => wrapper.find(GlEmptyState); - const findEmptyStateAction = () => findEmptyState().find(GlButton); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('contains the correct svg', () => { - wrapper = createComponent(); - - expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath); - }); - - it('contains the correct text', () => { - wrapper = createComponent(); - - const emptyState = findEmptyState(); - - expect(emptyState.props('title')).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.title); - expect(emptyState.props('description')).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.description); - }); - - it('contains an overridden action button', () => { - wrapper = createComponent({ stubs: { GlEmptyState } }); - - const actionButton = findEmptyStateAction(); - - expect(actionButton.exists()).toBe(true); - expect(actionButton.text()).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.button); - }); -}); diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap index c553787d2c0..68b5eb12b99 100644 --- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap +++ b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap @@ -1,21 +1,90 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = ` -"<gl-form-stub class=\\"gl-mt-6\\"> +"<form class=\\"gl-mt-6\\"> <h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5> - <gl-form-group-stub id=\\"integration-type\\" label=\\"1. Select integration type\\" label-for=\\"integration-type\\"> - <gl-form-select-stub options=\\"[object Object],[object Object],[object Object],[object Object]\\" value=\\"\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span> - </gl-form-group-stub> - <b-collapse-stub tag=\\"div\\" class=\\"gl-mt-3\\"> - <gl-form-group-stub id=\\"name-integration\\" label=\\"2. Name integration\\" label-for=\\"name-integration\\"> - <b-form-input-stub value=\\"\\" placeholder=\\"Enter integration name\\" debounce=\\"0\\" type=\\"text\\" class=\\"gl-form-input\\"></b-form-input-stub> - </gl-form-group-stub> - <!----> - <div class=\\"gl-display-flex gl-justify-content-end\\"> - <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"reset\\" class=\\"gl-mr-3 js-no-auto-disable\\">Cancel</gl-button-stub> - <gl-button-stub category=\\"secondary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"submit\\" class=\\"gl-mr-1 js-no-auto-disable\\">Save and test payload</gl-button-stub> - <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"submit\\" class=\\"js-no-auto-disable\\">Save integration</gl-button-stub> + <div id=\\"integration-type\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-type__BV_label_\\" for=\\"integration-type\\" class=\\"d-block col-form-label\\">1. Select integration type</label> + <div class=\\"bv-no-focus-ring\\"><select class=\\"gl-form-select custom-select\\" id=\\"__BVID__8\\"> + <option value=\\"\\">Select integration type</option> + <option value=\\"HTTP\\">HTTP Endpoint</option> + <option value=\\"PROMETHEUS\\">External Prometheus</option> + <option value=\\"OPSGENIE\\">Opsgenie</option> + </select> <span class=\\"gl-text-gray-500\\">Learn more about our upcoming <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://gitlab.com/groups/gitlab-org/-/epics/4390\\" class=\\"gl-link gl-display-inline-block\\">integrations</a></span> + <!----> + <!----> + <!----> + </div> + </div> + <div class=\\"gl-mt-3 collapse\\" style=\\"display: none;\\" id=\\"__BVID__13\\"> + <div id=\\"name-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"name-integration__BV_label_\\" for=\\"name-integration\\" class=\\"d-block col-form-label\\">2. Name integration</label> + <div class=\\"bv-no-focus-ring\\"><input type=\\"text\\" placeholder=\\"Enter integration name\\" class=\\"gl-form-input form-control\\" id=\\"__BVID__18\\"> + <!----> + <!----> + <!----> + </div> + </div> + <div id=\\"integration-webhook\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-webhook__BV_label_\\" for=\\"integration-webhook\\" class=\\"d-block col-form-label\\">3. Set up webhook</label> + <div class=\\"bv-no-focus-ring\\"><span class=\\"gl-text-gray-500\\">Utilize the URL and authorization key below to authorize an external service to send Alerts to GitLab. Review your chosen services documentation to learn where to add these details, and the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html\\" class=\\"gl-link gl-display-inline-block\\">GitLab documentation</a> to learn more about configuring your endpoint.</span> <label class=\\"gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal\\"> + <div class=\\"gl-toggle-wrapper\\"><span class=\\"gl-toggle-label\\">Active</span> + <!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div> + <!----> + </label> + <!----> + <div class=\\"gl-my-4\\"><span> + Webhook URL + </span> + <div id=\\"url\\" readonly=\\"readonly\\"> + <div role=\\"group\\" class=\\"input-group\\"> + <!----> + <!----> <input id=\\"url\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\"> + <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\"> + <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\"> + <use href=\\"#copy-to-clipboard\\"></use> + </svg> + <!----></button></div> + <!----> + </div> + </div> + </div> + <div class=\\"gl-my-4\\"><span> + Authorization key + </span> + <div id=\\"authorization-key\\" readonly=\\"readonly\\" class=\\"gl-mb-2\\"> + <div role=\\"group\\" class=\\"input-group\\"> + <!----> + <!----> <input id=\\"authorization-key\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\"> + <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\"> + <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\"> + <use href=\\"#copy-to-clipboard\\"></use> + </svg> + <!----></button></div> + <!----> + </div> + </div> <button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn gl-mt-3 btn-default btn-md disabled gl-button\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Reset Key</span></button> + <!----> + </div> + <!----> + <!----> + <!----> + </div> </div> - </b-collapse-stub> -</gl-form-stub>" + <div id=\\"test-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"test-integration__BV_label_\\" for=\\"test-integration\\" class=\\"d-block col-form-label\\">4. Test integration(optional)</label> + <div class=\\"bv-no-focus-ring\\"><span class=\\"gl-text-gray-500\\">Provide an example payload from the monitoring tool you intend to integrate with to send a test alert to the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"http://invalid\\" class=\\"gl-link gl-display-inline-block\\">alerts page</a>. This payload can be used to test the integration using the \\"save and test payload\\" button.</span> <textarea id=\\"test-integration\\" disabled=\\"disabled\\" placeholder=\\"Enter test alert JSON....\\" wrap=\\"soft\\" class=\\"gl-form-input gl-form-textarea gl-my-4 form-control is-valid\\" style=\\"resize: none; overflow-y: scroll;\\"></textarea> + <!----> + <!----> + <!----> + </div> + </div> + <!----> + <div class=\\"gl-display-flex gl-justify-content-end\\"><button type=\\"reset\\" class=\\"btn gl-mr-3 js-no-auto-disable btn-default btn-md gl-button\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Cancel</span></button> <button type=\\"button\\" class=\\"btn gl-mr-1 js-no-auto-disable btn-success btn-md gl-button btn-success-secondary\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Save integration</span></button></div> + </div> +</form>" `; diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap index b7b4d96548e..17188f73677 100644 --- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap +++ b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AlertsSettingsForm with default values renders the initial template 1`] = ` +exports[`AlertsSettingsFormOld with default values renders the initial template 1`] = ` "<gl-form-stub> <h5 class=\\"gl-font-lg gl-my-5\\"></h5> <!----> @@ -13,10 +13,10 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] </p> </div> <gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\"> - <gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span> + <gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"HTTP\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span> </gl-form-group-stub> - <gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\"> - <toggle-button-stub id=\\"activated\\"></toggle-button-stub> + <gl-form-group-stub label=\\"Active\\" label-for=\\"active\\"> + <toggle-button-stub id=\\"active\\"></toggle-button-stub> </gl-form-group-stub> <!----> <gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\"> @@ -25,7 +25,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] </span> </gl-form-group-stub> <gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\"> - <gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub> + <gl-form-input-group-stub value=\\"\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub> <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub> <gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\"> Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in. diff --git a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js index a71560d9c7c..51d529c5e70 100644 --- a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js +++ b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js @@ -1,34 +1,38 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlForm, GlFormSelect, GlCollapse, GlFormInput } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { GlForm, GlFormSelect, GlCollapse, GlFormInput, GlToggle } from '@gitlab/ui'; import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue'; import { defaultAlertSettingsConfig } from './util'; +import { typeSet } from '~/alerts_settings/constants'; describe('AlertsSettingsFormNew', () => { let wrapper; - const createComponent = ( - { methods } = {}, - data, + const createComponent = ({ + data = {}, + props = { loading: false }, multipleHttpIntegrationsCustomMapping = false, - ) => { - wrapper = shallowMount(AlertsSettingsForm, { + } = {}) => { + wrapper = mount(AlertsSettingsForm, { data() { return { ...data }; }, + propsData: { + ...props, + }, provide: { glFeatures: { multipleHttpIntegrationsCustomMapping }, ...defaultAlertSettingsConfig, }, - methods, - stubs: { GlCollapse, GlFormInput }, }); }; const findForm = () => wrapper.find(GlForm); const findSelect = () => wrapper.find(GlFormSelect); const findFormSteps = () => wrapper.find(GlCollapse); - const findFormName = () => wrapper.find(GlFormInput); + const findFormFields = () => wrapper.findAll(GlFormInput); + const findFormToggle = () => wrapper.find(GlToggle); const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`); + const findSubmitButton = () => wrapper.find(`[type = "submit"]`); afterEach(() => { if (wrapper) { @@ -53,17 +57,83 @@ describe('AlertsSettingsFormNew', () => { }); it('shows the rest of the form when the dropdown is used', async () => { - findSelect().vm.$emit('change', 'prometheus'); + const options = findSelect().findAll('option'); + await options.at(1).setSelected(); + + await wrapper.vm.$nextTick(); + + expect( + findFormFields() + .at(0) + .isVisible(), + ).toBe(true); + }); + }); + + describe('when form is invalid', () => { + // TODO, implement specs for when form is invalid + }); + + describe('when form is valid', () => { + beforeEach(() => { + createComponent({}); + }); + + it('allows for on-create-new-integration with the correct form values for HTTP', async () => { + const options = findSelect().findAll('option'); + await options.at(1).setSelected(); + + await findFormFields() + .at(0) + .setValue('Test integration'); + await findFormToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().exists()).toBe(true); + expect(findSubmitButton().text()).toBe('Save integration'); + + findForm().trigger('submit'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('on-create-new-integration')).toBeTruthy(); + expect(wrapper.emitted('on-create-new-integration')[0]).toEqual([ + { type: typeSet.http, variables: { name: 'Test integration', active: true } }, + ]); + }); + + it('allows for on-create-new-integration with the correct form values for PROMETHEUS', async () => { + const options = findSelect().findAll('option'); + await options.at(2).setSelected(); + + await findFormFields() + .at(0) + .setValue('Test integration'); + await findFormFields() + .at(1) + .setValue('https://test.com'); + await findFormToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().exists()).toBe(true); + expect(findSubmitButton().text()).toBe('Save integration'); + + findForm().trigger('submit'); await wrapper.vm.$nextTick(); - expect(findFormName().isVisible()).toBe(true); + expect(wrapper.emitted('on-create-new-integration')).toBeTruthy(); + expect(wrapper.emitted('on-create-new-integration')[0]).toEqual([ + { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } }, + ]); }); }); describe('Mapping builder section', () => { beforeEach(() => { - createComponent({}, {}); + createComponent({}); }); it('should NOT render when feature flag disabled', () => { @@ -71,7 +141,7 @@ describe('AlertsSettingsFormNew', () => { }); it('should render when feature flag is enabled', () => { - createComponent({}, {}, true); + createComponent({ multipleHttpIntegrationsCustomMapping: true }); expect(findMappingBuilderSection().exists()).toBe(true); }); }); diff --git a/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js index 0fb601b41d5..da4c74360d8 100644 --- a/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js +++ b/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js @@ -8,7 +8,7 @@ import { defaultAlertSettingsConfig } from './util'; jest.mock('~/alerts_settings/services'); -describe('AlertsSettingsForm', () => { +describe('AlertsSettingsFormOld', () => { let wrapper; const createComponent = ({ methods } = {}, data) => { @@ -113,7 +113,7 @@ describe('AlertsSettingsForm', () => { createComponent( {}, { - selectedIntegration: 'prometheus', + selectedIntegration: 'PROMETHEUS', }, ); }); @@ -127,9 +127,7 @@ describe('AlertsSettingsForm', () => { }); it('shows the correct default API URL', () => { - expect(findUrl().attributes('value')).toBe( - defaultAlertSettingsConfig.prometheus.prometheusUrl, - ); + expect(findUrl().attributes('value')).toBe(defaultAlertSettingsConfig.prometheus.url); }); }); @@ -138,7 +136,7 @@ describe('AlertsSettingsForm', () => { createComponent( {}, { - selectedIntegration: 'opsgenie', + selectedIntegration: 'OPSGENIE', }, ); }); diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js index 8c289cb0f1c..ce7bed0dc0c 100644 --- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js @@ -4,9 +4,16 @@ import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_ import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue'; import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue'; import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; +import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; +import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql'; +import createFlash from '~/flash'; import { defaultAlertSettingsConfig } from './util'; import mockIntegrations from './mocks/integrations.json'; +jest.mock('~/flash'); + +const projectPath = ''; + describe('AlertsSettingsWrapper', () => { let wrapper; @@ -25,6 +32,7 @@ describe('AlertsSettingsWrapper', () => { }, mocks: { $apollo: { + mutate: jest.fn(), query: jest.fn(), queries: { integrations: { @@ -79,5 +87,85 @@ describe('AlertsSettingsWrapper', () => { expect(findLoader().exists()).toBe(false); expect(findIntegrations()).toHaveLength(mockIntegrations.length); }); + + it('shows an error message when a user cannot create a new integration', () => { + createComponent({ + data: { integrations: { list: mockIntegrations } }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + expect(findLoader().exists()).toBe(false); + expect(findIntegrations()).toHaveLength(mockIntegrations.length); + }); + + it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => { + createComponent({ + data: { integrations: { list: mockIntegrations } }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { createHttpIntegrationMutation: { integration: { id: '1' } } }, + }); + wrapper.find(AlertsSettingsFormNew).vm.$emit('on-create-new-integration', { + type: 'HTTP', + variables: { name: 'Test 1', active: true }, + }); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: createHttpIntegrationMutation, + update: expect.anything(), + variables: { + name: 'Test 1', + active: true, + projectPath, + }, + }); + }); + + it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => { + createComponent({ + data: { integrations: { list: mockIntegrations } }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } }, + }); + wrapper.find(AlertsSettingsFormNew).vm.$emit('on-create-new-integration', { + type: 'PROMETHEUS', + variables: { apiUrl: 'https://test.com', active: true }, + }); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: createPrometheusIntegrationMutation, + update: expect.anything(), + variables: { + apiUrl: 'https://test.com', + active: true, + projectPath, + }, + }); + }); + + it('shows error alert when integration creation fails ', () => { + const errorMsg = 'Something went wrong'; + createComponent({ + data: { integrations: { list: mockIntegrations } }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); + wrapper.find(AlertsSettingsFormNew).vm.$emit('on-create-new-integration', {}); + + setImmediate(() => { + expect(createFlash).toHaveBeenCalledWith({ message: errorMsg }); + }); + }); }); }); diff --git a/spec/frontend/alerts_settings/util.js b/spec/frontend/alerts_settings/util.js index cbb98097b43..beb6a724f20 100644 --- a/spec/frontend/alerts_settings/util.js +++ b/spec/frontend/alerts_settings/util.js @@ -2,7 +2,7 @@ const PROMETHEUS_URL = '/prometheus/alerts/notify.json'; const GENERIC_URL = '/alerts/notify.json'; const KEY = 'abcedfg123'; const INVALID_URL = 'http://invalid'; -const ACTIVATED = false; +const ACTIVE = false; export const defaultAlertSettingsConfig = { generic: { @@ -11,18 +11,18 @@ export const defaultAlertSettingsConfig = { url: GENERIC_URL, alertsSetupUrl: INVALID_URL, alertsUsageUrl: INVALID_URL, - activated: ACTIVATED, + active: ACTIVE, }, prometheus: { authorizationKey: KEY, prometheusFormPath: INVALID_URL, - prometheusUrl: PROMETHEUS_URL, - activated: ACTIVATED, + url: PROMETHEUS_URL, + active: ACTIVE, }, opsgenie: { opsgenieMvcIsAvailable: true, formPath: INVALID_URL, - activated: ACTIVATED, + active: ACTIVE, opsgenieMvcTargetUrl: GENERIC_URL, }, }; diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 400f36f10a9..7fdd0d7d6db 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -118,6 +118,24 @@ describe('Api', () => { }); }); + describe('container registry', () => { + describe('containerRegistryDetails', () => { + it('fetch container registry details', async () => { + const expectedUrl = `foo`; + const apiResponse = {}; + + jest.spyOn(axios, 'get'); + jest.spyOn(Api, 'buildUrl').mockReturnValueOnce(expectedUrl); + mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse); + + const { data } = await Api.containerRegistryDetails(1); + + expect(data).toEqual(apiResponse); + expect(axios.get).toHaveBeenCalledWith(expectedUrl, {}); + }); + }); + }); + describe('group', () => { it('fetches a group', done => { const groupId = '123456'; diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 47d92d4a868..0af5ddd9764 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -1149,7 +1149,11 @@ describe('DiffsStoreActions', () => { file_hash: 'testhash', alternate_viewer: { name: updatedViewerName }, }; - const updatedViewer = { name: updatedViewerName, automaticallyCollapsed: false }; + const updatedViewer = { + name: updatedViewerName, + automaticallyCollapsed: false, + manuallyCollapsed: false, + }; const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }]; let renamedFile; let mock; diff --git a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js index edc38efdafb..b34fe7779e3 100644 --- a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js +++ b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js @@ -25,6 +25,8 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { propsData: { ...DEFAULT_PROPS, ...props }, }); + const findDropdown = () => wrapper.find(GlDropdown); + describe('with user lists', () => { const findDropdownItem = () => wrapper.find(GlDropdownItem); @@ -34,7 +36,7 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { }); it('should show the input for userListId with the correct value', () => { - const dropdownWrapper = wrapper.find(GlDropdown); + const dropdownWrapper = findDropdown(); expect(dropdownWrapper.exists()).toBe(true); expect(dropdownWrapper.props('text')).toBe(userList.name); }); @@ -87,11 +89,15 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { describe('without user lists', () => { beforeEach(() => { Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [] }); - wrapper = factory(); + wrapper = factory({ strategy: { ...userListStrategy, userList: null } }); }); it('should display a message that there are no user lists', () => { expect(wrapper.text()).toContain('There are no configured user lists'); }); + + it('should dispaly a message that no list has been selected', () => { + expect(findDropdown().text()).toContain('No user list selected'); + }); }); }); diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js index fb93ab06ca8..66efbf6446b 100644 --- a/spec/frontend/registry/explorer/stores/actions_spec.js +++ b/spec/frontend/registry/explorer/stores/actions_spec.js @@ -1,10 +1,11 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import { TEST_HOST } from 'helpers/test_constants'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; import * as actions from '~/registry/explorer/stores/actions'; import * as types from '~/registry/explorer/stores/mutation_types'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; import { reposServerResponse, registryServerResponse } from '../mock_data'; jest.mock('~/flash.js'); @@ -227,6 +228,47 @@ describe('Actions RegistryExplorer Store', () => { }); }); + describe('requestImageDetailsAndTagsList', () => { + it('sets the imageDetails and dispatch requestTagsList', done => { + const resolvedValue = { foo: 'bar' }; + jest.spyOn(Api, 'containerRegistryDetails').mockResolvedValue({ data: resolvedValue }); + + testAction( + actions.requestImageDetailsAndTagsList, + 1, + {}, + [ + { type: types.SET_MAIN_LOADING, payload: true }, + { type: types.SET_IMAGE_DETAILS, payload: resolvedValue }, + ], + [ + { + type: 'requestTagsList', + }, + ], + done, + ); + }); + + it('should create flash on error', done => { + jest.spyOn(Api, 'containerRegistryDetails').mockRejectedValue(); + testAction( + actions.requestImageDetailsAndTagsList, + 1, + {}, + [ + { type: types.SET_MAIN_LOADING, payload: true }, + { type: types.SET_MAIN_LOADING, payload: false }, + ], + [], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + }); + describe('request delete multiple tags', () => { const url = `project-path/registry/repository/foo/tags`; const params = window.btoa(JSON.stringify({ tags_path: `${url}?format=json` })); diff --git a/spec/frontend/registry/explorer/stores/mutations_spec.js b/spec/frontend/registry/explorer/stores/mutations_spec.js index 4ca0211cdc3..1908d3f0350 100644 --- a/spec/frontend/registry/explorer/stores/mutations_spec.js +++ b/spec/frontend/registry/explorer/stores/mutations_spec.js @@ -121,4 +121,13 @@ describe('Mutations Registry Explorer Store', () => { expect(mockState).toEqual(expectedState); }); }); + + describe('SET_IMAGE_DETAILS', () => { + it('should set imageDetails', () => { + const expectedState = { ...mockState, imageDetails: { foo: 'bar' } }; + mutations[types.SET_IMAGE_DETAILS](mockState, { foo: 'bar' }); + + expect(mockState).toEqual(expectedState); + }); + }); }); diff --git a/spec/frontend/registry/explorer/utils_spec.js b/spec/frontend/registry/explorer/utils_spec.js new file mode 100644 index 00000000000..77c14ccee65 --- /dev/null +++ b/spec/frontend/registry/explorer/utils_spec.js @@ -0,0 +1,19 @@ +import { pathGenerator } from '~/registry/explorer/utils'; + +describe('Utils', () => { + describe('pathGenerator', () => { + const imageDetails = { + path: 'foo/bar/baz', + name: 'baz', + id: 1, + }; + + it('returns the fetch url when no ending is passed', () => { + expect(pathGenerator(imageDetails)).toBe('/foo/bar/registry/repository/1/tags?format=json'); + }); + + it('returns the url with an ending when is passed', () => { + expect(pathGenerator(imageDetails, 'foo')).toBe('/foo/bar/registry/repository/1/foo'); + }); + }); +}); diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb index 2ed52716802..63f821da2bb 100644 --- a/spec/helpers/operations_helper_spec.rb +++ b/spec/helpers/operations_helper_spec.rb @@ -44,7 +44,7 @@ RSpec.describe OperationsHelper do 'prometheus_activated' => 'false', 'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json), 'disabled' => 'false', - 'project_path' => project_path(project) + 'project_path' => project.full_path ) end end diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..4be92e8608e --- /dev/null +++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('AWS/Deploy-ECS') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + let(:platform_target) { 'ECS' } + + before do + create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_target) + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + shared_examples 'no pipeline yaml error' do + it 'does not have any error' do + expect(pipeline.has_yaml_errors?).to be_falsey + end + end + + it_behaves_like 'no pipeline yaml error' + + it 'creates the expected jobs' do + expect(build_names).to include('production_ecs') + end + + context 'when running a pipeline for a branch' do + let(:pipeline_branch) { 'test_branch' } + + before do + project.repository.create_branch(pipeline_branch) + end + + it_behaves_like 'no pipeline yaml error' + + it 'creates the expected jobs' do + expect(build_names).to include('review_ecs', 'stop_review_ecs') + end + + context 'when deploying to ECS Fargate' do + let(:platform_target) { 'FARGATE' } + + it 'creates the expected jobs' do + expect(build_names).to include('review_fargate', 'stop_review_fargate') + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 740c6849952..793df55f45d 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -122,6 +122,15 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do end end end + + context 'when the platform target is EC2' do + let(:platform_value) { 'EC2' } + + it 'contains the build_artifact job, not the build job' do + expect(build_names).to include('build_artifact') + expect(build_names).not_to include('build') + end + end end context 'when the project has no active cluster' do diff --git a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb index 91299de0751..487b19a98e0 100644 --- a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb +++ b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb @@ -15,7 +15,8 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do path: '/api/v4/projects/1', params: { 'description': '[FILTERED]', - 'name': 'gitlab test' + 'name': 'gitlab test', + 'int': 42 }, host: 'localhost', remote_ip: '127.0.0.1', @@ -44,7 +45,8 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do expect(params).to eq([ { 'key' => 'description', 'value' => '[FILTERED]' }, - { 'key' => 'name', 'value' => 'gitlab test' } + { 'key' => 'name', 'value' => 'gitlab test' }, + { 'key' => 'int', 'value' => 42 } ]) end end diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb index 33ad0e12c37..8282ad9a070 100644 --- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb @@ -23,13 +23,13 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do end describe '#save' do + before do + project.uploads << upload + end + context 'when the project has uploads locally stored' do let(:upload) { create(:upload, :issuable_upload, :with_file, model: project) } - before do - project.uploads << upload - end - it 'does not cause errors' do manager.save @@ -74,6 +74,22 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do end end end + + context 'when upload is in object storage' do + before do + stub_uploads_object_storage(FileUploader) + allow(manager).to receive(:download_or_copy_upload).and_raise(Errno::ENAMETOOLONG) + end + + it 'ignores problematic upload and logs exception' do + expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Errno::ENAMETOOLONG), project_id: project.id) + + manager.save + + expect(shared.errors).to be_empty + expect(File).not_to exist(exported_file_path) + end + end end describe '#restore' do diff --git a/yarn.lock b/yarn.lock index 453bcbdd8c6..71fe6d7d750 100644 --- a/yarn.lock +++ b/yarn.lock @@ -866,10 +866,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.175.0.tgz#734f341784af1cd1d62d160a17bcdfb61ff7b04d" integrity sha512-gXpc87TGSXIzfAr4QER1Qw1v3P47pBO6BXkma52blgwXVmcFNe3nhQzqsqt66wKNzrIrk3lAcB4GUyPHbPVXpg== -"@gitlab/ui@21.44.0": - version "21.44.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.44.0.tgz#a8bf21434aa269eb198bcf88091eedb9d021e58f" - integrity sha512-WvHodwtJX2Xxj0D4QBG8KwQ21zpvDjL6rA1lRGbh5TO90p2NMYL+COi2WRqX+5Hj0bQhKcMEmpCQLqdO7Y2u4g== +"@gitlab/ui@22.0.3": + version "22.0.3" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-22.0.3.tgz#5c2cf8fa6cb95dea63b7ad390a310eb4b1dfc793" + integrity sha512-mTVNQTZwWtHJW03EpJEhdP+ZHYHFuIzamrskH7Sa1VdLce86zIeagi79tu2xZvf70CaQ7QhVZfVZIl5kJYMtfg== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |