Commit c79f3fe5 authored by Clement Ho's avatar Clement Ho

Merge branch '4452-in-line-chart-designer-mvc' into 'master'

Add custom metrics form to dashboard

Closes #4452

See merge request gitlab-org/gitlab-ee!11281
parents bda09311 da6649f0
<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,57 +243,97 @@ 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="environmentsEndpoint"
class="dropdowns d-flex align-items-center justify-content-between"
>
<div class="d-flex align-items-center">
<strong>{{ s__('Metrics|Environment') }}</strong>
<gl-dropdown
class="prepend-left-10 js-environments-dropdown"
toggle-class="dropdown-menu-toggle"
:text="currentEnvironmentName"
:disabled="store.environmentsData.length === 0"
>
<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
<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"
>
<div class="d-flex align-items-center">
<strong>{{ s__('Metrics|Environment') }}</strong>
<gl-dropdown
class="prepend-left-10 js-environments-dropdown"
toggle-class="dropdown-menu-toggle"
:text="currentEnvironmentName"
:disabled="store.environmentsData.length === 0"
>
<gl-dropdown-item
v-for="environment in store.environmentsData"
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
>{{ 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 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-item
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
<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"
>
</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>
<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>
<graph-group
v-for="(groupData, index) in store.groups"
......
......@@ -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