Commit da6649f0 authored by Adriel Santiago's avatar Adriel Santiago Committed by Clement Ho

Add custom metrics form to dashboard

Use existing form to allow users to add custom metrics via the dashboard
parent bda09311
<script> <script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; import {
GlButton,
GlDropdown,
GlDropdownItem,
GlModal,
GlModalDirective,
GlLink,
} from '@gitlab/ui';
import _ from 'underscore'; import _ from 'underscore';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -27,8 +34,11 @@ export default { ...@@ -27,8 +34,11 @@ export default {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlLink, GlLink,
GlModal,
},
directives: {
GlModalDirective,
}, },
props: { props: {
externalDashboardPath: { externalDashboardPath: {
type: String, type: String,
...@@ -102,6 +112,19 @@ export default { ...@@ -102,6 +112,19 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
customMetricsAvailable: {
type: Boolean,
required: false,
default: false,
},
customMetricsPath: {
type: String,
required: true,
},
validateQueryPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -111,8 +134,14 @@ export default { ...@@ -111,8 +134,14 @@ export default {
elWidth: 0, elWidth: 0,
selectedTimeWindow: '', selectedTimeWindow: '',
selectedTimeWindowKey: '', selectedTimeWindowKey: '',
formIsValid: null,
}; };
}, },
computed: {
canAddMetrics() {
return this.customMetricsAvailable && this.customMetricsPath.length;
},
},
created() { created() {
this.service = new MonitoringService({ this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint, metricsEndpoint: this.metricsEndpoint,
...@@ -193,11 +222,20 @@ export default { ...@@ -193,11 +222,20 @@ export default {
this.state = 'unableToConnect'; this.state = 'unableToConnect';
}); });
}, },
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
onSidebarMutation() { onSidebarMutation() {
setTimeout(() => { setTimeout(() => {
this.elWidth = this.$el.clientWidth; this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration); }, sidebarAnimationDuration);
}, },
setFormValidity(isValid) {
this.formIsValid = isValid;
},
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
activeTimeWindow(key) { activeTimeWindow(key) {
return this.timeWindows[key] === this.selectedTimeWindow; return this.timeWindows[key] === this.selectedTimeWindow;
}, },
...@@ -205,57 +243,97 @@ export default { ...@@ -205,57 +243,97 @@ export default {
return `?time_window=${key}`; return `?time_window=${key}`;
}, },
}, },
addMetric: {
title: s__('Metrics|Add metric'),
modalId: 'add-metric',
},
}; };
</script> </script>
<template> <template>
<div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> <div v-if="!showEmptyState" class="prometheus-graphs">
<div <div class="gl-p-3 border-bottom bg-gray-light d-flex justify-content-between">
v-if="environmentsEndpoint" <div
class="dropdowns d-flex align-items-center justify-content-between" v-if="environmentsEndpoint"
> class="dropdowns d-flex align-items-center justify-content-between"
<div class="d-flex align-items-center"> >
<strong>{{ s__('Metrics|Environment') }}</strong> <div class="d-flex align-items-center">
<gl-dropdown <strong>{{ s__('Metrics|Environment') }}</strong>
class="prepend-left-10 js-environments-dropdown" <gl-dropdown
toggle-class="dropdown-menu-toggle" class="prepend-left-10 js-environments-dropdown"
:text="currentEnvironmentName" toggle-class="dropdown-menu-toggle"
:disabled="store.environmentsData.length === 0" :text="currentEnvironmentName"
> :disabled="store.environmentsData.length === 0"
<gl-dropdown-item >
v-for="environment in store.environmentsData" <gl-dropdown-item
:key="environment.id" v-for="environment in store.environmentsData"
:href="environment.metrics_path" :key="environment.id"
:active="environment.name === currentEnvironmentName" :active="environment.name === currentEnvironmentName"
active-class="is-active" active-class="is-active"
>{{ environment.name }}</gl-dropdown-item >{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</div>
<div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
toggle-class="dropdown-menu-toggle"
:text="selectedTimeWindow"
> >
</gl-dropdown> <gl-dropdown-item
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
>
</gl-dropdown>
</div>
</div> </div>
<div v-if="showTimeWindowDropdown" class="d-flex align-items-center"> <div class="d-flex">
<strong>{{ s__('Metrics|Show last') }}</strong> <div v-if="isEE && canAddMetrics">
<gl-dropdown <gl-button
class="prepend-left-10 js-time-window-dropdown" v-gl-modal-directive="$options.addMetric.modalId"
toggle-class="dropdown-menu-toggle" class="js-add-metric-button text-success border-success"
:text="selectedTimeWindow"
>
<gl-dropdown-item
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
> >
</gl-dropdown> {{ $options.addMetric.title }}
</gl-button>
<gl-modal
ref="addMetricModal"
:modal-id="$options.addMetric.modalId"
:title="$options.addMetric.title"
>
<form ref="customMetricsForm" :action="customMetricsPath" method="post">
<custom-metrics-form-fields
:validate-query-path="validateQueryPath"
form-operation="post"
@formValidation="setFormValidity"
/>
</form>
<div slot="modal-footer">
<gl-button @click="hideAddMetricModal">
{{ __('Cancel') }}
</gl-button>
<gl-button
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-modal>
</div>
<gl-button
v-if="externalDashboardPath.length"
class="js-external-dashboard-link prepend-left-8"
variant="primary"
:href="externalDashboardPath"
>
{{ __('View full dashboard') }}
<icon name="external-link" />
</gl-button>
</div> </div>
<gl-button
v-if="externalDashboardPath.length"
class="js-external-dashboard-link"
variant="primary"
:href="externalDashboardPath"
>
{{ __('View full dashboard') }}
<icon name="external-link" />
</gl-button>
</div> </div>
<graph-group <graph-group
v-for="(groupData, index) in store.groups" v-for="(groupData, index) in store.groups"
......
...@@ -48,6 +48,10 @@ ...@@ -48,6 +48,10 @@
color: $brand-info; color: $brand-info;
} }
.bg-gray-light {
background-color: $gray-light;
}
.text-break-word { .text-break-word {
word-break: break-all; word-break: break-all;
} }
...@@ -446,19 +450,13 @@ img.emoji { ...@@ -446,19 +450,13 @@ img.emoji {
} }
/** COMMON SPACING CLASSES **/ /** COMMON SPACING CLASSES **/
.gl-pl-0 { padding-left: 0; } @each $index, $padding in $spacing-scale {
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; } #{'.gl-p-#{$index}'} { padding: $padding; }
.gl-pl-2 { padding-left: $grid-size; } #{'.gl-pl-#{$index}'} { padding-left: $padding; }
.gl-pl-3 { padding-left: #{2 * $grid-size}; } #{'.gl-pr-#{$index}'} { padding-right: $padding; }
.gl-pl-4 { padding-left: #{3 * $grid-size}; } #{'.gl-pt-#{$index}'} { padding-top: $padding; }
.gl-pl-5 { padding-left: #{4 * $grid-size}; } #{'.gl-pb-#{$index}'} { padding-bottom: $padding; }
}
.gl-pr-0 { padding-right: 0; }
.gl-pr-1 { padding-right: #{0.5 * $grid-size}; }
.gl-pr-2 { padding-right: $grid-size; }
.gl-pr-3 { padding-right: #{2 * $grid-size}; }
.gl-pr-4 { padding-right: #{3 * $grid-size}; }
.gl-pr-5 { padding-right: #{4 * $grid-size}; }
/** /**
* Removes browser specific clear icon from input fields in * Removes browser specific clear icon from input fields in
......
...@@ -11,6 +11,14 @@ $default-transition-duration: 0.15s; ...@@ -11,6 +11,14 @@ $default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px; $contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px; $contextual-sidebar-collapsed-width: 50px;
$toggle-sidebar-height: 48px; $toggle-sidebar-height: 48px;
$spacing-scale: (
0: 0,
1: #{0.5 * $grid-size},
2: $grid-size,
3: #{2 * $grid-size},
4: #{3 * $grid-size},
5: #{4 * $grid-size}
);
/* /*
* Color schema * Color schema
......
<script> <script>
import CeDashboard from '~/monitoring/components/dashboard.vue'; import CeDashboard from '~/monitoring/components/dashboard.vue';
import AlertWidget from './alert_widget.vue'; import AlertWidget from './alert_widget.vue';
import CustomMetricsFormFields from 'ee/custom_metrics/components/custom_metrics_form_fields.vue';
export default { export default {
components: { components: {
AlertWidget, AlertWidget,
CustomMetricsFormFields,
}, },
extends: CeDashboard, extends: CeDashboard,
props: { props: {
...@@ -24,11 +26,6 @@ export default { ...@@ -24,11 +26,6 @@ export default {
allAlerts: {}, allAlerts: {},
}; };
}, },
computed: {
alertsAvailable() {
return this.prometheusAlertsAvailable && this.alertsEndpoint;
},
},
methods: { methods: {
setAlerts(alertPath, alertAttributes) { setAlerts(alertPath, alertAttributes) {
if (alertAttributes) { if (alertAttributes) {
......
...@@ -6,6 +6,7 @@ export default () => { ...@@ -6,6 +6,7 @@ export default () => {
if (el && el.dataset) { if (el && el.dataset) {
initCeBundle({ initCeBundle({
customMetricsAvailable: parseBoolean(el.dataset.customMetricsAvailable),
prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable), prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable),
}); });
} }
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { GlModal } from '@gitlab/ui';
import Dashboard from 'ee/monitoring/components/dashboard.vue'; import Dashboard from 'ee/monitoring/components/dashboard.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint } from 'spec/monitoring/mock_data'; import { metricsGroupsAPIResponse, mockApiEndpoint } from 'spec/monitoring/mock_data';
import propsData from 'spec/monitoring/dashboard_spec'; import propsData from 'spec/monitoring/dashboard_spec';
import AlertWidget from 'ee/monitoring/components/alert_widget.vue'; import AlertWidget from 'ee/monitoring/components/alert_widget.vue';
import CustomMetricsFormFields from 'ee/custom_metrics/components/custom_metrics_form_fields.vue';
describe('Dashboard', () => { describe('Dashboard', () => {
let Component; let Component;
...@@ -24,6 +26,7 @@ describe('Dashboard', () => { ...@@ -24,6 +26,7 @@ describe('Dashboard', () => {
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
Component = localVue.extend(Dashboard); Component = localVue.extend(Dashboard);
}); });
...@@ -34,7 +37,6 @@ describe('Dashboard', () => { ...@@ -34,7 +37,6 @@ describe('Dashboard', () => {
describe('metrics with alert', () => { describe('metrics with alert', () => {
describe('with license', () => { describe('with license', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
vm = shallowMount(Component, { vm = shallowMount(Component, {
propsData: { propsData: {
...propsData, ...propsData,
...@@ -56,7 +58,6 @@ describe('Dashboard', () => { ...@@ -56,7 +58,6 @@ describe('Dashboard', () => {
describe('without license', () => { describe('without license', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
vm = shallowMount(Component, { vm = shallowMount(Component, {
propsData: { propsData: {
...propsData, ...propsData,
...@@ -76,4 +77,60 @@ describe('Dashboard', () => { ...@@ -76,4 +77,60 @@ describe('Dashboard', () => {
}); });
}); });
}); });
describe('add custom metrics', () => {
describe('when not available', () => {
beforeEach(() => {
vm = shallowMount(Component, {
propsData: {
...propsData,
customMetricsAvailable: false,
customMetricsPath: '/endpoint',
hasMetrics: true,
prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint',
showTimeWindowDropdown: false,
},
});
});
it('does not render add button on the dashboard', done => {
setTimeout(() => {
expect(vm.element.querySelector('.js-add-metric-button')).toBe(null);
done();
});
});
});
describe('when available', () => {
beforeEach(done => {
vm = shallowMount(Component, {
propsData: {
...propsData,
customMetricsAvailable: true,
customMetricsPath: '/endpoint',
hasMetrics: true,
prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint',
showTimeWindowDropdown: false,
},
});
setTimeout(done);
});
it('renders add button on the dashboard', () => {
expect(vm.element.querySelector('.js-add-metric-button').innerText).toContain('Add metric');
});
it('uses modal for custom metrics form', () => {
expect(vm.find(GlModal).exists()).toBe(true);
expect(vm.find(GlModal).attributes().modalid).toBe('add-metric');
});
it('renders custom metrics form fields', () => {
expect(vm.find(CustomMetricsFormFields).exists()).toBe(true);
});
});
});
}); });
...@@ -7800,6 +7800,9 @@ msgstr "" ...@@ -7800,6 +7800,9 @@ msgstr ""
msgid "Metrics for environment" msgid "Metrics for environment"
msgstr "" msgstr ""
msgid "Metrics|Add metric"
msgstr ""
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment" msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr "" msgstr ""
...@@ -13682,6 +13685,9 @@ msgstr "" ...@@ -13682,6 +13685,9 @@ msgstr ""
msgid "View file @ " msgid "View file @ "
msgstr "" msgstr ""
msgid "View full dashboard"
msgstr ""
msgid "View group labels" msgid "View group labels"
msgstr "" msgstr ""
......
...@@ -20,6 +20,9 @@ const propsData = { ...@@ -20,6 +20,9 @@ const propsData = {
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
environmentsEndpoint: '/root/hello-prometheus/environments/35', environmentsEndpoint: '/root/hello-prometheus/environments/35',
currentEnvironmentName: 'production', currentEnvironmentName: 'production',
customMetricsAvailable: false,
customMetricsPath: '',
validateQueryPath: '',
}; };
export default propsData; export default propsData;
...@@ -163,7 +166,7 @@ describe('Dashboard', () => { ...@@ -163,7 +166,7 @@ describe('Dashboard', () => {
}); });
}); });
it('renders the environments dropdown with a single is-active element', done => { it('renders the environments dropdown with a single active element', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { propsData: {
...@@ -178,7 +181,7 @@ describe('Dashboard', () => { ...@@ -178,7 +181,7 @@ describe('Dashboard', () => {
setTimeout(() => { setTimeout(() => {
const dropdownItems = component.$el.querySelectorAll( const dropdownItems = component.$el.querySelectorAll(
'.js-environments-dropdown .dropdown-item.is-active', '.js-environments-dropdown .dropdown-item[active="true"]',
); );
expect(dropdownItems.length).toEqual(1); expect(dropdownItems.length).toEqual(1);
......
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