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 projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => {
if (gon.features && gon.features.selfMonitoringProject) {
selfMonitor();
}
// Initialize expandable settings panels
initSettingsPanels();
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 @@
.settings-content
= 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?) }
.settings-header#usage-statistics
%h4
......
......@@ -5217,6 +5217,9 @@ msgstr ""
msgid "Create New Domain"
msgstr ""
msgid "Create Project"
msgstr ""
msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr ""
......@@ -5849,6 +5852,9 @@ msgstr ""
msgid "Delete pipeline"
msgstr ""
msgid "Delete project"
msgstr ""
msgid "Delete snippet"
msgstr ""
......@@ -16409,6 +16415,30 @@ msgstr ""
msgid "Self-monitoring project was not deleted. Please check logs for any error messages"
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."
msgstr ""
......@@ -20441,6 +20471,9 @@ msgstr ""
msgid "View previous app"
msgstr ""
msgid "View project"
msgstr ""
msgid "View project labels"
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