Commit 202a150f authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Fatih Acet

Add support for additional panel types

This introduces the single stat chart component aside from
the area chart component
parent ef348618
......@@ -8,6 +8,7 @@ import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { chartHeight, graphTypes, lineTypes } from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
let debouncedResize;
......@@ -23,19 +24,7 @@ export default {
graphData: {
type: Object,
required: true,
validator(data) {
return (
Array.isArray(data.queries) &&
data.queries.filter(query => {
if (Array.isArray(query.result)) {
return (
query.result.filter(res => Array.isArray(res.values)).length === query.result.length
);
}
return false;
}).length === data.queries.length
);
},
validator: graphDataValidatorForValues.bind(null, false),
},
containerWidth: {
type: Number,
......
......@@ -4,6 +4,7 @@ import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { chartHeight } from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
export default {
components: {
......@@ -14,23 +15,11 @@ export default {
graphData: {
type: Object,
required: true,
validator(data) {
return (
Array.isArray(data.queries) &&
data.queries.filter(query => {
if (Array.isArray(query.result)) {
return (
query.result.filter(res => Array.isArray(res.values)).length === query.result.length
);
}
return false;
}).length === data.queries.length
);
},
containerWidth: {
type: Number,
required: true,
},
validator: graphDataValidatorForValues.bind(null, false),
},
containerWidth: {
type: Number,
required: true,
},
},
data() {
......
<script>
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { graphDataValidatorForValues } from '../../utils';
export default {
components: {
......@@ -7,22 +9,21 @@ export default {
},
inheritAttrs: false,
props: {
title: {
type: String,
required: true,
},
value: {
type: Number,
required: true,
},
unit: {
type: String,
graphData: {
type: Object,
required: true,
validator: graphDataValidatorForValues.bind(null, true),
},
},
computed: {
valueWithUnit() {
return `${this.value}${this.unit}`;
queryInfo() {
return this.graphData.queries[0];
},
engineeringNotation() {
return `${roundOffFloat(this.queryInfo.result[0].value[1], 1)}${this.queryInfo.unit}`;
},
graphTitle() {
return this.queryInfo.label;
},
},
};
......@@ -30,8 +31,8 @@ export default {
<template>
<div class="prometheus-graph col-12 col-lg-6">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ title }}</h5>
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5>
</div>
<gl-single-stat :value="valueWithUnit" :title="title" variant="success" />
<gl-single-stat :value="engineeringNotation" :title="graphTitle" variant="success" />
</div>
</template>
......@@ -7,6 +7,8 @@ import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import MonitorAreaChart from './charts/area.vue';
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';
......@@ -18,6 +20,8 @@ let sidebarMutationObserver;
export default {
components: {
MonitorAreaChart,
MonitorSingleStatChart,
PanelType,
GraphGroup,
EmptyState,
Icon,
......@@ -152,6 +156,7 @@ export default {
'useDashboardEndpoint',
'allDashboards',
'multipleDashboardsEnabled',
'additionalPanelTypesEnabled',
]),
groupsWithData() {
return this.groups.filter(group => this.chartsWithData(group.metrics).length > 0);
......@@ -173,6 +178,7 @@ export default {
deploymentsEndpoint: this.deploymentsEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
currentDashboard: this.currentDashboard,
projectPath: this.projectPath,
});
this.timeWindows = timeWindows;
......@@ -220,6 +226,8 @@ export default {
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
// TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
// Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
......@@ -228,6 +236,7 @@ export default {
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
// TODO: END
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
......@@ -366,24 +375,34 @@ export default {
:name="groupData.group"
:show-panels="showPanels"
>
<monitor-area-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:project-path="projectPath"
:graph-data="graphData"
:deployment-data="deploymentData"
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
group-id="monitor-area-chart"
>
<alert-widget
v-if="alertWidgetAvailable && graphData"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
@setAlerts="setAlerts"
<template v-if="additionalPanelTypesEnabled">
<panel-type
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="`panel-type-${graphIndex}`"
:graph-data="graphData"
:dashboard-width="elWidth"
/>
</monitor-area-chart>
</template>
<template v-else>
<monitor-area-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:graph-data="graphData"
:deployment-data="deploymentData"
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
:project-path="projectPath"
group-id="monitor-area-chart"
>
<alert-widget
v-if="alertWidgetAvailable && graphData"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
@setAlerts="setAlerts"
/>
</monitor-area-chart>
</template>
</graph-group>
</div>
<empty-state
......
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
import MonitorAreaChart from './charts/area.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
export default {
components: {
MonitorAreaChart,
MonitorSingleStatChart,
},
props: {
graphData: {
type: Object,
required: true,
},
dashboardWidth: {
type: Number,
required: true,
},
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath']),
alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
},
methods: {
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
},
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
isPanelType(type) {
return this.graphData.type && this.graphData.type === type;
},
},
};
</script>
<template>
<monitor-single-stat-chart v-if="isPanelType('single-stat')" :graph-data="graphData" />
<monitor-area-chart
v-else
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="dashboardWidth"
group-id="monitor-area-chart"
>
<alert-widget
v-if="alertWidgetAvailable"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
@setAlerts="setAlerts"
/>
</monitor-area-chart>
</template>
......@@ -12,6 +12,7 @@ export default (props = {}) => {
store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
});
}
......
......@@ -37,10 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => {
export const setFeatureFlags = (
{ commit },
{ prometheusEndpointEnabled, multipleDashboardsEnabled },
{ prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled },
) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
};
export const requestMetricsDashboard = ({ commit }) => {
......
......@@ -11,6 +11,7 @@ export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
export const SET_MULTIPLE_DASHBOARDS_ENABLED = 'SET_MULTIPLE_DASHBOARDS_ENABLED';
export const SET_ADDITIONAL_PANEL_TYPES_ENABLED = 'SET_ADDITIONAL_PANEL_TYPES_ENABLED';
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';
......
......@@ -75,6 +75,7 @@ export default {
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
state.dashboardEndpoint = endpoints.dashboardEndpoint;
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
},
[types.SET_DASHBOARD_ENABLED](state, enabled) {
state.useDashboardEndpoint = enabled;
......@@ -92,4 +93,7 @@ export default {
[types.SET_ALL_DASHBOARDS](state, dashboards) {
state.allDashboards = dashboards;
},
[types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) {
state.additionalPanelTypesEnabled = enabled;
},
};
......@@ -9,6 +9,7 @@ export default () => ({
dashboardEndpoint: invalidUrl,
useDashboardEndpoint: false,
multipleDashboardsEnabled: false,
additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
groups: [],
......@@ -17,4 +18,5 @@ export default () => ({
metricsWithData: [],
allDashboards: [],
currentDashboard: null,
projectPath: null,
});
......@@ -69,13 +69,26 @@ export const sortMetrics = metrics =>
.sortBy('weight')
.value();
export const normalizeQueryResult = timeSeries => ({
...timeSeries,
values: timeSeries.values.map(([timestamp, value]) => [
new Date(timestamp * 1000).toISOString(),
Number(value),
]),
});
export const normalizeQueryResult = timeSeries => {
let normalizedResult = {};
if (timeSeries.values) {
normalizedResult = {
...timeSeries,
values: timeSeries.values.map(([timestamp, value]) => [
new Date(timestamp * 1000).toISOString(),
Number(value),
]),
};
} else if (timeSeries.value) {
normalizedResult = {
...timeSeries,
value: [new Date(timeSeries.value[0] * 1000).toISOString(), Number(timeSeries.value[1])],
};
}
return normalizedResult;
};
export const normalizeMetrics = metrics => {
const groupedMetrics = groupQueriesByChartInfo(metrics);
......
......@@ -30,4 +30,28 @@ export const getTimeDiff = selectedTimeWindow => {
return { start, end };
};
/**
* This method is used to validate if the graph data format for a chart component
* that needs a time series as a response from a prometheus query (query_range) is
* of a valid format or not.
* @param {Object} graphData the graph data response from a prometheus request
* @returns {boolean} whether the graphData format is correct
*/
export const graphDataValidatorForValues = (isValues, graphData) => {
const responseValueKeyName = isValues ? 'value' : 'values';
return (
Array.isArray(graphData.queries) &&
graphData.queries.filter(query => {
if (Array.isArray(query.result)) {
return (
query.result.filter(res => Array.isArray(res[responseValueKeyName])).length ===
query.result.length
);
}
return false;
}).length === graphData.queries.length
);
};
export default {};
......@@ -13,6 +13,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts)
end
......
import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { graphDataPrometheusQuery } from '../mock_data';
describe('Single Stat Chart component', () => {
let singleStatChart;
......@@ -7,9 +8,7 @@ describe('Single Stat Chart component', () => {
beforeEach(() => {
singleStatChart = shallowMount(SingleStatChart, {
propsData: {
title: 'Time to render',
value: 1,
unit: 'sec',
graphData: graphDataPrometheusQuery,
},
});
});
......@@ -19,9 +18,9 @@ describe('Single Stat Chart component', () => {
});
describe('computed', () => {
describe('valueWithUnit', () => {
describe('engineeringNotation', () => {
it('should interpolate the value and unit props', () => {
expect(singleStatChart.vm.valueWithUnit).toBe('1sec');
expect(singleStatChart.vm.engineeringNotation).toBe('91MB');
});
});
});
......
......@@ -935,3 +935,75 @@ export const dashboardGitResponse = [
default: false,
},
];
export const graphDataPrometheusQuery = {
title: 'Super Chart A2',
type: 'single-stat',
weight: 2,
metrics: [
{
id: 'metric_a1',
metric_id: 2,
query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
unit: 'MB',
label: 'Total Consumption',
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
},
],
queries: [
{
metricId: null,
id: 'metric_a1',
metric_id: 2,
query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
unit: 'MB',
label: 'Total Consumption',
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
result: [
{
metric: { job: 'prometheus' },
value: ['2019-06-26T21:03:20.881Z', 91],
},
],
},
],
};
export const graphDataPrometheusQueryRange = {
title: 'Super Chart A1',
type: 'area',
weight: 2,
metrics: [
{
id: 'metric_a1',
metric_id: 2,
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
unit: 'MB',
label: 'Total Consumption',
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
},
],
queries: [
{
metricId: null,
id: 'metric_a1',
metric_id: 2,
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
unit: 'MB',
label: 'Total Consumption',
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
result: [
{
metric: {},
values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']],
},
],
},
],
};
......@@ -115,12 +115,14 @@ describe('Monitoring mutations', () => {
environmentsEndpoint: 'environments.json',
deploymentsEndpoint: 'deployments.json',
dashboardEndpoint: 'dashboard.json',
projectPath: '/gitlab-org/gitlab-ce',
});
expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json');
expect(stateCopy.environmentsEndpoint).toEqual('environments.json');
expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-ce');
});
});
......
import { getTimeDiff } from '~/monitoring/utils';
import { getTimeDiff, graphDataValidatorForValues } from '~/monitoring/utils';
import { timeWindows } from '~/monitoring/constants';
import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data';
describe('getTimeDiff', () => {
it('defaults to an 8 hour (28800s) difference', () => {
......@@ -27,3 +28,27 @@ describe('getTimeDiff', () => {
});
});
});
describe('graphDataValidatorForValues', () => {
/*
* When dealing with a metric using the query format, e.g.
* query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024'
* the validator will look for the `value` key instead of `values`
*/
it('validates data with the query format', () => {
const validGraphData = graphDataValidatorForValues(true, graphDataPrometheusQuery);
expect(validGraphData).toBe(true);
});
/*
* When dealing with a metric using the query?range format, e.g.
* query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
* the validator will look for the `values` key instead of `value`
*/
it('validates data with the query_range format', () => {
const validGraphData = graphDataValidatorForValues(false, graphDataPrometheusQueryRange);
expect(validGraphData).toBe(true);
});
});
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