Unsere GitLab-Installation wurde aktualisiert (Informationen zu den Neuerungen).

Commit 43e713eb authored by Reuben Pereira's avatar Reuben Pereira Committed by Sean McGivern

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.
parent 4471ab81
<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>
<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>
<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>
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,
},
});
},
});
};
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 () => {};
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 () => {};
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,
});
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';
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;
},
};
export default () => ({
apiHost: '',
enabled: false,
token: '',
projects: [],
selectedProject: null,
settingsLoading: false,
connectSuccessful: false,
connectError: false,
listProjectsEndpoint: '',
operationsSettingsEndpoint: '',
});
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 () => {};
import mountErrorTrackingForm from '~/error_tracking_settings';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
});
......@@ -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
......
......@@ -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)
......