Commit 832cbc24 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Fatih Acet

Add self monitoring admin section

This adds a new admin section inside the
admin area-> settings -> metrics and profiling
that allows to create/delete  a self monitoring project
for the gitlab instance
parent de570d49
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select'; import projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
if (gon.features && gon.features.selfMonitoringProject) {
selfMonitor();
}
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
projectSelect(); projectSelect();
......
<script>
import Vue from 'vue';
import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { __, s__, sprintf } from '~/locale';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
Vue.use(GlToast);
export default {
components: {
GlFormGroup,
GlButton,
GlModal,
GlToggle,
},
formLabels: {
createProject: __('Create Project'),
},
data() {
return {
modalId: 'delete-self-monitor-modal',
};
},
computed: {
...mapState('selfMonitoring', [
'projectEnabled',
'projectCreated',
'showAlert',
'projectPath',
'loading',
'alertContent',
]),
selfMonitorEnabled: {
get() {
return this.projectEnabled;
},
set(projectEnabled) {
this.setSelfMonitor(projectEnabled);
},
},
selfMonitorProjectFullUrl() {
return `${getBaseURL()}/${this.projectPath}`;
},
selfMonitoringFormText() {
if (this.projectCreated) {
return sprintf(
s__(
'SelfMonitoring|Enabling this feature creates a %{projectLinkStart}project%{projectLinkEnd} that can be used to monitor the health of your instance.',
),
{
projectLinkStart: `<a href="${this.selfMonitorProjectFullUrl}">`,
projectLinkEnd: '</a>',
},
false,
);
}
return s__(
'SelfMonitoring|Enabling this feature creates a project that can be used to monitor the health of your instance.',
);
},
},
watch: {
selfMonitorEnabled() {
this.saveChangesSelfMonitorProject();
},
showAlert() {
let toastOptions = {
onComplete: () => {
this.resetAlert();
},
};
if (this.showAlert) {
if (this.alertContent.actionName && this.alertContent.actionName.length > 0) {
toastOptions = {
...toastOptions,
action: {
text: this.alertContent.actionText,
onClick: (_, toastObject) => {
this[this.alertContent.actionName]();
toastObject.goAway(0);
},
},
};
}
this.$toast.show(this.alertContent.message, toastOptions);
}
},
},
methods: {
...mapActions('selfMonitoring', [
'setSelfMonitor',
'createProject',
'deleteProject',
'resetAlert',
]),
hideSelfMonitorModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
this.setSelfMonitor(true);
},
showSelfMonitorModal() {
this.$root.$emit('bv::show::modal', this.modalId);
},
saveChangesSelfMonitorProject() {
if (this.projectCreated && !this.projectEnabled) {
this.showSelfMonitorModal();
} else {
this.createProject();
}
},
viewSelfMonitorProject() {
visitUrl(this.selfMonitorProjectFullUrl);
},
},
};
</script>
<template>
<section class="settings no-animate js-self-monitoring-settings">
<div class="settings-header">
<h4 class="js-section-header">
{{ s__('SelfMonitoring|Self monitoring') }}
</h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('SelfMonitoring|Enable or disable instance self monitoring') }}
</p>
</div>
<div class="settings-content">
<form name="self-monitoring-form">
<p v-html="selfMonitoringFormText"></p>
<gl-form-group :label="$options.formLabels.createProject" label-for="self-monitor-toggle">
<gl-toggle
v-model="selfMonitorEnabled"
:is-loading="loading"
name="self-monitor-toggle"
/>
</gl-form-group>
</form>
</div>
<gl-modal
:title="s__('SelfMonitoring|Disable self monitoring?')"
:modal-id="modalId"
:ok-title="__('Delete project')"
:cancel-title="__('Cancel')"
ok-variant="danger"
@ok="deleteProject"
@cancel="hideSelfMonitorModal"
>
<div>
{{
s__(
'SelfMonitoring|Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?',
)
}}
</div>
</gl-modal>
</section>
</template>
import Vue from 'vue';
import store from './store';
import SelfMonitorForm from './components/self_monitor_form.vue';
export default () => {
const el = document.querySelector('.js-self-monitoring-settings');
let selfMonitorProjectCreated;
if (el) {
selfMonitorProjectCreated = el.dataset.selfMonitoringProjectExists;
// eslint-disable-next-line no-new
new Vue({
el,
store: store({
projectEnabled: selfMonitorProjectCreated,
...el.dataset,
}),
render(createElement) {
return createElement(SelfMonitorForm);
},
});
}
};
import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
const TWO_MINUTES = 120000;
function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then(resp => {
if (resp.status === statusCodes.ACCEPTED) {
next();
} else {
stop(resp);
}
})
.catch(stop);
}, TWO_MINUTES);
}
export const setSelfMonitor = ({ commit }, enabled) => commit(types.SET_ENABLED, enabled);
export const createProject = ({ dispatch }) => dispatch('requestCreateProject');
export const resetAlert = ({ commit }) => commit(types.SET_SHOW_ALERT, false);
export const requestCreateProject = ({ dispatch, state, commit }) => {
commit(types.SET_LOADING, true);
axios
.post(state.createProjectEndpoint)
.then(resp => {
if (resp.status === statusCodes.ACCEPTED) {
dispatch('requestCreateProjectStatus', resp.data.job_id);
}
})
.catch(error => {
dispatch('requestCreateProjectError', error);
});
};
export const requestCreateProjectStatus = ({ dispatch, state }, jobId) => {
backOffRequest(() => axios.get(state.createProjectStatusEndpoint, { params: { job_id: jobId } }))
.then(resp => {
if (resp.status === statusCodes.OK) {
dispatch('requestCreateProjectSuccess', resp.data);
}
})
.catch(error => {
dispatch('requestCreateProjectError', error);
});
};
export const requestCreateProjectSuccess = ({ commit }, selfMonitorData) => {
commit(types.SET_LOADING, false);
commit(types.SET_PROJECT_URL, selfMonitorData.project_full_path);
commit(types.SET_ALERT_CONTENT, {
message: s__('SelfMonitoring|Self monitoring project has been successfully created.'),
actionText: __('View project'),
actionName: 'viewSelfMonitorProject',
});
commit(types.SET_SHOW_ALERT, true);
commit(types.SET_PROJECT_CREATED, true);
};
export const requestCreateProjectError = ({ commit }, error) => {
const { response } = error;
const message = response.data && response.data.message ? response.data.message : '';
commit(types.SET_ALERT_CONTENT, {
message: `${__('There was an error saving your changes.')} ${message}`,
});
commit(types.SET_SHOW_ALERT, true);
commit(types.SET_LOADING, false);
};
export const deleteProject = ({ dispatch }) => dispatch('requestDeleteProject');
export const requestDeleteProject = ({ dispatch, state, commit }) => {
commit(types.SET_LOADING, true);
axios
.delete(state.deleteProjectEndpoint)
.then(resp => {
if (resp.status === statusCodes.ACCEPTED) {
dispatch('requestDeleteProjectStatus', resp.data.job_id);
}
})
.catch(error => {
dispatch('requestDeleteProjectError', error);
});
};
export const requestDeleteProjectStatus = ({ dispatch, state }, jobId) => {
backOffRequest(() => axios.get(state.deleteProjectStatusEndpoint, { params: { job_id: jobId } }))
.then(resp => {
if (resp.status === statusCodes.OK) {
dispatch('requestDeleteProjectSuccess', resp.data);
}
})
.catch(error => {
dispatch('requestDeleteProjectError', error);
});
};
export const requestDeleteProjectSuccess = ({ commit }) => {
commit(types.SET_PROJECT_URL, '');
commit(types.SET_PROJECT_CREATED, false);
commit(types.SET_ALERT_CONTENT, {
message: s__('SelfMonitoring|Self monitoring project has been successfully deleted.'),
actionText: __('Undo'),
actionName: 'createProject',
});
commit(types.SET_SHOW_ALERT, true);
commit(types.SET_LOADING, false);
};
export const requestDeleteProjectError = ({ commit }, error) => {
const { response } = error;
const message = response.data && response.data.message ? response.data.message : '';
commit(types.SET_ALERT_CONTENT, {
message: `${__('There was an error saving your changes.')} ${message}`,
});
commit(types.SET_LOADING, false);
};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export const createStore = initialState =>
new Vuex.Store({
modules: {
selfMonitoring: {
namespaced: true,
state: createState(initialState),
actions,
mutations,
},
},
});
export default createStore;
export const SET_ENABLED = 'SET_ENABLED';
export const SET_PROJECT_CREATED = 'SET_PROJECT_CREATED';
export const SET_SHOW_ALERT = 'SET_SHOW_ALERT';
export const SET_PROJECT_URL = 'SET_PROJECT_URL';
export const SET_LOADING = 'SET_LOADING';
export const SET_ALERT_CONTENT = 'SET_ALERT_CONTENT';
import * as types from './mutation_types';
export default {
[types.SET_ENABLED](state, enabled) {
state.projectEnabled = enabled;
},
[types.SET_PROJECT_CREATED](state, created) {
state.projectCreated = created;
},
[types.SET_SHOW_ALERT](state, show) {
state.showAlert = show;
},
[types.SET_PROJECT_URL](state, url) {
state.projectPath = url;
},
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
[types.SET_ALERT_CONTENT](state, content) {
state.alertContent = content;
},
};
import { parseBoolean } from '~/lib/utils/common_utils';
export default (initialState = {}) => ({
projectEnabled: parseBoolean(initialState.projectEnabled) || false,
projectCreated: parseBoolean(initialState.selfMonitorProjectCreated) || false,
createProjectEndpoint: initialState.createSelfMonitoringProjectPath || '',
deleteProjectEndpoint: initialState.deleteSelfMonitoringProjectPath || '',
createProjectStatusEndpoint: initialState.statusCreateSelfMonitoringProjectPath || '',
deleteProjectStatusEndpoint: initialState.statusDeleteSelfMonitoringProjectPath || '',
selfMonitorProjectPath: initialState.selfMonitoringProjectFullPath || '',
showAlert: false,
projectPath: '',
loading: false,
alertContent: {},
});
...@@ -47,6 +47,9 @@ ...@@ -47,6 +47,9 @@
.settings-content .settings-content
= render 'performance_bar' = render 'performance_bar'
- if Feature.enabled?(:self_monitoring_project)
.js-self-monitoring-settings{ data: self_monitoring_project_data }
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) } %section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header#usage-statistics .settings-header#usage-statistics
%h4 %h4
......
...@@ -5217,6 +5217,9 @@ msgstr "" ...@@ -5217,6 +5217,9 @@ msgstr ""
msgid "Create New Domain" msgid "Create New Domain"
msgstr "" msgstr ""
msgid "Create Project"
msgstr ""
msgid "Create a GitLab account first, and then connect it to your %{label} account." msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr "" msgstr ""
...@@ -5849,6 +5852,9 @@ msgstr "" ...@@ -5849,6 +5852,9 @@ msgstr ""
msgid "Delete pipeline" msgid "Delete pipeline"
msgstr "" msgstr ""
msgid "Delete project"
msgstr ""
msgid "Delete snippet" msgid "Delete snippet"
msgstr "" msgstr ""
...@@ -16409,6 +16415,30 @@ msgstr "" ...@@ -16409,6 +16415,30 @@ msgstr ""
msgid "Self-monitoring project was not deleted. Please check logs for any error messages" msgid "Self-monitoring project was not deleted. Please check logs for any error messages"
msgstr "" msgstr ""
msgid "SelfMonitoring|Disable self monitoring?"
msgstr ""
msgid "SelfMonitoring|Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?"
msgstr ""
msgid "SelfMonitoring|Enable or disable instance self monitoring"
msgstr ""
msgid "SelfMonitoring|Enabling this feature creates a %{projectLinkStart}project%{projectLinkEnd} that can be used to monitor the health of your instance."
msgstr ""
msgid "SelfMonitoring|Enabling this feature creates a project that can be used to monitor the health of your instance."
msgstr ""
msgid "SelfMonitoring|Self monitoring"
msgstr ""
msgid "SelfMonitoring|Self monitoring project has been successfully created."
msgstr ""
msgid "SelfMonitoring|Self monitoring project has been successfully deleted."
msgstr ""
msgid "Send a separate email notification to Developers." msgid "Send a separate email notification to Developers."
msgstr "" msgstr ""
...@@ -20441,6 +20471,9 @@ msgstr "" ...@@ -20441,6 +20471,9 @@ msgstr ""
msgid "View previous app" msgid "View previous app"
msgstr "" msgstr ""
msgid "View project"
msgstr ""
msgid "View project labels" msgid "View project labels"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`self monitor component When the self monitor project has not been created default state to match the default snapshot 1`] = `
<section
class="settings no-animate js-self-monitoring-settings"
>
<div
class="settings-header"
>
<h4
class="js-section-header"
>
Self monitoring
</h4>
<gl-button-stub
class="js-settings-toggle"
>
Expand
</gl-button-stub>
<p
class="js-section-sub-header"
>
Enable or disable instance self monitoring
</p>
</div>
<div
class="settings-content"
>
<form
name="self-monitoring-form"
>
<p>
Enabling this feature creates a project that can be used to monitor the health of your instance.
</p>
<gl-form-group-stub
label="Create Project"
label-for="self-monitor-toggle"
>
<gl-toggle-stub
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
name="self-monitor-toggle"
/>
</gl-form-group-stub>
</form>
</div>
<gl-modal-stub
cancel-title="Cancel"
modalclass=""
modalid="delete-self-monitor-modal"
ok-title="Delete project"
ok-variant="danger"
title="Disable self monitoring?"
titletag="h4"
>
<div>
Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?
</div>
</gl-modal-stub>
</section>
`;
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
import { createStore } from '~/self_monitor/store';
describe('self monitor component', () => {
let wrapper;
let store;
describe('When the self monitor project has not been created', () => {
beforeEach(() => {
store = createStore({
projectEnabled: false,
selfMonitorProjectCreated: false,
createSelfMonitoringProjectPath: '/create',
deleteSelfMonitoringProjectPath: '/delete',
});
});
afterEach(() => {
if (wrapper.destroy) {
wrapper.destroy();
}
});
describe('default state', () => {
it('to match the default snapshot', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.element).toMatchSnapshot();
});
});
it('renders header text', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.find('.js-section-header').text()).toBe('Self monitoring');
});
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
wrapper = shallowMount(SelfMonitor, { store });
const button = wrapper.find(GlButton);
expect(button.text()).toBe('Expand');
});
});
describe('sub-header', () => {
it('renders descriptive text', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.find('.js-section-sub-header').text()).toContain(
'Enable or disable instance self monitoring',
);
});
});
describe('settings-content', () => {
it('renders the form description without a link', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.vm.selfMonitoringFormText).toContain(
'Enabling this feature creates a project that can be used to monitor the health of your instance.',
);
});
it('renders the form description with a link', () => {
store = createStore({
projectEnabled: true,
selfMonitorProjectCreated: true,
createSelfMonitoringProjectPath: '/create',
deleteSelfMonitoringProjectPath: '/delete',
});
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.vm.selfMonitoringFormText).toContain('<a href="http://localhost/">');
});
});
});
});
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import statusCodes from '~/lib/utils/http_status';
import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
describe('self monitor actions', () => {
let state;
let mock;
beforeEach(() => {
state = createState();
mock = new MockAdapter(axios);
});
describe('setSelfMonitor', () => {
it('commits the SET_ENABLED mutation', done => {
testAction(
actions.setSelfMonitor,
null,
state,
[{ type: types.SET_ENABLED, payload: null }],
[],
done,
);
});
});
describe('resetAlert', () => {
it('commits the SET_ENABLED mutation', done => {
testAction(
actions.resetAlert,
null,
state,
[{ type: types.SET_SHOW_ALERT, payload: false }],
[],
done,
);
});
});
describe('requestCreateProject', () => {
describe('success', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
state.createProjectStatusEndpoint = '/create_status';
mock.onPost(state.createProjectEndpoint).reply(statusCodes.ACCEPTED, {
job_id: '123',
});
mock.onGet(state.createProjectStatusEndpoint).reply(statusCodes.OK, {
project_full_path: '/self-monitor-url',
});
});
it('dispatches status request with job data', done => {
testAction(
actions.requestCreateProject,
null,
state,
[
{
type: types.SET_LOADING,
payload: true,
},
],
[
{
type: 'requestCreateProjectStatus',
payload: '123',
},
],
done,
);
});
it('dispatches success with project path', done => {
testAction(
actions.requestCreateProjectStatus,
null,
state,
[],
[
{
type: 'requestCreateProjectSuccess',
payload: { project_full_path: '/self-monitor-url' },
},
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
mock.onPost(state.createProjectEndpoint).reply(500);
});
it('dispatches error', done => {
testAction(
actions.requestCreateProject,
null,
state,
[
{
type: types.SET_LOADING,
payload: true,
},
],
[
{
type: 'requestCreateProjectError',
payload: new Error('Request failed with status code 500'),
},
],
done,
);
});
});
describe('requestCreateProjectSuccess', () => {
it('should commit the received data', done => {
testAction(
actions.requestCreateProjectSuccess,
{ project_full_path: '/self-monitor-url' },
state,
[
{ type: types.SET_LOADING, payload: false },
{ type: types.SET_PROJECT_URL, payload: '/self-monitor-url' },
{
type: types.SET_ALERT_CONTENT,
payload: {
actionName: 'viewSelfMonitorProject',
actionText: 'View project',
message: 'Self monitoring project has been successfully created.',
},
},
{ type: types.SET_SHOW_ALERT, payload: true },
{ type: types.SET_PROJECT_CREATED, payload: true },
],
[],
done,
);
});
});
});
describe('deleteSelfMonitorProject', () => {
describe('success', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
state.deleteProjectStatusEndpoint = '/delete-status';
mock.onDelete(state.deleteProjectEndpoint).reply(statusCodes.ACCEPTED, {
job_id: '456',
});
mock.onGet(state.deleteProjectStatusEndpoint).reply(statusCodes.OK, {
status: 'success',
});
});
it('dispatches status request with job data', done => {
testAction(
actions.requestDeleteProject,
null,
state,
[
{
type: types.SET_LOADING,
payload: true,
},
],
[
{
type: 'requestDeleteProjectStatus',
payload: '456',
},
],
done,
);
});
it('dispatches success with status', done => {
testAction(
actions.requestDeleteProjectStatus,
null,
state,
[],
[
{
type: 'requestDeleteProjectSuccess',
payload: { status: 'success' },
},
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
mock.onDelete(state.deleteProjectEndpoint).reply(500);
});
it('dispatches error', done => {
testAction(
actions.requestDeleteProject,
null,
state,
[
{
type: types.SET_LOADING,
payload: true,
},
],
[
{
type: 'requestDeleteProjectError',
payload: new Error('Request failed with status code 500'),
},
],
done,
);
});
});
describe('requestDeleteProjectSuccess', () => {
it('should commit mutations to remove previously set data', done => {
testAction(
actions.requestDeleteProjectSuccess,
null,
state,
[
{ type: types.SET_PROJECT_URL, payload: '' },
{ type: types.SET_PROJECT_CREATED, payload: false },
{
type: types.SET_ALERT_CONTENT,
payload: {
actionName: 'createProject',
actionText: 'Undo',
message: 'Self monitoring project has been successfully deleted.',
},
},
{ type: types.SET_SHOW_ALERT, payload: true },
{ type: types.SET_LOADING, payload: false },
],
[],
done,
);
});
});
});
});
import mutations from '~/self_monitor/store/mutations';
import createState from '~/self_monitor/store/state';
describe('self monitoring mutations', () => {
let localState;
beforeEach(() => {
localState = createState();
});
describe('SET_ENABLED', () => {
it('sets selfMonitor', () => {
mutations.SET_ENABLED(localState, true);
expect(localState.projectEnabled).toBe(true);
});
});
describe('SET_PROJECT_CREATED', () => {
it('sets projectCreated', () => {
mutations.SET_PROJECT_CREATED(localState, true);
expect(localState.projectCreated).toBe(true);
});
});
describe('SET_SHOW_ALERT', () => {
it('sets showAlert', () => {
mutations.SET_SHOW_ALERT(localState, true);
expect(localState.showAlert).toBe(true);
});
});
describe('SET_PROJECT_URL', () => {
it('sets projectPath', () => {
mutations.SET_PROJECT_URL(localState, '/url/');
expect(localState.projectPath).toBe('/url/');
});
});
describe('SET_LOADING', () => {
it('sets loading', () => {
mutations.SET_LOADING(localState, true);
expect(localState.loading).toBe(true);
});
});
describe('SET_ALERT_CONTENT', () => {
it('set alertContent', () => {
const alertContent = {
message: 'success',
actionText: 'undo',
actionName: 'createProject',
};
mutations.SET_ALERT_CONTENT(localState, alertContent);
expect(localState.alertContent).toBe(alertContent);
});
});
});
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