Skip to content
Snippets Groups Projects
Commit 97b325a4 authored by Tristan Read's avatar Tristan Read Committed by Fatih Acet
Browse files

Add ability to embed metrics

parent 86e00214
No related branches found
No related tags found
No related merge requests found
Showing
with 410 additions and 50 deletions
Loading
Loading
@@ -2,6 +2,7 @@ import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import renderMetrics from './render_metrics';
import highlightCurrentUser from './highlight_current_user';
import initUserPopovers from '../../user_popovers';
import initMRPopovers from '../../mr_popover';
Loading
Loading
@@ -17,6 +18,9 @@ $.fn.renderGFM = function renderGFM() {
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.gfm-project_member').get());
initMRPopovers(this.find('.gfm-merge_request').get());
if (gon.features && gon.features.gfmEmbeddedMetrics) {
renderMetrics(this.find('.js-render-metrics').get());
}
return this;
};
 
Loading
Loading
import Vue from 'vue';
import Metrics from '~/monitoring/components/embed.vue';
import { createStore } from '~/monitoring/stores';
// TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-ce/issues/64369.
export default function renderMetrics(elements) {
if (!elements.length) {
return;
}
elements.forEach(element => {
const { dashboardUrl } = element.dataset;
const MetricsComponent = Vue.extend(Metrics);
// eslint-disable-next-line no-new
new MetricsComponent({
el: element,
store: createStore(),
propsData: {
dashboardUrl,
},
});
});
}
Loading
Loading
@@ -37,7 +37,13 @@ export default {
},
projectPath: {
type: String,
required: true,
required: false,
default: () => '',
},
showBorder: {
type: Boolean,
required: false,
default: () => false,
},
thresholds: {
type: Array,
Loading
Loading
@@ -234,52 +240,54 @@ export default {
</script>
 
<template>
<div class="prometheus-graph col-12 col-lg-6">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
ref="areaChart"
v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:thresholds="thresholds"
:width="width"
:height="height"
@updated="onChartUpdated"
>
<template v-if="tooltip.isDeployment">
<template slot="tooltipTitle">
{{ __('Deployed') }}
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
<icon name="commit" class="mr-2" />
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
<template v-else>
<template slot="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
<div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
<div class="prometheus-graph" :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
ref="areaChart"
v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:thresholds="thresholds"
:width="width"
:height="height"
@updated="onChartUpdated"
>
<template v-if="tooltip.isDeployment">
<template slot="tooltipTitle">
{{ __('Deployed') }}
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
<icon name="commit" class="mr-2" />
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
<template slot="tooltipContent">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
<template v-else>
<template slot="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</div>
</template>
<template slot="tooltipContent">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
</div>
</div>
</template>
</template>
</template>
</gl-area-chart>
</gl-area-chart>
</div>
</div>
</template>
Loading
Loading
@@ -11,10 +11,9 @@ import MonitorSingleStatChart from './charts/single_stat.vue';
import PanelType from './panel_type.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { timeWindows, timeWindowsKeyNames } from '../constants';
import { sidebarAnimationDuration, timeWindows, timeWindowsKeyNames } from '../constants';
import { getTimeDiff } from '../utils';
 
const sidebarAnimationDuration = 150;
let sidebarMutationObserver;
 
export default {
Loading
Loading
@@ -370,8 +369,8 @@ export default {
</div>
<div v-if="!showEmptyState">
<graph-group
v-for="(groupData, index) in groupsWithData"
:key="index"
v-for="groupData in groupsWithData"
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
>
Loading
Loading
<script>
import { mapActions, mapState } from 'vuex';
import GraphGroup from './graph_group.vue';
import MonitorAreaChart from './charts/area.vue';
import { sidebarAnimationDuration, timeWindowsKeyNames, timeWindows } from '../constants';
import { getTimeDiff } from '../utils';
let sidebarMutationObserver;
export default {
components: {
GraphGroup,
MonitorAreaChart,
},
props: {
dashboardUrl: {
type: String,
required: true,
},
},
data() {
return {
params: {
...getTimeDiff(timeWindows[timeWindowsKeyNames.eightHours]),
},
elWidth: 0,
};
},
computed: {
...mapState('monitoringDashboard', ['groups', 'metricsWithData']),
groupData() {
const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length);
if (groupsWithData.length) {
return groupsWithData[0];
}
return null;
},
},
mounted() {
this.setInitialState();
this.fetchMetricsData(this.params);
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true,
childList: false,
subtree: false,
});
},
beforeDestroy() {
if (sidebarMutationObserver) {
sidebarMutationObserver.disconnect();
}
},
methods: {
...mapActions('monitoringDashboard', [
'fetchMetricsData',
'setEndpoints',
'setFeatureFlags',
'setShowErrorBanner',
]),
chartsWithData(charts) {
return charts.filter(chart =>
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
onSidebarMutation() {
setTimeout(() => {
this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration);
},
setInitialState() {
this.setFeatureFlags({
prometheusEndpointEnabled: true,
});
this.setEndpoints({
dashboardEndpoint: this.dashboardUrl,
});
this.setShowErrorBanner(false);
},
},
};
</script>
<template>
<div class="metrics-embed">
<div v-if="groupData" class="row w-100 m-n2 pb-4">
<monitor-area-chart
v-for="graphData in chartsWithData(groupData.metrics)"
:key="graphData.title"
:graph-data="graphData"
:container-width="elWidth"
group-id="monitor-area-chart"
:project-path="null"
:show-border="true"
/>
</div>
</div>
</template>
import { __ } from '~/locale';
 
export const sidebarAnimationDuration = 300; // milliseconds.
export const chartHeight = 300;
 
export const graphTypes = {
Loading
Loading
Loading
Loading
@@ -44,6 +44,10 @@ export const setFeatureFlags = (
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
};
 
export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA);
};
Loading
Loading
@@ -99,7 +103,9 @@ export const fetchMetricsData = ({ state, dispatch }, params) => {
})
.catch(error => {
dispatch('receiveMetricsDataFailure', error);
createFlash(s__('Metrics|There was an error while retrieving metrics'));
if (state.setShowErrorBanner) {
createFlash(s__('Metrics|There was an error while retrieving metrics'));
}
});
};
 
Loading
Loading
@@ -119,7 +125,9 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
})
.catch(error => {
dispatch('receiveMetricsDashboardFailure', error);
createFlash(s__('Metrics|There was an error while retrieving metrics'));
if (state.setShowErrorBanner) {
createFlash(s__('Metrics|There was an error while retrieving metrics'));
}
});
};
 
Loading
Loading
Loading
Loading
@@ -16,3 +16,4 @@ export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
Loading
Loading
@@ -96,4 +96,7 @@ export default {
[types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) {
state.additionalPanelTypesEnabled = enabled;
},
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
};
Loading
Loading
@@ -12,6 +12,7 @@ export default () => ({
additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
groups: [],
deploymentData: [],
environments: [],
Loading
Loading
Loading
Loading
@@ -29,6 +29,11 @@
padding: $gl-padding / 2;
}
 
.prometheus-graph-embed {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.prometheus-graph-header {
display: flex;
align-items: center;
Loading
Loading
import Vue from 'vue';
import renderMetrics from '~/behaviors/markdown/render_metrics';
import { TEST_HOST } from 'helpers/test_constants';
const originalExtend = Vue.extend;
describe('Render metrics for Gitlab Flavoured Markdown', () => {
const container = {
Metrics() {},
};
let spyExtend;
beforeEach(() => {
Vue.extend = () => container.Metrics;
spyExtend = jest.spyOn(Vue, 'extend');
});
afterEach(() => {
Vue.extend = originalExtend;
});
it('does nothing when no elements are found', () => {
renderMetrics([]);
expect(spyExtend).not.toHaveBeenCalled();
});
it('renders a vue component when elements are found', () => {
const element = document.createElement('div');
element.setAttribute('data-dashboard-url', TEST_HOST);
renderMetrics([element]);
expect(spyExtend).toHaveBeenCalled();
});
});
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Embed from '~/monitoring/components/embed.vue';
import MonitorAreaChart from '~/monitoring/components/charts/area.vue';
import { TEST_HOST } from 'helpers/test_constants';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Embed', () => {
let wrapper;
let store;
let actions;
function mountComponent() {
wrapper = shallowMount(Embed, {
localVue,
store,
propsData: {
dashboardUrl: TEST_HOST,
},
});
}
beforeEach(() => {
actions = {
setFeatureFlags: () => {},
setShowErrorBanner: () => {},
setEndpoints: () => {},
fetchMetricsData: () => {},
};
store = new Vuex.Store({
modules: {
monitoringDashboard: {
namespaced: true,
actions,
state: initialState,
},
},
});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('no metrics are available yet', () => {
beforeEach(() => {
mountComponent();
});
it('shows an empty state when no metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
expect(wrapper.find(MonitorAreaChart).exists()).toBe(false);
});
});
describe('metrics are available', () => {
beforeEach(() => {
store.state.monitoringDashboard.groups = groups;
store.state.monitoringDashboard.groups[0].metrics = metricsData;
store.state.monitoringDashboard.metricsWithData = metricsWithData;
mountComponent();
});
it('shows a chart when metrics are present', () => {
wrapper.setProps({});
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
expect(wrapper.find(MonitorAreaChart).exists()).toBe(true);
expect(wrapper.findAll(MonitorAreaChart).length).toBe(2);
});
});
});
export const metricsWithData = [15, 16];
export const groups = [
{
panels: [
{
title: 'Memory Usage (Total)',
type: 'area-chart',
y_label: 'Total Memory Used',
weight: 4,
metrics: [
{
id: 'system_metrics_kubernetes_container_memory_total',
metric_id: 15,
},
],
},
{
title: 'Core Usage (Total)',
type: 'area-chart',
y_label: 'Total Cores',
weight: 3,
metrics: [
{
id: 'system_metrics_kubernetes_container_cores_total',
metric_id: 16,
},
],
},
],
},
];
export const metrics = [
{
id: 'system_metrics_kubernetes_container_memory_total',
metric_id: 15,
},
{
id: 'system_metrics_kubernetes_container_cores_total',
metric_id: 16,
},
];
const queries = [
{
result: [
{
values: [
['Mon', 1220],
['Tue', 932],
['Wed', 901],
['Thu', 934],
['Fri', 1290],
['Sat', 1330],
['Sun', 1320],
],
},
],
},
];
export const metricsData = [
{
queries,
metrics: [
{
metric_id: 15,
},
],
},
{
queries,
metrics: [
{
metric_id: 16,
},
],
},
];
export const initialState = {
monitoringDashboard: {},
groups: [],
metricsWithData: [],
useDashboardEndpoint: true,
};
Loading
Loading
@@ -69,3 +69,9 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
 
// Tech debt issue TBD
testUtilsConfig.logModifiedComponents = false;
// Basic stub for MutationObserver
global.MutationObserver = () => ({
disconnect: () => {},
observe: () => {},
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment