diff options
author | Reuben Pereira <rpereira@gitlab.com> | 2019-03-01 17:51:54 +0300 |
---|---|---|
committer | Sean McGivern <sean@gitlab.com> | 2019-03-01 17:51:54 +0300 |
commit | 43e713eb41117138c13ee4b9279321ca4331a302 (patch) | |
tree | 1f2047b2ba5279fdad38b0da18db32ba350311d8 /app | |
parent | 4471ab81c8484d9942183bd8114a757b8630b8ec (diff) |
Refactor model and spec
- Move some specs into contexts
- Let get_slugs method take a parameter and return a specific slug.
- Add rescues when using Addressable::URI.
Diffstat (limited to 'app')
18 files changed, 701 insertions, 40 deletions
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue new file mode 100644 index 00000000000..50eb3e63b7c --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/components/app.vue @@ -0,0 +1,129 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlButton } from '@gitlab/ui'; +import ProjectDropdown from './project_dropdown.vue'; +import ErrorTrackingForm from './error_tracking_form.vue'; + +export default { + components: { ProjectDropdown, ErrorTrackingForm, GlButton }, + props: { + initialApiHost: { + type: String, + required: false, + default: '', + }, + initialEnabled: { + type: String, + required: true, + }, + initialProject: { + type: String, + required: false, + default: null, + }, + initialToken: { + type: String, + required: false, + default: '', + }, + listProjectsEndpoint: { + type: String, + required: true, + }, + operationsSettingsEndpoint: { + type: String, + required: true, + }, + }, + computed: { + ...mapGetters([ + 'dropdownLabel', + 'hasProjects', + 'invalidProjectLabel', + 'isProjectInvalid', + 'projectSelectionLabel', + ]), + ...mapState([ + 'apiHost', + 'connectError', + 'connectSuccessful', + 'enabled', + 'projects', + 'selectedProject', + 'settingsLoading', + 'token', + ]), + }, + created() { + this.setInitialState({ + apiHost: this.initialApiHost, + enabled: this.initialEnabled, + project: this.initialProject, + token: this.initialToken, + listProjectsEndpoint: this.listProjectsEndpoint, + operationsSettingsEndpoint: this.operationsSettingsEndpoint, + }); + }, + methods: { + ...mapActions([ + 'fetchProjects', + 'setInitialState', + 'updateApiHost', + 'updateEnabled', + 'updateSelectedProject', + 'updateSettings', + 'updateToken', + ]), + handleSubmit() { + this.updateSettings(); + }, + }, +}; +</script> + +<template> + <div> + <div class="form-check form-group"> + <input + id="error-tracking-enabled" + :checked="enabled" + class="form-check-input" + type="checkbox" + @change="updateEnabled($event.target.checked)" + /> + <label class="form-check-label" for="error-tracking-enabled">{{ + s__('ErrorTracking|Active') + }}</label> + </div> + <error-tracking-form + :api-host="apiHost" + :connect-error="connectError" + :connect-successful="connectSuccessful" + :token="token" + @handle-connect="fetchProjects" + @update-api-host="updateApiHost" + @update-token="updateToken" + /> + <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> + <gl-button + :disabled="settingsLoading" + class="js-error-tracking-button" + variant="success" + @click="handleSubmit" + > + {{ __('Save changes') }} + </gl-button> + </div> +</template> diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue new file mode 100644 index 00000000000..060d8e25227 --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue @@ -0,0 +1,91 @@ +<script> +import { GlButton, GlFormInput } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { GlButton, GlFormInput, Icon }, + props: { + apiHost: { + type: String, + required: true, + }, + connectError: { + type: Boolean, + required: true, + }, + connectSuccessful: { + type: Boolean, + required: true, + }, + token: { + type: String, + required: true, + }, + }, + computed: { + tokenInputState() { + return this.connectError ? false : null; + }, + }, +}; +</script> + +<template> + <div> + <div class="form-group"> + <label class="label-bold" for="error-tracking-api-host">{{ __('Sentry API URL') }}</label> + <div class="row"> + <div class="col-8 col-md-9 gl-pr-0"> + <gl-form-input + id="error-tracking-api-host" + :value="apiHost" + placeholder="https://mysentryserver.com" + @input="$emit('update-api-host', $event)" + /> + </div> + </div> + <p class="form-text text-muted"> + {{ s__('ErrorTracking|Find your hostname in your Sentry account settings page') }} + </p> + </div> + <div class="form-group" :class="{ 'gl-show-field-errors': connectError }"> + <label class="label-bold" for="error-tracking-token">{{ + s__('ErrorTracking|Auth Token') + }}</label> + <div class="row"> + <div class="col-8 col-md-9 gl-pr-0"> + <gl-form-input + id="error-tracking-token" + :value="token" + :state="tokenInputState" + @input="$emit('update-token', $event)" + /> + </div> + <div class="col-4 col-md-3 gl-pl-0"> + <gl-button + class="js-error-tracking-connect prepend-left-5" + @click="$emit('handle-connect')" + > + {{ __('Connect') }} + </gl-button> + <icon + v-show="connectSuccessful" + class="js-error-tracking-connect-success prepend-left-5 text-success align-middle" + :aria-label="__('Projects Successfully Retrieved')" + name="check-circle" + /> + </div> + </div> + <p v-if="connectError" class="gl-field-error"> + {{ s__('ErrorTracking|Connection has failed. Re-check Auth Token and try again.') }} + </p> + <p v-else class="form-text text-muted"> + {{ + s__( + "ErrorTracking|After adding your Auth Token, use the 'Connect' button to load projects", + ) + }} + </p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue new file mode 100644 index 00000000000..82df02afafd --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue @@ -0,0 +1,82 @@ +<script> +import { GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import { getDisplayName } from '../utils'; + +export default { + components: { + GlDropdown, + GlDropdownHeader, + GlDropdownItem, + Icon, + }, + props: { + dropdownLabel: { + type: String, + required: true, + }, + hasProjects: { + type: Boolean, + required: true, + }, + invalidProjectLabel: { + type: String, + required: true, + }, + isProjectInvalid: { + type: Boolean, + required: true, + }, + projects: { + type: Array, + required: true, + }, + selectedProject: { + type: Object, + required: false, + default: null, + }, + projectSelectionLabel: { + type: String, + required: true, + }, + token: { + type: String, + required: true, + }, + }, + methods: { + getDisplayName, + }, +}; +</script> + +<template> + <div :class="{ 'gl-show-field-errors': isProjectInvalid }"> + <label class="label-bold" for="project-dropdown">{{ __('Project') }}</label> + <div class="row"> + <gl-dropdown + id="project-dropdown" + class="col-8 col-md-9 gl-pr-0" + :disabled="!hasProjects" + menu-class="w-100 mw-100" + toggle-class="dropdown-menu-toggle w-100 gl-field-error-outline" + :text="dropdownLabel" + > + <gl-dropdown-item + v-for="project in projects" + :key="`${project.organizationSlug}.${project.slug}`" + class="w-100" + @click="$emit('select-project', project)" + >{{ getDisplayName(project) }}</gl-dropdown-item + > + </gl-dropdown> + </div> + <p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error"> + {{ invalidProjectLabel }} + </p> + <p v-else-if="!hasProjects" class="js-project-dropdown-label form-text text-muted"> + {{ projectSelectionLabel }} + </p> + </div> +</template> diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js new file mode 100644 index 00000000000..ce315963723 --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/index.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import ErrorTrackingSettings from './components/app.vue'; +import createStore from './store'; + +export default () => { + const formContainerEl = document.querySelector('.js-error-tracking-form'); + const { + dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint }, + } = formContainerEl; + + return new Vue({ + el: formContainerEl, + store: createStore(), + render(createElement) { + return createElement(ErrorTrackingSettings, { + props: { + initialApiHost: apiHost, + initialEnabled: enabled, + initialProject: project, + initialToken: token, + listProjectsEndpoint, + operationsSettingsEndpoint, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js new file mode 100644 index 00000000000..95105797807 --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/store/actions.js @@ -0,0 +1,91 @@ +import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { refreshCurrentPage } from '~/lib/utils/url_utility'; +import createFlash from '~/flash'; +import { transformFrontendSettings } from '../utils'; +import * as types from './mutation_types'; + +export const requestProjects = ({ commit }) => { + commit(types.RESET_CONNECT); +}; + +export const receiveProjectsSuccess = ({ commit }, projects) => { + commit(types.UPDATE_CONNECT_SUCCESS); + commit(types.RECEIVE_PROJECTS, projects); +}; + +export const receiveProjectsError = ({ commit }) => { + commit(types.UPDATE_CONNECT_ERROR); + commit(types.CLEAR_PROJECTS); +}; + +export const fetchProjects = ({ dispatch, state }) => { + dispatch('requestProjects'); + return axios + .post(state.listProjectsEndpoint, { + error_tracking_setting: { + api_host: state.apiHost, + token: state.token, + }, + }) + .then(({ data: { projects } }) => { + dispatch('receiveProjectsSuccess', projects); + }) + .catch(() => { + dispatch('receiveProjectsError'); + }); +}; + +export const requestSettings = ({ commit }) => { + commit(types.UPDATE_SETTINGS_LOADING, true); +}; + +export const receiveSettingsError = ({ commit }, { response = {} }) => { + const message = response.data && response.data.message ? response.data.message : ''; + + createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert'); + commit(types.UPDATE_SETTINGS_LOADING, false); +}; + +export const updateSettings = ({ dispatch, state }) => { + dispatch('requestSettings'); + return axios + .patch(state.operationsSettingsEndpoint, { + project: { + error_tracking_setting_attributes: { + ...transformFrontendSettings(state), + }, + }, + }) + .then(() => { + refreshCurrentPage(); + }) + .catch(err => { + dispatch('receiveSettingsError', err); + }); +}; + +export const updateApiHost = ({ commit }, apiHost) => { + commit(types.UPDATE_API_HOST, apiHost); + commit(types.RESET_CONNECT); +}; + +export const updateEnabled = ({ commit }, enabled) => { + commit(types.UPDATE_ENABLED, enabled); +}; + +export const updateToken = ({ commit }, token) => { + commit(types.UPDATE_TOKEN, token); + commit(types.RESET_CONNECT); +}; + +export const updateSelectedProject = ({ commit }, selectedProject) => { + commit(types.UPDATE_SELECTED_PROJECT, selectedProject); +}; + +export const setInitialState = ({ commit }, data) => { + commit(types.SET_INITIAL_STATE, data); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js new file mode 100644 index 00000000000..a008b181907 --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/store/getters.js @@ -0,0 +1,44 @@ +import _ from 'underscore'; +import { __, s__, sprintf } from '~/locale'; +import { getDisplayName } from '../utils'; + +export const hasProjects = state => !!state.projects && state.projects.length > 0; + +export const isProjectInvalid = (state, getters) => + !!state.selectedProject && + getters.hasProjects && + !state.projects.some(project => _.isMatch(state.selectedProject, project)); + +export const dropdownLabel = (state, getters) => { + if (state.selectedProject !== null) { + return getDisplayName(state.selectedProject); + } + if (!getters.hasProjects) { + return s__('ErrorTracking|No projects available'); + } + return s__('ErrorTracking|Select project'); +}; + +export const invalidProjectLabel = state => { + if (state.selectedProject) { + return sprintf( + __('Project "%{name}" is no longer available. Select another project to continue.'), + { + name: state.selectedProject.name, + }, + ); + } + return ''; +}; + +export const projectSelectionLabel = state => { + if (state.token) { + return s__( + "ErrorTracking|Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.", + ); + } + return s__('ErrorTracking|To enable project selection, enter a valid Auth Token'); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/error_tracking_settings/store/index.js b/app/assets/javascripts/error_tracking_settings/store/index.js new file mode 100644 index 00000000000..560f265a2ea --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/store/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import createState from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default () => + new Vuex.Store({ + state: createState(), + actions, + getters, + mutations, + }); diff --git a/app/assets/javascripts/error_tracking_settings/store/mutation_types.js b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js new file mode 100644 index 00000000000..b4f8a237947 --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js @@ -0,0 +1,11 @@ +export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; +export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; +export const RECEIVE_PROJECTS = 'RECEIVE_PROJECTS'; +export const RESET_CONNECT = 'RESET_CONNECT'; +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_SELECTED_PROJECT = 'UPDATE_SELECTED_PROJECT'; +export const UPDATE_SETTINGS_LOADING = 'UPDATE_SETTINGS_LOADING'; +export const UPDATE_TOKEN = 'UPDATE_TOKEN'; diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js new file mode 100644 index 00000000000..4089d1ee94e --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js @@ -0,0 +1,61 @@ +import _ from 'underscore'; +import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; +import * as types from './mutation_types'; +import { projectKeys } from '../utils'; + +export default { + [types.CLEAR_PROJECTS](state) { + state.projects = []; + }, + [types.RECEIVE_PROJECTS](state, projects) { + state.projects = projects + .map(convertObjectPropsToCamelCase) + // The `pick` strips out extra properties returned from Sentry. + // Such properties could be problematic later, e.g. when checking whether `projects` contains `selectedProject` + .map(project => _.pick(project, projectKeys)); + }, + [types.RESET_CONNECT](state) { + state.connectSuccessful = false; + state.connectError = false; + }, + [types.SET_INITIAL_STATE]( + state, + { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint }, + ) { + state.enabled = parseBoolean(enabled); + state.apiHost = apiHost; + state.token = token; + state.listProjectsEndpoint = listProjectsEndpoint; + state.operationsSettingsEndpoint = operationsSettingsEndpoint; + + if (project) { + state.selectedProject = _.pick( + convertObjectPropsToCamelCase(JSON.parse(project)), + projectKeys, + ); + } + }, + [types.UPDATE_API_HOST](state, apiHost) { + state.apiHost = apiHost; + }, + [types.UPDATE_ENABLED](state, enabled) { + state.enabled = enabled; + }, + [types.UPDATE_TOKEN](state, token) { + state.token = token; + }, + [types.UPDATE_SELECTED_PROJECT](state, selectedProject) { + state.selectedProject = selectedProject; + }, + [types.UPDATE_SETTINGS_LOADING](state, settingsLoading) { + state.settingsLoading = settingsLoading; + }, + [types.UPDATE_CONNECT_SUCCESS](state) { + state.connectSuccessful = true; + state.connectError = false; + }, + [types.UPDATE_CONNECT_ERROR](state) { + state.connectSuccessful = false; + state.connectError = true; + }, +}; diff --git a/app/assets/javascripts/error_tracking_settings/store/state.js b/app/assets/javascripts/error_tracking_settings/store/state.js new file mode 100644 index 00000000000..98219d33f4d --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/store/state.js @@ -0,0 +1,12 @@ +export default () => ({ + apiHost: '', + enabled: false, + token: '', + projects: [], + selectedProject: null, + settingsLoading: false, + connectSuccessful: false, + connectError: false, + listProjectsEndpoint: '', + operationsSettingsEndpoint: '', +}); diff --git a/app/assets/javascripts/error_tracking_settings/utils.js b/app/assets/javascripts/error_tracking_settings/utils.js new file mode 100644 index 00000000000..6613e04ee0e --- /dev/null +++ b/app/assets/javascripts/error_tracking_settings/utils.js @@ -0,0 +1,18 @@ +export const projectKeys = ['name', 'organizationName', 'organizationSlug', 'slug']; + +export const transformFrontendSettings = ({ apiHost, enabled, token, selectedProject }) => { + const project = selectedProject + ? { + slug: selectedProject.slug, + name: selectedProject.name, + organization_name: selectedProject.organizationName, + organization_slug: selectedProject.organizationSlug, + } + : null; + + return { api_host: apiHost || null, enabled, token: token || null, project }; +}; + +export const getDisplayName = project => `${project.organizationName} | ${project.name}`; + +export default () => {}; diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js new file mode 100644 index 00000000000..73c745179be --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -0,0 +1,5 @@ +import mountErrorTrackingForm from '~/error_tracking_settings'; + +document.addEventListener('DOMContentLoaded', () => { + mountErrorTrackingForm(); +}); diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 521ec2acebb..7276964b6e1 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -14,16 +14,37 @@ module Projects def update result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute + render_update_response(result) + end + + private + + # overridden in EE + def render_update_response(result) + respond_to do |format| + format.json do + render_update_json_response(result) + end + end + end + + def render_update_json_response(result) if result[:status] == :success flash[:notice] = _('Your changes have been saved') - redirect_to project_settings_operations_path(@project) + render json: { + status: result[:status] + } else - render 'show' + render( + status: result[:http_status] || :bad_request, + json: { + status: result[:status], + message: result[:message] + } + ) end end - private - def error_tracking_setting @error_tracking_setting ||= project.error_tracking_setting || project.build_error_tracking_setting @@ -35,7 +56,14 @@ module Projects # overridden in EE def permitted_project_params - { error_tracking_setting_attributes: [:enabled, :api_url, :token] } + { + error_tracking_setting_attributes: [ + :enabled, + :api_host, + :token, + project: [:slug, :name, :organization_slug, :organization_name] + ] + } end def check_license diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c400302cda3..2ac8ddc5244 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -284,6 +284,20 @@ module ProjectsHelper can?(current_user, :read_environment, @project) end + def error_tracking_setting_project_json + setting = @project.error_tracking_setting + + return if setting.blank? || setting.project_slug.blank? || + setting.organization_slug.blank? + + { + name: setting.project_name, + organization_name: setting.organization_name, + organization_slug: setting.organization_slug, + slug: setting.project_slug + }.to_json + end + private def get_project_nav_tabs(project, current_user) diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 57283a78ea9..1e2bd3bda7f 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -2,19 +2,30 @@ module ErrorTracking class ProjectErrorTrackingSetting < ActiveRecord::Base + include Gitlab::Utils::StrongMemoize include ReactiveCaching + API_URL_PATH_REGEXP = %r{ + \A + (?<prefix>/api/0/projects/+) + (?: + (?<organization>[^/]+)/+ + (?<project>[^/]+)/* + )? + \z + }x + self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } belongs_to :project validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true - validates :api_url, presence: true, if: :enabled + validates :api_url, presence: { message: 'is a required field' }, if: :enabled validate :validate_api_url_path, if: :enabled - validates :token, presence: true, if: :enabled + validates :token, presence: { message: 'is a required field' }, if: :enabled attr_encrypted :token, mode: :per_attribute_iv, @@ -23,6 +34,11 @@ module ErrorTracking after_save :clear_reactive_cache! + def api_url=(value) + super + clear_memoization(:api_url_slugs) + end + def project_name super || project_name_from_slug end @@ -40,6 +56,8 @@ module ErrorTracking end def self.build_api_url_from(api_host:, project_slug:, organization_slug:) + return if api_host.blank? + uri = Addressable::URI.parse("#{api_host}/api/0/projects/#{organization_slug}/#{project_slug}/") uri.path = uri.path.squeeze('/') @@ -100,34 +118,39 @@ module ErrorTracking end def project_slug_from_api_url - extract_slug(:project) + api_url_slug(:project) end def organization_slug_from_api_url - extract_slug(:organization) + api_url_slug(:organization) + end + + def api_url_slug(capture) + slugs = strong_memoize(:api_url_slugs) { extract_api_url_slugs || {} } + slugs[capture] end - def extract_slug(capture) + def extract_api_url_slugs return if api_url.blank? begin url = Addressable::URI.parse(api_url) rescue Addressable::URI::InvalidURIError - return nil + return end - @slug_match ||= url.path.match(%r{^/api/0/projects/+(?<organization>[^/]+)/+(?<project>[^/|$]+)}) || {} - @slug_match[capture] + url.path.match(API_URL_PATH_REGEXP) end def validate_api_url_path return if api_url.blank? - begin - unless Addressable::URI.parse(api_url).path.starts_with?('/api/0/projects') - errors.add(:api_url, 'path needs to start with /api/0/projects') - end - rescue Addressable::URI::InvalidURIError + unless api_url_slug(:prefix) + return errors.add(:api_url, 'is invalid') + end + + unless api_url_slug(:organization) + errors.add(:project, 'is a required field') end end end diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb index c6e8be0f2be..4e92353a13c 100644 --- a/app/services/error_tracking/list_projects_service.rb +++ b/app/services/error_tracking/list_projects_service.rb @@ -28,8 +28,8 @@ module ErrorTracking (project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting| setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( api_host: params[:api_host], - organization_slug: nil, - project_slug: nil + organization_slug: 'org', + project_slug: 'proj' ) setting.token = params[:token] diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb index abd6d8de750..aedf79c86d7 100644 --- a/app/services/projects/operations/update_service.rb +++ b/app/services/projects/operations/update_service.rb @@ -12,7 +12,28 @@ module Projects private def project_update_params - params.slice(:error_tracking_setting_attributes) + error_tracking_params + end + + def error_tracking_params + settings = params[:error_tracking_setting_attributes] + return {} if settings.blank? + + api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( + api_host: settings[:api_host], + project_slug: settings.dig(:project, :slug), + organization_slug: settings.dig(:project, :organization_slug) + ) + + { + error_tracking_setting_attributes: { + api_url: api_url, + token: settings[:token], + enabled: settings[:enabled], + project_name: settings.dig(:project, :name), + organization_name: settings.dig(:project, :organization_name) + } + } end end end diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 4911e8d3770..6b15331db01 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -8,23 +8,11 @@ = _('Error Tracking') %p = _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.') + = link_to _('More information'), help_page_path('user/project/operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer' .settings-content - = form_for @project, url: project_settings_operations_path(@project), method: :patch do |f| - = form_errors(@project) - .form-group - = f.fields_for :error_tracking_setting_attributes, setting do |form| - .form-check.form-group - = form.check_box :enabled, class: 'form-check-input' - = form.label :enabled, _('Active'), class: 'form-check-label' - .form-group - = form.label :api_url, _('Sentry API URL'), class: 'label-bold' - = form.url_field :api_url, class: 'form-control', placeholder: _('http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/') - %p.form-text.text-muted - = _('Enter your Sentry API URL') - .form-group - = form.label :token, _('Auth Token'), class: 'label-bold' - = form.text_field :token, class: 'form-control' - %p.form-text.text-muted - = _('Find and manage Auth Tokens in your Sentry account settings page.') - - = f.submit _('Save changes'), class: 'btn btn-success' + .js-error-tracking-form{ data: { list_projects_endpoint: list_projects_project_error_tracking_index_path(@project, format: :json), + operations_settings_endpoint: project_settings_operations_path(@project), + project: error_tracking_setting_project_json, + api_host: setting.api_host, + enabled: setting.enabled.to_json, + token: setting.token } } |