Commit 30a156cd authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '263357-core-error-message' into 'master'

Resolve "No additional integrations for Core users"

See merge request gitlab-org/gitlab!46889
parents a4d3e18f 9cfaccf4
...@@ -33,6 +33,9 @@ export default { ...@@ -33,6 +33,9 @@ export default {
step1: { step1: {
label: s__('AlertSettings|1. Select integration type'), label: s__('AlertSettings|1. Select integration type'),
help: s__('AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}'), help: s__('AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}'),
enterprise: s__(
'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.',
),
}, },
step2: { step2: {
label: s__('AlertSettings|2. Name integration'), label: s__('AlertSettings|2. Name integration'),
...@@ -107,6 +110,10 @@ export default { ...@@ -107,6 +110,10 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
canAddIntegration: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -236,15 +243,24 @@ export default { ...@@ -236,15 +243,24 @@ export default {
> >
<gl-form-select <gl-form-select
v-model="selectedIntegration" v-model="selectedIntegration"
:disabled="currentIntegration !== null" :disabled="currentIntegration !== null || !canAddIntegration"
:options="options" :options="options"
@change="integrationTypeSelect" @change="integrationTypeSelect"
/> />
<div class="gl-my-4">
<alert-settings-form-help-block <alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.step1.help" :message="$options.i18n.integrationFormSteps.step1.help"
link="https://gitlab.com/groups/gitlab-org/-/epics/4390" link="https://gitlab.com/groups/gitlab-org/-/epics/4390"
/> />
</div>
<div v-if="!canAddIntegration" class="gl-my-4" data-testid="multi-integrations-not-supported">
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.step1.enterprise"
link="https://about.gitlab.com/pricing"
/>
</div>
</gl-form-group> </gl-form-group>
<gl-collapse v-model="formVisible" class="gl-mt-3"> <gl-collapse v-model="formVisible" class="gl-mt-3">
<gl-form-group <gl-form-group
......
...@@ -19,6 +19,11 @@ import { ...@@ -19,6 +19,11 @@ import {
updateStoreAfterIntegrationDelete, updateStoreAfterIntegrationDelete,
updateStoreAfterIntegrationAdd, updateStoreAfterIntegrationAdd,
} from '../utils/cache_updates'; } from '../utils/cache_updates';
import {
DELETE_INTEGRATION_ERROR,
ADD_INTEGRATION_ERROR,
RESET_INTEGRATION_TOKEN_ERROR,
} from '../utils/error_messages';
export default { export default {
typeSet, typeSet,
...@@ -44,6 +49,9 @@ export default { ...@@ -44,6 +49,9 @@ export default {
projectPath: { projectPath: {
default: '', default: '',
}, },
multiIntegrations: {
default: false,
},
}, },
apollo: { apollo: {
integrations: { integrations: {
...@@ -91,6 +99,9 @@ export default { ...@@ -91,6 +99,9 @@ export default {
}, },
]; ];
}, },
canAddIntegration() {
return this.multiIntegrations || this.integrations?.list?.length < 2;
},
}, },
methods: { methods: {
createNewIntegration({ type, variables }) { createNewIntegration({ type, variables }) {
...@@ -121,8 +132,8 @@ export default { ...@@ -121,8 +132,8 @@ export default {
type: FLASH_TYPES.SUCCESS, type: FLASH_TYPES.SUCCESS,
}); });
}) })
.catch(err => { .catch(() => {
createFlash({ message: err }); createFlash({ message: ADD_INTEGRATION_ERROR });
}) })
.finally(() => { .finally(() => {
this.isUpdating = false; this.isUpdating = false;
...@@ -187,8 +198,8 @@ export default { ...@@ -187,8 +198,8 @@ export default {
}); });
}, },
) )
.catch(err => { .catch(() => {
createFlash({ message: err }); createFlash({ message: RESET_INTEGRATION_TOKEN_ERROR });
}) })
.finally(() => { .finally(() => {
this.isUpdating = false; this.isUpdating = false;
...@@ -222,9 +233,9 @@ export default { ...@@ -222,9 +233,9 @@ export default {
type: FLASH_TYPES.SUCCESS, type: FLASH_TYPES.SUCCESS,
}); });
}) })
.catch(err => { .catch(() => {
this.errored = true; this.errored = true;
createFlash({ message: err }); createFlash({ message: DELETE_INTEGRATION_ERROR });
}) })
.finally(() => { .finally(() => {
this.isUpdating = false; this.isUpdating = false;
...@@ -249,6 +260,7 @@ export default { ...@@ -249,6 +260,7 @@ export default {
v-if="glFeatures.httpIntegrationsList" v-if="glFeatures.httpIntegrationsList"
:loading="isUpdating" :loading="isUpdating"
:current-integration="currentIntegration" :current-integration="currentIntegration"
:can-add-integration="canAddIntegration"
@create-new-integration="createNewIntegration" @create-new-integration="createNewIntegration"
@update-integration="updateIntegration" @update-integration="updateIntegration"
@reset-token="resetToken" @reset-token="resetToken"
......
...@@ -29,19 +29,16 @@ export default el => { ...@@ -29,19 +29,16 @@ export default el => {
opsgenieMvcEnabled, opsgenieMvcEnabled,
opsgenieMvcTargetUrl, opsgenieMvcTargetUrl,
projectPath, projectPath,
multiIntegrations,
} = el.dataset; } = el.dataset;
const resolvers = {};
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(resolvers, {
{},
{
cacheConfig: {}, cacheConfig: {},
}, assumeImmutableResults: true,
), }),
});
apolloProvider.clients.defaultClient.cache.writeData({
data: {},
}); });
return new Vue({ return new Vue({
...@@ -70,6 +67,7 @@ export default el => { ...@@ -70,6 +67,7 @@ export default el => {
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable), opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
}, },
projectPath, projectPath,
multiIntegrations: parseBoolean(multiIntegrations),
}, },
apolloProvider, apolloProvider,
components: { components: {
......
...@@ -7,3 +7,7 @@ export const DELETE_INTEGRATION_ERROR = s__( ...@@ -7,3 +7,7 @@ export const DELETE_INTEGRATION_ERROR = s__(
export const ADD_INTEGRATION_ERROR = s__( export const ADD_INTEGRATION_ERROR = s__(
'AlertsIntegrations|The integration could not be added. Please try again.', 'AlertsIntegrations|The integration could not be added. Please try again.',
); );
export const RESET_INTEGRATION_TOKEN_ERROR = s__(
'AlertsIntegrations|The integration token could not be reset. Please try again.',
);
...@@ -30,7 +30,8 @@ module OperationsHelper ...@@ -30,7 +30,8 @@ module OperationsHelper
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'), '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), 'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s, 'disabled' => disabled.to_s,
'project_path' => @project.full_path 'project_path' => @project.full_path,
'multi_integrations' => 'false'
} }
end end
......
...@@ -37,7 +37,7 @@ module EE ...@@ -37,7 +37,7 @@ module EE
override :alerts_settings_data override :alerts_settings_data
def alerts_settings_data(disabled: false) def alerts_settings_data(disabled: false)
super.merge(opsgenie_mvc_data) super.merge(opsgenie_mvc_data, alert_management_multiple_integrations_data)
end end
override :operations_settings_data override :operations_settings_data
...@@ -71,5 +71,11 @@ module EE ...@@ -71,5 +71,11 @@ module EE
'opsgenie_mvc_target_url' => alerts_service.opsgenie_mvc_target_url.to_s 'opsgenie_mvc_target_url' => alerts_service.opsgenie_mvc_target_url.to_s
} }
end end
def alert_management_multiple_integrations_data
{
'multi_integrations' => @project.feature_available?(:multiple_alert_http_integrations).to_s
}
end
end end
end end
...@@ -115,6 +115,24 @@ RSpec.describe OperationsHelper, :routing do ...@@ -115,6 +115,24 @@ RSpec.describe OperationsHelper, :routing do
it { is_expected.not_to include(opsgenie_keys) } it { is_expected.not_to include(opsgenie_keys) }
end end
end end
describe 'Multiple Integrations Support' do
before do
stub_licensed_features(multiple_alert_http_integrations: multi_integrations)
end
context 'when available' do
let(:multi_integrations) { true }
it { is_expected.to include('multi_integrations' => 'true') }
end
context 'when not available' do
let(:multi_integrations) { false }
it { is_expected.to include('multi_integrations' => 'false') }
end
end
end end
describe '#operations_settings_data' do describe '#operations_settings_data' do
......
...@@ -2566,6 +2566,9 @@ msgstr "" ...@@ -2566,6 +2566,9 @@ msgstr ""
msgid "AlertSettings|HTTP endpoint" msgid "AlertSettings|HTTP endpoint"
msgstr "" msgstr ""
msgid "AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations."
msgstr ""
msgid "AlertSettings|Integration" msgid "AlertSettings|Integration"
msgstr "" msgstr ""
...@@ -2680,6 +2683,9 @@ msgstr "" ...@@ -2680,6 +2683,9 @@ msgstr ""
msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list." msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
msgstr "" msgstr ""
msgid "AlertsIntegrations|The integration token could not be reset. Please try again."
msgstr ""
msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone." msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone."
msgstr "" msgstr ""
......
...@@ -9,7 +9,9 @@ exports[`AlertsSettingsFormNew with default values renders the initial template ...@@ -9,7 +9,9 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
<option value=\\"HTTP\\">HTTP Endpoint</option> <option value=\\"HTTP\\">HTTP Endpoint</option>
<option value=\\"PROMETHEUS\\">External Prometheus</option> <option value=\\"PROMETHEUS\\">External Prometheus</option>
<option value=\\"OPSGENIE\\">Opsgenie</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> </select>
<div class=\\"gl-my-4\\"><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>
<!---->
<!----> <!---->
<!----> <!---->
<!----> <!---->
......
...@@ -9,7 +9,7 @@ describe('AlertsSettingsFormNew', () => { ...@@ -9,7 +9,7 @@ describe('AlertsSettingsFormNew', () => {
const createComponent = ({ const createComponent = ({
data = {}, data = {},
props = { loading: false }, props = {},
multipleHttpIntegrationsCustomMapping = false, multipleHttpIntegrationsCustomMapping = false,
} = {}) => { } = {}) => {
wrapper = mount(AlertsSettingsForm, { wrapper = mount(AlertsSettingsForm, {
...@@ -17,6 +17,8 @@ describe('AlertsSettingsFormNew', () => { ...@@ -17,6 +17,8 @@ describe('AlertsSettingsFormNew', () => {
return { ...data }; return { ...data };
}, },
propsData: { propsData: {
loading: false,
canAddIntegration: true,
...props, ...props,
}, },
provide: { provide: {
...@@ -33,6 +35,8 @@ describe('AlertsSettingsFormNew', () => { ...@@ -33,6 +35,8 @@ describe('AlertsSettingsFormNew', () => {
const findFormToggle = () => wrapper.find(GlToggle); const findFormToggle = () => wrapper.find(GlToggle);
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`); const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`); const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
const findMultiSupportText = () =>
wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
...@@ -53,6 +57,7 @@ describe('AlertsSettingsFormNew', () => { ...@@ -53,6 +57,7 @@ describe('AlertsSettingsFormNew', () => {
it('render the initial form with only an integration type dropdown', () => { it('render the initial form with only an integration type dropdown', () => {
expect(findForm().exists()).toBe(true); expect(findForm().exists()).toBe(true);
expect(findSelect().exists()).toBe(true); expect(findSelect().exists()).toBe(true);
expect(findMultiSupportText().exists()).toBe(false);
expect(findFormSteps().attributes('visible')).toBeUndefined(); expect(findFormSteps().attributes('visible')).toBeUndefined();
}); });
...@@ -68,6 +73,12 @@ describe('AlertsSettingsFormNew', () => { ...@@ -68,6 +73,12 @@ describe('AlertsSettingsFormNew', () => {
.isVisible(), .isVisible(),
).toBe(true); ).toBe(true);
}); });
it('disabled the dropdown and shows help text when multi integrations are not supported', async () => {
createComponent({ props: { canAddIntegration: false } });
expect(findSelect().attributes('disabled')).toBe('disabled');
expect(findMultiSupportText().exists()).toBe(true);
});
}); });
describe('submitting integration form', () => { describe('submitting integration form', () => {
......
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper'; import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue'; import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue'; import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
...@@ -15,6 +16,10 @@ import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/ ...@@ -15,6 +16,10 @@ import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql'; import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql'; import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
import { typeSet } from '~/alerts_settings/constants'; import { typeSet } from '~/alerts_settings/constants';
import {
ADD_INTEGRATION_ERROR,
RESET_INTEGRATION_TOKEN_ERROR,
} from '~/alerts_settings/utils/error_messages';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { defaultAlertSettingsConfig } from './util'; import { defaultAlertSettingsConfig } from './util';
import mockIntegrations from './mocks/integrations.json'; import mockIntegrations from './mocks/integrations.json';
...@@ -294,31 +299,30 @@ describe('AlertsSettingsWrapper', () => { ...@@ -294,31 +299,30 @@ describe('AlertsSettingsWrapper', () => {
loading: false, loading: false,
}); });
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR);
wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', {}); wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', {});
setImmediate(() => { await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: errorMsg });
}); expect(createFlash).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR });
}); });
it('shows error alert when integration token reset fails ', () => { it('shows error alert when integration token reset fails ', async () => {
createComponent({ createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } }, provide: { glFeatures: { httpIntegrationsList: true } },
loading: false, loading: false,
}); });
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR);
wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', {}); wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', {});
setImmediate(() => { await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: errorMsg }); expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
});
}); });
it('shows error alert when integration update fails ', () => { it('shows error alert when integration update fails ', async () => {
createComponent({ createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } }, provide: { glFeatures: { httpIntegrationsList: true } },
...@@ -329,11 +333,10 @@ describe('AlertsSettingsWrapper', () => { ...@@ -329,11 +333,10 @@ describe('AlertsSettingsWrapper', () => {
wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {}); wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {});
setImmediate(() => { await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: errorMsg }); expect(createFlash).toHaveBeenCalledWith({ message: errorMsg });
}); });
}); });
});
describe('with mocked Apollo client', () => { describe('with mocked Apollo client', () => {
it('has a selection of integrations loaded via the getIntegrationsQuery', async () => { it('has a selection of integrations loaded via the getIntegrationsQuery', async () => {
......
...@@ -25,4 +25,6 @@ export const defaultAlertSettingsConfig = { ...@@ -25,4 +25,6 @@ export const defaultAlertSettingsConfig = {
active: ACTIVE, active: ACTIVE,
opsgenieMvcTargetUrl: GENERIC_URL, opsgenieMvcTargetUrl: GENERIC_URL,
}, },
projectPath: '',
multiIntegrations: true,
}; };
...@@ -44,7 +44,8 @@ RSpec.describe OperationsHelper do ...@@ -44,7 +44,8 @@ RSpec.describe OperationsHelper do
'prometheus_activated' => 'false', 'prometheus_activated' => 'false',
'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json), 'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json),
'disabled' => 'false', 'disabled' => 'false',
'project_path' => project.full_path 'project_path' => project.full_path,
'multi_integrations' => 'false'
) )
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment