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