Commit 487779ca authored by Dmitriy Zaporozhets (DZ)'s avatar Dmitriy Zaporozhets (DZ)

Merge branch 'dz-integrated-error-tracking-setting' into 'master'

Add integrated setting to error tracking

See merge request gitlab-org/gitlab!66845
parents e3d8019b 34cbeb38
<script>
import { GlButton, GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
import { GlButton, GlFormGroup, GlFormCheckbox, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import ErrorTrackingForm from './error_tracking_form.vue';
import ProjectDropdown from './project_dropdown.vue';
......@@ -10,6 +10,8 @@ export default {
GlButton,
GlFormCheckbox,
GlFormGroup,
GlFormRadioGroup,
GlFormRadio,
ProjectDropdown,
},
props: {
......@@ -22,6 +24,10 @@ export default {
type: String,
required: true,
},
initialIntegrated: {
type: String,
required: true,
},
initialProject: {
type: String,
required: false,
......@@ -49,12 +55,20 @@ export default {
'isProjectInvalid',
'projectSelectionLabel',
]),
...mapState(['enabled', 'projects', 'selectedProject', 'settingsLoading', 'token']),
...mapState([
'enabled',
'integrated',
'projects',
'selectedProject',
'settingsLoading',
'token',
]),
},
created() {
this.setInitialState({
apiHost: this.initialApiHost,
enabled: this.initialEnabled,
integrated: this.initialIntegrated,
project: this.initialProject,
token: this.initialToken,
listProjectsEndpoint: this.listProjectsEndpoint,
......@@ -62,7 +76,13 @@ export default {
});
},
methods: {
...mapActions(['setInitialState', 'updateEnabled', 'updateSelectedProject', 'updateSettings']),
...mapActions([
'setInitialState',
'updateEnabled',
'updateIntegrated',
'updateSelectedProject',
'updateSettings',
]),
handleSubmit() {
this.updateSettings();
},
......@@ -76,27 +96,44 @@ export default {
:label="s__('ErrorTracking|Enable error tracking')"
label-for="error-tracking-enabled"
>
<gl-form-checkbox
id="error-tracking-enabled"
:checked="enabled"
@change="updateEnabled($event)"
>
<gl-form-checkbox id="error-tracking-enabled" :checked="enabled" @change="updateEnabled">
{{ s__('ErrorTracking|Active') }}
</gl-form-checkbox>
</gl-form-group>
<error-tracking-form />
<div class="form-group">
<project-dropdown
:has-projects="hasProjects"
:invalid-project-label="invalidProjectLabel"
:is-project-invalid="isProjectInvalid"
:dropdown-label="dropdownLabel"
:project-selection-label="projectSelectionLabel"
:projects="projects"
:selected-project="selectedProject"
:token="token"
@select-project="updateSelectedProject"
/>
<gl-form-group
:label="s__('ErrorTracking|Error tracking backend')"
data-testid="tracking-backend-settings"
>
<gl-form-radio-group name="explicit" :checked="integrated" @change="updateIntegrated">
<gl-form-radio name="error-tracking-integrated" :value="false">
{{ __('Sentry') }}
<template #help>
{{ __('Requires you to deploy or set up cloud-hosted Sentry.') }}
</template>
</gl-form-radio>
<gl-form-radio name="error-tracking-integrated" :value="true">
{{ __('GitLab') }}
<template #help>
{{ __('Uses GitLab as a lightweight alternative to Sentry.') }}
</template>
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
<div v-if="!integrated" class="js-sentry-setting-form" data-testid="sentry-setting-form">
<error-tracking-form />
<div class="form-group">
<project-dropdown
:has-projects="hasProjects"
:invalid-project-label="invalidProjectLabel"
:is-project-invalid="isProjectInvalid"
:dropdown-label="dropdownLabel"
:project-selection-label="projectSelectionLabel"
:projects="projects"
:selected-project="selectedProject"
:token="token"
@select-project="updateSelectedProject"
/>
</div>
</div>
<gl-button
:disabled="settingsLoading"
......
......@@ -5,7 +5,15 @@ import createStore from './store';
export default () => {
const formContainerEl = document.querySelector('.js-error-tracking-form');
const {
dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
dataset: {
apiHost,
enabled,
integrated,
project,
token,
listProjectsEndpoint,
operationsSettingsEndpoint,
},
} = formContainerEl;
return new Vue({
......@@ -16,6 +24,7 @@ export default () => {
props: {
initialApiHost: apiHost,
initialEnabled: enabled,
initialIntegrated: integrated,
initialProject: project,
initialToken: token,
listProjectsEndpoint,
......
......@@ -79,6 +79,10 @@ export const updateEnabled = ({ commit }, enabled) => {
commit(types.UPDATE_ENABLED, enabled);
};
export const updateIntegrated = ({ commit }, integrated) => {
commit(types.UPDATE_INTEGRATED, integrated);
};
export const updateToken = ({ commit }, token) => {
commit(types.UPDATE_TOKEN, token);
commit(types.RESET_CONNECT);
......
......@@ -6,6 +6,7 @@ export const UPDATE_API_HOST = 'UPDATE_API_HOST';
export const UPDATE_CONNECT_ERROR = 'UPDATE_CONNECT_ERROR';
export const UPDATE_CONNECT_SUCCESS = 'UPDATE_CONNECT_SUCCESS';
export const UPDATE_ENABLED = 'UPDATE_ENABLED';
export const UPDATE_INTEGRATED = 'UPDATE_INTEGRATED';
export const UPDATE_SELECTED_PROJECT = 'UPDATE_SELECTED_PROJECT';
export const UPDATE_SETTINGS_LOADING = 'UPDATE_SETTINGS_LOADING';
export const UPDATE_TOKEN = 'UPDATE_TOKEN';
......
......@@ -20,9 +20,18 @@ export default {
},
[types.SET_INITIAL_STATE](
state,
{ apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
{
apiHost,
enabled,
integrated,
project,
token,
listProjectsEndpoint,
operationsSettingsEndpoint,
},
) {
state.enabled = parseBoolean(enabled);
state.integrated = parseBoolean(integrated);
state.apiHost = apiHost;
state.token = token;
state.listProjectsEndpoint = listProjectsEndpoint;
......@@ -38,6 +47,9 @@ export default {
[types.UPDATE_ENABLED](state, enabled) {
state.enabled = enabled;
},
[types.UPDATE_INTEGRATED](state, integrated) {
state.integrated = integrated;
},
[types.UPDATE_TOKEN](state, token) {
state.token = token;
},
......
export default () => ({
apiHost: '',
enabled: false,
integrated: false,
token: '',
projects: [],
isLoadingProjects: false,
......
export const projectKeys = ['name', 'organizationName', 'organizationSlug', 'slug'];
export const transformFrontendSettings = ({ apiHost, enabled, token, selectedProject }) => {
export const transformFrontendSettings = ({
apiHost,
enabled,
integrated,
token,
selectedProject,
}) => {
const project = selectedProject
? {
slug: selectedProject.slug,
......@@ -10,7 +16,7 @@ export const transformFrontendSettings = ({ apiHost, enabled, token, selectedPro
}
: null;
return { api_host: apiHost || null, enabled, token: token || null, project };
return { api_host: apiHost || null, enabled, integrated, token: token || null, project };
};
export const getDisplayName = (project) => `${project.organizationName} | ${project.slug}`;
......@@ -136,6 +136,7 @@ module Projects
error_tracking_setting_attributes: [
:enabled,
:integrated,
:api_host,
:token,
project: [:slug, :name, :organization_slug, :organization_name]
......
......@@ -94,6 +94,7 @@ module Projects
}
}
params[:error_tracking_setting_attributes][:token] = settings[:token] unless /\A\*+\z/.match?(settings[:token]) # Don't update token if we receive masked value
params[:error_tracking_setting_attributes][:integrated] = settings[:integrated] unless settings[:integrated].nil?
params
end
......
......@@ -17,4 +17,5 @@
project: error_tracking_setting_project_json,
api_host: setting.api_host,
enabled: setting.enabled.to_json,
integrated: setting.integrated.to_json,
token: setting.token.present? ? '*' * 12 : nil } }
......@@ -13366,6 +13366,9 @@ msgstr ""
msgid "ErrorTracking|Enable error tracking"
msgstr ""
msgid "ErrorTracking|Error tracking backend"
msgstr ""
msgid "ErrorTracking|If you self-host Sentry, enter your Sentry instance's full URL. If you use Sentry's hosted solution, enter https://sentry.io"
msgstr ""
......@@ -28637,6 +28640,9 @@ msgstr[1] ""
msgid "Requires values to meet regular expression requirements."
msgstr ""
msgid "Requires you to deploy or set up cloud-hosted Sentry."
msgstr ""
msgid "Requires your primary GitLab email address."
msgstr ""
......@@ -30517,6 +30523,9 @@ msgstr ""
msgid "Send service data"
msgstr ""
msgid "Sentry"
msgstr ""
msgid "Sentry API URL"
msgstr ""
......@@ -37013,6 +37022,9 @@ msgstr ""
msgid "UsersSelect|Unassigned"
msgstr ""
msgid "Uses GitLab as a lightweight alternative to Sentry."
msgstr ""
msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}"
msgstr ""
......
......@@ -150,6 +150,33 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
assert_text('Connection failed. Check Auth Token and try again.')
end
end
context 'integrated error tracking backend' do
it 'successfully fills and submits the form' do
visit project_settings_operations_path(project)
wait_for_requests
within '.js-error-tracking-settings' do
click_button('Expand')
end
expect(page).to have_content('Error tracking backend')
within '.js-error-tracking-settings' do
check('Active')
choose('GitLab')
end
expect(page).not_to have_content('Sentry API URL')
click_button('Save changes')
wait_for_requests
assert_text('Your changes have been saved')
end
end
end
context 'grafana integration settings form' do
......
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue';
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
......@@ -14,20 +17,31 @@ describe('error tracking settings app', () => {
let wrapper;
function mountComponent() {
wrapper = shallowMount(ErrorTrackingSettings, {
localVue,
store, // Override the imported store
propsData: {
initialEnabled: 'true',
initialApiHost: TEST_HOST,
initialToken: 'someToken',
initialProject: null,
listProjectsEndpoint: TEST_HOST,
operationsSettingsEndpoint: TEST_HOST,
},
});
wrapper = extendedWrapper(
shallowMount(ErrorTrackingSettings, {
localVue,
store, // Override the imported store
propsData: {
initialEnabled: 'true',
initialIntegrated: 'false',
initialApiHost: TEST_HOST,
initialToken: 'someToken',
initialProject: null,
listProjectsEndpoint: TEST_HOST,
operationsSettingsEndpoint: TEST_HOST,
},
}),
);
}
const findBackendSettingsSection = () => wrapper.findByTestId('tracking-backend-settings');
const findBackendSettingsRadioGroup = () =>
findBackendSettingsSection().findComponent(GlFormRadioGroup);
const findBackendSettingsRadioButtons = () =>
findBackendSettingsRadioGroup().findAllComponents(GlFormRadio);
const findElementWithText = (wrappers, text) => wrappers.filter((item) => item.text() === text);
const findSentrySettings = () => wrapper.findByTestId('sentry-setting-form');
beforeEach(() => {
store = createStore();
......@@ -62,4 +76,46 @@ describe('error tracking settings app', () => {
});
});
});
describe('tracking-backend settings', () => {
it('contains a form-group with the correct label', () => {
expect(findBackendSettingsSection().attributes('label')).toBe('Error tracking backend');
});
it('contains a radio group', () => {
expect(findBackendSettingsRadioGroup().exists()).toBe(true);
});
it('contains the correct radio buttons', () => {
expect(findBackendSettingsRadioButtons()).toHaveLength(2);
expect(findElementWithText(findBackendSettingsRadioButtons(), 'Sentry')).toHaveLength(1);
expect(findElementWithText(findBackendSettingsRadioButtons(), 'GitLab')).toHaveLength(1);
});
it('toggles the sentry-settings section when sentry is selected as a tracking-backend', async () => {
expect(findSentrySettings().exists()).toBe(true);
// set the "integrated" setting to "true"
findBackendSettingsRadioGroup().vm.$emit('change', true);
await nextTick();
expect(findSentrySettings().exists()).toBe(false);
});
it.each([true, false])(
'calls the `updateIntegrated` action when the setting changes to `%s`',
(integrated) => {
jest.spyOn(store, 'dispatch').mockImplementation();
expect(store.dispatch).toHaveBeenCalledTimes(0);
findBackendSettingsRadioGroup().vm.$emit('change', integrated);
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith('updateIntegrated', integrated);
},
);
});
});
......@@ -42,6 +42,7 @@ export const sampleBackendProject = {
export const sampleFrontendSettings = {
apiHost: 'apiHost',
enabled: false,
integrated: false,
token: 'token',
selectedProject: {
slug: normalizedProject.slug,
......@@ -54,6 +55,7 @@ export const sampleFrontendSettings = {
export const transformedSettings = {
api_host: 'apiHost',
enabled: false,
integrated: false,
token: 'token',
project: {
slug: normalizedProject.slug,
......@@ -71,6 +73,7 @@ export const defaultProps = {
export const initialEmptyState = {
apiHost: '',
enabled: false,
integrated: false,
project: null,
token: '',
listProjectsEndpoint: TEST_HOST,
......@@ -80,6 +83,7 @@ export const initialEmptyState = {
export const initialPopulatedState = {
apiHost: 'apiHost',
enabled: true,
integrated: true,
project: JSON.stringify(projectList[0]),
token: 'token',
listProjectsEndpoint: TEST_HOST,
......
......@@ -202,5 +202,11 @@ describe('error tracking settings actions', () => {
done,
);
});
it.each([true, false])('should set the `integrated` flag to `%s`', async (payload) => {
await testAction(actions.updateIntegrated, payload, state, [
{ type: types.UPDATE_INTEGRATED, payload },
]);
});
});
});
......@@ -25,6 +25,7 @@ describe('error tracking settings mutations', () => {
expect(state.apiHost).toEqual('');
expect(state.enabled).toEqual(false);
expect(state.integrated).toEqual(false);
expect(state.selectedProject).toEqual(null);
expect(state.token).toEqual('');
expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
......@@ -38,6 +39,7 @@ describe('error tracking settings mutations', () => {
expect(state.apiHost).toEqual('apiHost');
expect(state.enabled).toEqual(true);
expect(state.integrated).toEqual(true);
expect(state.selectedProject).toEqual(projectList[0]);
expect(state.token).toEqual('token');
expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
......@@ -78,5 +80,11 @@ describe('error tracking settings mutations', () => {
expect(state.connectSuccessful).toBe(false);
expect(state.connectError).toBe(false);
});
it.each([true, false])('should update `integrated` to `%s`', (integrated) => {
mutations[types.UPDATE_INTEGRATED](state, integrated);
expect(state.integrated).toBe(integrated);
});
});
});
......@@ -11,12 +11,14 @@ describe('error tracking settings utils', () => {
const emptyFrontendSettingsObject = {
apiHost: '',
enabled: false,
integrated: false,
token: '',
selectedProject: null,
};
const transformedEmptySettingsObject = {
api_host: null,
enabled: false,
integrated: false,
token: null,
project: null,
};
......
......@@ -153,6 +153,7 @@ RSpec.describe Projects::Operations::UpdateService do
{
error_tracking_setting_attributes: {
enabled: false,
integrated: true,
api_host: 'http://gitlab.com/',
token: 'token',
project: {
......@@ -174,6 +175,7 @@ RSpec.describe Projects::Operations::UpdateService do
project.reload
expect(project.error_tracking_setting).not_to be_enabled
expect(project.error_tracking_setting.integrated).to be_truthy
expect(project.error_tracking_setting.api_url).to eq(
'http://gitlab.com/api/0/projects/org/project/'
)
......@@ -206,6 +208,7 @@ RSpec.describe Projects::Operations::UpdateService do
{
error_tracking_setting_attributes: {
enabled: true,
integrated: true,
api_host: 'http://gitlab.com/',
token: 'token',
project: {
......@@ -222,6 +225,7 @@ RSpec.describe Projects::Operations::UpdateService do
expect(result[:status]).to eq(:success)
expect(project.error_tracking_setting).to be_enabled
expect(project.error_tracking_setting.integrated).to be_truthy
expect(project.error_tracking_setting.api_url).to eq(
'http://gitlab.com/api/0/projects/org/project/'
)
......
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