Commit 379e1f96 authored by Tristan Read's avatar Tristan Read Committed by Fatih Acet

Add Grafana Integration settings

Adds the frontend for Grafana integration settings
parent 7d2a7f12
<script>
import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { mapState, mapActions } from 'vuex';
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlLink,
Icon,
},
data() {
return { placeholderUrl: 'https://my-url.grafana.net/my-dashboard' };
},
computed: {
...mapState(['operationsSettingsEndpoint', 'grafanaToken', 'grafanaUrl']),
localGrafanaToken: {
get() {
return this.grafanaToken;
},
set(token) {
this.setGrafanaToken(token);
},
},
localGrafanaUrl: {
get() {
return this.grafanaUrl;
},
set(url) {
this.setGrafanaUrl(url);
},
},
},
methods: {
...mapActions(['setGrafanaUrl', 'setGrafanaToken', 'updateGrafanaIntegration']),
},
};
</script>
<template>
<section id="grafana" class="settings no-animate js-grafana-integration">
<div class="settings-header">
<h4 class="js-section-header">
{{ s__('GrafanaIntegration|Grafana Authentication') }}
</h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
</p>
</div>
<div class="settings-content">
<form>
<gl-form-group
:label="s__('GrafanaIntegration|Grafana URL')"
label-for="grafana-url"
:description="s__('GrafanaIntegration|Enter the base URL of the Grafana instance.')"
>
<gl-form-input id="grafana-url" v-model="localGrafanaUrl" :placeholder="placeholderUrl" />
</gl-form-group>
<gl-form-group :label="s__('GrafanaIntegration|API Token')" label-for="grafana-token">
<gl-form-input id="grafana-token" v-model="localGrafanaToken" />
<p class="form-text text-muted">
{{ s__('GrafanaIntegration|Enter the Grafana API Token.') }}
<a
href="https://grafana.com/docs/http_api/auth/#create-api-token"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
<icon name="external-link" class="vertical-align-middle" />
</a>
</p>
</gl-form-group>
<gl-button variant="success" @click="updateGrafanaIntegration">
{{ __('Save Changes') }}
</gl-button>
</form>
</div>
</section>
</template>
import Vue from 'vue';
import store from './store';
import GrafanaIntegration from './components/grafana_integration.vue';
export default () => {
const el = document.querySelector('.js-grafana-integration');
return new Vue({
el,
store: store(el.dataset),
render(createElement) {
return createElement(GrafanaIntegration);
},
});
};
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import * as mutationTypes from './mutation_types';
export const setGrafanaUrl = ({ commit }, url) => commit(mutationTypes.SET_GRAFANA_URL, url);
export const setGrafanaToken = ({ commit }, token) =>
commit(mutationTypes.SET_GRAFANA_TOKEN, token);
export const updateGrafanaIntegration = ({ state, dispatch }) =>
axios
.patch(state.operationsSettingsEndpoint, {
project: {
grafana_integration_attributes: {
grafana_url: state.grafanaUrl,
token: state.grafanaToken,
},
},
})
.then(() => dispatch('receiveGrafanaIntegrationUpdateSuccess'))
.catch(error => dispatch('receiveGrafanaIntegrationUpdateError', error));
export const receiveGrafanaIntegrationUpdateSuccess = () => {
/**
* The operations_controller currently handles successful requests
* by creating a flash banner messsage to notify the user.
*/
refreshCurrentPage();
};
export const receiveGrafanaIntegrationUpdateError = (_, error) => {
const { response } = error;
const message = response.data && response.data.message ? response.data.message : '';
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
};
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({
state: createState(initialState),
actions,
mutations,
});
export default createStore;
export const SET_GRAFANA_URL = 'SET_GRAFANA_URL';
export const SET_GRAFANA_TOKEN = 'SET_GRAFANA_TOKEN';
import * as types from './mutation_types';
export default {
[types.SET_GRAFANA_URL](state, url) {
state.grafanaUrl = url;
},
[types.SET_GRAFANA_TOKEN](state, token) {
state.grafanaToken = token;
},
};
export default (initialState = {}) => ({
operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
grafanaToken: initialState.grafanaIntegrationToken || '',
grafanaUrl: initialState.grafanaIntegrationUrl || '',
});
import mountErrorTrackingForm from '~/error_tracking_settings';
import mountOperationSettings from '~/operation_settings';
import mountGrafanaIntegration from '~/grafana_integration';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
mountOperationSettings();
if (gon.features.gfmGrafanaIntegration) {
mountGrafanaIntegration();
}
initSettingsPanels();
});
.js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
grafana_integration: { url: grafana_integration_url, token: grafana_integration_token } } }
......@@ -5,4 +5,5 @@
= render_if_exists 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/external_dashboard'
= render 'projects/settings/operations/grafana_integration'
= render_if_exists 'projects/settings/operations/tracing'
......@@ -8210,6 +8210,24 @@ msgstr ""
msgid "Grafana URL"
msgstr ""
msgid "GrafanaIntegration|API Token"
msgstr ""
msgid "GrafanaIntegration|Embed Grafana charts in GitLab issues."
msgstr ""
msgid "GrafanaIntegration|Enter the Grafana API Token."
msgstr ""
msgid "GrafanaIntegration|Enter the base URL of the Grafana instance."
msgstr ""
msgid "GrafanaIntegration|Grafana Authentication"
msgstr ""
msgid "GrafanaIntegration|Grafana URL"
msgstr ""
msgid "Grant access"
msgstr ""
......
......@@ -102,5 +102,40 @@ describe 'Projects > Settings > For a forked project', :js do
end
end
end
context 'grafana integration settings form' do
it 'is not present when the feature flag is disabled' do
stub_feature_flags(gfm_grafana_integration: false)
visit project_settings_operations_path(project)
wait_for_requests
expect(page).to have_no_css('.js-grafana-integration')
end
it 'is present when the feature flag is enabled' do
visit project_settings_operations_path(project)
wait_for_requests
within '.js-grafana-integration' do
click_button('Expand')
end
expect(page).to have_content('Grafana URL')
expect(page).to have_content('API Token')
expect(page).to have_button('Save Changes')
fill_in('grafana-url', with: 'http://gitlab-test.grafana.net')
fill_in('grafana-token', with: 'token')
click_button('Save Changes')
wait_for_requests
assert_text('Your changes have been saved')
end
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`grafana integration component default state to match the default snapshot 1`] = `
<section
class="settings no-animate js-grafana-integration"
id="grafana"
>
<div
class="settings-header"
>
<h4
class="js-section-header"
>
Grafana Authentication
</h4>
<glbutton-stub
class="js-settings-toggle"
>
Expand
</glbutton-stub>
<p
class="js-section-sub-header"
>
Embed Grafana charts in GitLab issues.
</p>
</div>
<div
class="settings-content"
>
<form>
<glformgroup-stub
description="Enter the base URL of the Grafana instance."
label="Grafana URL"
label-for="grafana-url"
>
<glforminput-stub
id="grafana-url"
placeholder="https://my-url.grafana.net/my-dashboard"
value="http://test.host"
/>
</glformgroup-stub>
<glformgroup-stub
label="API Token"
label-for="grafana-token"
>
<glforminput-stub
id="grafana-token"
value="someToken"
/>
<p
class="form-text text-muted"
>
Enter the Grafana API Token.
<a
href="https://grafana.com/docs/http_api/auth/#create-api-token"
rel="noopener noreferrer"
target="_blank"
>
More information
<icon-stub
class="vertical-align-middle"
name="external-link"
size="16"
/>
</a>
</p>
</glformgroup-stub>
<glbutton-stub
variant="success"
>
Save Changes
</glbutton-stub>
</form>
</div>
</section>
`;
import { mount, shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
describe('grafana integration component', () => {
let wrapper;
let store;
const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
const grafanaIntegrationUrl = `${TEST_HOST}`;
const grafanaIntegrationToken = 'someToken';
beforeEach(() => {
store = createStore({
operationsSettingsEndpoint,
grafanaIntegrationUrl,
grafanaIntegrationToken,
});
});
afterEach(() => {
if (wrapper.destroy) {
wrapper.destroy();
createFlash.mockReset();
refreshCurrentPage.mockReset();
}
});
describe('default state', () => {
it('to match the default snapshot', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
expect(wrapper.element).toMatchSnapshot();
});
});
it('renders header text', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
expect(wrapper.find('.js-section-header').text()).toBe('Grafana Authentication');
});
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
const button = wrapper.find(GlButton);
expect(button.text()).toBe('Expand');
});
});
describe('sub-header', () => {
it('renders descriptive text', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
expect(wrapper.find('.js-section-sub-header').text()).toContain(
'Embed Grafana charts in GitLab issues.',
);
});
});
describe('form', () => {
beforeEach(() => {
jest.spyOn(axios, 'patch').mockImplementation();
});
afterEach(() => {
axios.patch.mockReset();
});
describe('submit button', () => {
const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
const endpointRequest = [
operationsSettingsEndpoint,
{
project: {
grafana_integration_attributes: {
grafana_url: grafanaIntegrationUrl,
token: grafanaIntegrationToken,
},
},
},
];
it('submits form on click', () => {
wrapper = mount(GrafanaIntegration, { store });
axios.patch.mockResolvedValue();
findSubmitButton(wrapper).trigger('click');
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
});
it('creates flash banner on error', () => {
const message = 'mockErrorMessage';
wrapper = mount(GrafanaIntegration, { store });
axios.patch.mockRejectedValue({ response: { data: { message } } });
findSubmitButton().trigger('click');
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
return wrapper.vm
.$nextTick()
.then(jest.runAllTicks)
.then(() =>
expect(createFlash).toHaveBeenCalledWith(
`There was an error saving your changes. ${message}`,
'alert',
),
);
});
});
});
});
import mutations from '~/grafana_integration/store/mutations';
import createState from '~/grafana_integration/store/state';
describe('grafana integration mutations', () => {
let localState;
beforeEach(() => {
localState = createState();
});
describe('SET_GRAFANA_URL', () => {
it('sets grafanaUrl', () => {
const mockUrl = 'mockUrl';
mutations.SET_GRAFANA_URL(localState, mockUrl);
expect(localState.grafanaUrl).toBe(mockUrl);
});
});
describe('SET_GRAFANA_TOKEN', () => {
it('sets grafanaToken', () => {
const mockToken = 'mockToken';
mutations.SET_GRAFANA_TOKEN(localState, mockToken);
expect(localState.grafanaToken).toBe(mockToken);
});
});
});
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