summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/monitoring')
-rw-r--r--app/assets/javascripts/monitoring/components/charts/anomaly.vue227
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue73
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue96
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue232
-rw-r--r--app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/shared/prometheus_header.vue15
-rw-r--r--app/assets/javascripts/monitoring/constants.js15
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js7
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js54
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js38
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js6
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js6
-rw-r--r--app/assets/javascripts/monitoring/utils.js19
17 files changed, 559 insertions, 296 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
new file mode 100644
index 00000000000..8eeac737a11
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -0,0 +1,227 @@
+<script>
+import { flatten, isNumber } from 'underscore';
+import { GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import { roundOffFloat } from '~/lib/utils/common_utils';
+import { hexToRgb } from '~/lib/utils/color_utils';
+import { areaOpacityValues, symbolSizes, colorValues } from '../../constants';
+import { graphDataValidatorForAnomalyValues } from '../../utils';
+import MonitorTimeSeriesChart from './time_series.vue';
+
+/**
+ * Series indexes
+ */
+const METRIC = 0;
+const UPPER = 1;
+const LOWER = 2;
+
+/**
+ * Boundary area appearance
+ */
+const AREA_COLOR = colorValues.anomalyAreaColor;
+const AREA_OPACITY = areaOpacityValues.default;
+const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})`;
+
+/**
+ * The anomaly component highlights when a metric shows
+ * some anomalous behavior.
+ *
+ * It shows both a metric line and a boundary band in a
+ * time series chart, the boundary band shows the normal
+ * range of values the metric should take.
+ *
+ * This component accepts 3 queries, which contain the
+ * "metric", "upper" limit and "lower" limit.
+ *
+ * The upper and lower series are "stacked areas" visually
+ * to create the boundary band, and if any "metric" value
+ * is outside this band, it is highlighted to warn users.
+ *
+ * The boundary band stack must be painted above the 0 line
+ * so the area is shown correctly. If any of the values of
+ * the data are negative, the chart data is shifted to be
+ * above 0 line.
+ *
+ * The data passed to the time series is will always be
+ * positive, but reformatted to show the original values of
+ * data.
+ *
+ */
+export default {
+ components: {
+ GlLineChart,
+ GlChartSeriesLabel,
+ MonitorTimeSeriesChart,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForAnomalyValues,
+ },
+ },
+ computed: {
+ series() {
+ return this.graphData.queries.map(query => {
+ const values = query.result[0] ? query.result[0].values : [];
+ return {
+ label: query.label,
+ data: values.filter(([, value]) => !Number.isNaN(value)),
+ };
+ });
+ },
+ /**
+ * If any of the values of the data is negative, the
+ * chart data is shifted to the lowest value
+ *
+ * This offset is the lowest value.
+ */
+ yOffset() {
+ const values = flatten(this.series.map(ser => ser.data.map(([, y]) => y)));
+ const min = values.length ? Math.floor(Math.min(...values)) : 0;
+ return min < 0 ? -min : 0;
+ },
+ metricData() {
+ const originalMetricQuery = this.graphData.queries[0];
+
+ const metricQuery = { ...originalMetricQuery };
+ metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
+ x,
+ y + this.yOffset,
+ ]);
+ return {
+ ...this.graphData,
+ type: 'line-chart',
+ queries: [metricQuery],
+ };
+ },
+ metricSeriesConfig() {
+ return {
+ type: 'line',
+ symbol: 'circle',
+ symbolSize: (val, params) => {
+ if (this.isDatapointAnomaly(params.dataIndex)) {
+ return symbolSizes.anomaly;
+ }
+ // 0 causes echarts to throw an error, use small number instead
+ // see https://gitlab.com/gitlab-org/gitlab-ui/issues/423
+ return 0.001;
+ },
+ showSymbol: true,
+ itemStyle: {
+ color: params => {
+ if (this.isDatapointAnomaly(params.dataIndex)) {
+ return colorValues.anomalySymbol;
+ }
+ return colorValues.primaryColor;
+ },
+ },
+ };
+ },
+ chartOptions() {
+ const [, upperSeries, lowerSeries] = this.series;
+ const calcOffsetY = (data, offsetCallback) =>
+ data.map((value, dataIndex) => {
+ const [x, y] = value;
+ return [x, y + offsetCallback(dataIndex)];
+ });
+
+ const yAxisWithOffset = {
+ name: this.yAxisLabel,
+ axisLabel: {
+ formatter: num => roundOffFloat(num - this.yOffset, 3).toString(),
+ },
+ };
+
+ /**
+ * Boundary is rendered by 2 series: An invisible
+ * series (opacity: 0) stacked on a visible one.
+ *
+ * Order is important, lower boundary is stacked
+ * *below* the upper boundary.
+ */
+ const boundarySeries = [];
+
+ if (upperSeries.data.length && lowerSeries.data.length) {
+ // Lower boundary, plus the offset if negative values
+ boundarySeries.push(
+ this.makeBoundarySeries({
+ name: this.formatLegendLabel(lowerSeries),
+ data: calcOffsetY(lowerSeries.data, () => this.yOffset),
+ }),
+ );
+ // Upper boundary, minus the lower boundary
+ boundarySeries.push(
+ this.makeBoundarySeries({
+ name: this.formatLegendLabel(upperSeries),
+ data: calcOffsetY(upperSeries.data, i => -this.yValue(LOWER, i)),
+ areaStyle: {
+ color: AREA_COLOR,
+ opacity: AREA_OPACITY,
+ },
+ }),
+ );
+ }
+ return { yAxis: yAxisWithOffset, series: boundarySeries };
+ },
+ },
+ methods: {
+ formatLegendLabel(query) {
+ return query.label;
+ },
+ yValue(seriesIndex, dataIndex) {
+ const d = this.series[seriesIndex].data[dataIndex];
+ return d && d[1];
+ },
+ yValueFormatted(seriesIndex, dataIndex) {
+ const y = this.yValue(seriesIndex, dataIndex);
+ return isNumber(y) ? y.toFixed(3) : '';
+ },
+ isDatapointAnomaly(dataIndex) {
+ const yVal = this.yValue(METRIC, dataIndex);
+ const yUpper = this.yValue(UPPER, dataIndex);
+ const yLower = this.yValue(LOWER, dataIndex);
+ return (isNumber(yUpper) && yVal > yUpper) || (isNumber(yLower) && yVal < yLower);
+ },
+ makeBoundarySeries(series) {
+ const stackKey = 'anomaly-boundary-series-stack';
+ return {
+ type: 'line',
+ stack: stackKey,
+ lineStyle: {
+ width: 0,
+ color: AREA_COLOR_RGBA, // legend color
+ },
+ color: AREA_COLOR_RGBA, // tooltip color
+ symbol: 'none',
+ ...series,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <monitor-time-series-chart
+ v-bind="$attrs"
+ :graph-data="metricData"
+ :option="chartOptions"
+ :series-config="metricSeriesConfig"
+ >
+ <slot></slot>
+ <template v-slot:tooltipContent="slotProps">
+ <div
+ v-for="(content, seriesIndex) in slotProps.tooltip.content"
+ :key="seriesIndex"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="content.color">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ yValueFormatted(seriesIndex, content.dataIndex) }}
+ </div>
+ </div>
+ </template>
+ </monitor-time-series-chart>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
new file mode 100644
index 00000000000..b8158247e49
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlHeatmap } from '@gitlab/ui/dist/charts';
+import dateformat from 'dateformat';
+import PrometheusHeader from '../shared/prometheus_header.vue';
+import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
+import { graphDataValidatorForValues } from '../../utils';
+
+export default {
+ components: {
+ GlHeatmap,
+ ResizableChartContainer,
+ PrometheusHeader,
+ },
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, false),
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ chartData() {
+ return this.queries.result.reduce(
+ (acc, result, i) => [...acc, ...result.values.map((value, j) => [i, j, value[1]])],
+ [],
+ );
+ },
+ xAxisName() {
+ return this.graphData.x_label || '';
+ },
+ yAxisName() {
+ return this.graphData.y_label || '';
+ },
+ xAxisLabels() {
+ return this.queries.result.map(res => Object.values(res.metric)[0]);
+ },
+ yAxisLabels() {
+ return this.result.values.map(val => {
+ const [yLabel] = val;
+
+ return dateformat(new Date(yLabel), 'HH:MM:ss');
+ });
+ },
+ result() {
+ return this.queries.result[0];
+ },
+ queries() {
+ return this.graphData.queries[0];
+ },
+ },
+};
+</script>
+<template>
+ <div class="prometheus-graph col-12 col-lg-6">
+ <prometheus-header :graph-title="graphData.title" />
+ <resizable-chart-container>
+ <gl-heatmap
+ ref="heatmapChart"
+ v-bind="$attrs"
+ :data-series="chartData"
+ :x-axis-name="xAxisName"
+ :y-axis-name="yAxisName"
+ :x-axis-labels="xAxisLabels"
+ :y-axis-labels="yAxisLabels"
+ :width="containerWidth"
+ />
+ </resizable-chart-container>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 78fe575717a..6a88c8a5ee3 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,17 +1,23 @@
<script>
import { s__, __ } from '~/locale';
-import { GlLink, GlButton, GlTooltip } from '@gitlab/ui';
+import _ from 'underscore';
+import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
-import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
+import { roundOffFloat } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
-import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
+import {
+ chartHeight,
+ graphTypes,
+ lineTypes,
+ lineWidths,
+ symbolSizes,
+ dateFormats,
+} from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
-let debouncedResize;
-
export default {
components: {
GlAreaChart,
@@ -22,6 +28,9 @@ export default {
GlLink,
Icon,
},
+ directives: {
+ GlResizeObserverDirective,
+ },
inheritAttrs: false,
props: {
graphData: {
@@ -29,9 +38,15 @@ export default {
required: true,
validator: graphDataValidatorForValues.bind(null, false),
},
- containerWidth: {
- type: Number,
- required: true,
+ option: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ seriesConfig: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
deploymentData: {
type: Array,
@@ -99,29 +114,35 @@ export default {
const lineWidth =
appearance && appearance.line && appearance.line.width
? appearance.line.width
- : undefined;
+ : lineWidths.default;
const areaStyle = {
opacity:
appearance && appearance.area && typeof appearance.area.opacity === 'number'
? appearance.area.opacity
: undefined,
};
-
const series = makeDataSeries(query.result, {
name: this.formatLegendLabel(query),
lineStyle: {
type: lineType,
width: lineWidth,
+ color: this.primaryColor,
},
showSymbol: false,
areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
+ ...this.seriesConfig,
});
return acc.concat(series);
}, []);
},
+ chartOptionSeries() {
+ return (this.option.series || []).concat(this.scatterSeries ? [this.scatterSeries] : []);
+ },
chartOptions() {
+ const option = _.omit(this.option, 'series');
return {
+ series: this.chartOptionSeries,
xAxis: {
name: __('Time'),
type: 'time',
@@ -138,8 +159,8 @@ export default {
formatter: num => roundOffFloat(num, 3).toString(),
},
},
- series: this.scatterSeries,
dataZoom: [this.dataZoomConfig],
+ ...option,
};
},
dataZoomConfig() {
@@ -147,6 +168,14 @@ export default {
return handleIcon ? { handleIcon } : {};
},
+ /**
+ * This method returns the earliest time value in all series of a chart.
+ * Takes a chart data with data to populate a timeseries.
+ * data should be an array of data points [t, y] where t is a ISO formatted date,
+ * and is sorted by t (time).
+ * @returns {(String|null)} earliest x value from all series, or null when the
+ * chart series data is empty.
+ */
earliestDatapoint() {
return this.chartData.reduce((acc, series) => {
const { data } = series;
@@ -206,21 +235,13 @@ export default {
return `${this.graphData.y_label}`;
},
},
- watch: {
- containerWidth: 'onResize',
- },
mounted() {
const graphTitleEl = this.$refs.graphTitle;
if (graphTitleEl && graphTitleEl.scrollWidth > graphTitleEl.offsetWidth) {
this.showTitleTooltip = true;
}
},
- beforeDestroy() {
- window.removeEventListener('resize', debouncedResize);
- },
created() {
- debouncedResize = debounceByAnimationFrame(this.onResize);
- window.addEventListener('resize', debouncedResize);
this.setSvg('rocket');
this.setSvg('scroll-handle');
},
@@ -241,10 +262,11 @@ export default {
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else {
- const { seriesName, color } = dataPoint;
+ const { seriesName, color, dataIndex } = dataPoint;
const value = yVal.toFixed(3);
this.tooltip.content.push({
name: seriesName,
+ dataIndex,
value,
color,
});
@@ -276,7 +298,7 @@ export default {
</script>
<template>
- <div class="prometheus-graph">
+ <div v-gl-resize-observer-directive="onResize" class="prometheus-graph">
<div class="prometheus-graph-header">
<h5
ref="graphTitle"
@@ -317,23 +339,27 @@ export default {
</template>
<template v-else>
<template slot="tooltipTitle">
- <div class="text-nowrap">
- {{ tooltip.title }}
- </div>
+ <slot name="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </slot>
</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 }}
+ <slot name="tooltipContent" :tooltip="tooltip">
+ <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>
- </div>
+ </slot>
</template>
</template>
</component>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index b4ea415bb51..26e2c2568c1 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -11,7 +11,7 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
@@ -22,12 +22,9 @@ import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
-import { sidebarAnimationDuration } from '../constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-let sidebarMutationObserver;
-
export default {
components: {
VueDraggable,
@@ -167,10 +164,10 @@ export default {
data() {
return {
state: 'gettingStarted',
- elWidth: 0,
formIsValid: null,
selectedTimeWindow: {},
isRearrangingPanels: false,
+ hasValidDates: true,
};
},
computed: {
@@ -178,7 +175,7 @@ export default {
return this.customMetricsAvailable && this.customMetricsPath.length;
},
...mapState('monitoringDashboard', [
- 'groups',
+ 'dashboard',
'emptyState',
'showEmptyState',
'environments',
@@ -189,10 +186,15 @@ export default {
'additionalPanelTypesEnabled',
]),
firstDashboard() {
- return this.allDashboards[0] || {};
+ return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
+ ? this.allDashboards[0]
+ : {};
+ },
+ selectedDashboard() {
+ return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
},
selectedDashboardText() {
- return this.currentDashboard || this.firstDashboard.display_name;
+ return this.selectedDashboard.display_name;
},
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
@@ -200,8 +202,13 @@ export default {
addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState;
},
- alertWidgetAvailable() {
- return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint;
+ hasHeaderButtons() {
+ return (
+ this.addingMetricsAvailable ||
+ this.showRearrangePanelsBtn ||
+ this.selectedDashboard.can_edit ||
+ this.externalDashboardUrl.length
+ );
},
},
created() {
@@ -214,11 +221,6 @@ export default {
projectPath: this.projectPath,
});
},
- beforeDestroy() {
- if (sidebarMutationObserver) {
- sidebarMutationObserver.disconnect();
- }
- },
mounted() {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
@@ -235,17 +237,12 @@ export default {
this.selectedTimeWindow = range;
if (!isValidDate(start) || !isValidDate(end)) {
+ this.hasValidDates = false;
this.showInvalidDateError();
} else {
+ this.hasValidDates = true;
this.fetchData(range);
}
-
- sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
- sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
- attributes: true,
- childList: false,
- subtree: false,
- });
}
},
methods: {
@@ -253,43 +250,25 @@ export default {
'fetchData',
'setGettingStartedEmptyState',
'setEndpoints',
- 'setDashboardEnabled',
+ 'setPanelGroupMetrics',
]),
chartsWithData(charts) {
- if (!this.useDashboardEndpoint) {
- return charts;
- }
return charts.filter(chart =>
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
- csvText(graphData) {
- const chartData = graphData.queries[0].result[0].values;
- const yLabel = graphData.y_label;
- const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
- },
- downloadCsv(graphData) {
- const data = new Blob([this.csvText(graphData)], { type: 'text/plain' });
- return window.URL.createObjectURL(data);
- },
- // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
- // Issue number: https://gitlab.com/gitlab-org/gitlab-foss/issues/63845
- 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));
- },
- showToast() {
- this.$toast.show(__('Link copied'));
- },
- // TODO: END
+ updateMetrics(key, metrics) {
+ this.setPanelGroupMetrics({
+ metrics,
+ key,
+ });
+ },
+ removeMetric(key, metrics, graphIndex) {
+ this.setPanelGroupMetrics({
+ metrics: metrics.filter((v, i) => i !== graphIndex),
+ key,
+ });
+ },
removeGraph(metrics, graphIndex) {
// At present graphs will not be removed, they should removed using the vuex store
// See https://gitlab.com/gitlab-org/gitlab/issues/27835
@@ -306,11 +285,6 @@ export default {
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
- onSidebarMutation() {
- setTimeout(() => {
- this.elWidth = this.$el.clientWidth;
- }, sidebarAnimationDuration);
- },
toggleRearrangingPanels() {
this.isRearrangingPanels = !this.isRearrangingPanels;
},
@@ -389,7 +363,7 @@ export default {
</gl-form-group>
<gl-form-group
- v-if="!showEmptyState"
+ v-if="hasValidDates"
:label="s__('Metrics|Show last')"
label-size="sm"
label-for="monitor-time-window-dropdown"
@@ -403,7 +377,7 @@ export default {
</template>
<gl-form-group
- v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length"
+ v-if="hasHeaderButtons"
label-for="prometheus-graphs-dropdown-buttons"
class="dropdown-buttons col-md d-md-flex col-lg d-lg-flex align-items-end"
>
@@ -451,6 +425,14 @@ export default {
</gl-modal>
<gl-button
+ v-if="selectedDashboard.can_edit"
+ class="mt-1 js-edit-link"
+ :href="selectedDashboard.project_blob_path"
+ >
+ {{ __('Edit dashboard') }}
+ </gl-button>
+
+ <gl-button
v-if="externalDashboardUrl.length"
class="mt-1 js-external-dashboard-link"
variant="primary"
@@ -468,116 +450,46 @@ export default {
<div v-if="!showEmptyState">
<graph-group
- v-for="(groupData, index) in groups"
+ v-for="(groupData, index) in dashboard.panel_groups"
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
:collapse-group="groupHasData(groupData)"
>
- <template v-if="additionalPanelTypesEnabled">
- <vue-draggable
- :list="groupData.metrics"
- group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
- :disabled="!isRearrangingPanels"
+ <vue-draggable
+ :value="groupData.metrics"
+ group="metrics-dashboard"
+ :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :disabled="!isRearrangingPanels"
+ @input="updateMetrics(groupData.key, $event)"
+ >
+ <div
+ v-for="(graphData, graphIndex) in groupData.metrics"
+ :key="`panel-type-${graphIndex}`"
+ class="col-12 col-lg-6 px-2 mb-2 draggable"
+ :class="{ 'draggable-enabled': isRearrangingPanels }"
>
- <div
- v-for="(graphData, graphIndex) in groupData.metrics"
- :key="`panel-type-${graphIndex}`"
- class="col-12 col-lg-6 px-2 mb-2 draggable"
- :class="{ 'draggable-enabled': isRearrangingPanels }"
- >
- <div class="position-relative draggable-panel js-draggable-panel">
- <div
- v-if="isRearrangingPanels"
- class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
- @click="removeGraph(groupData.metrics, graphIndex)"
- >
- <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
- ><icon name="close"
- /></a>
- </div>
-
- <panel-type
- :clipboard-text="
- generateLink(groupData.group, graphData.title, graphData.y_label)
- "
- :graph-data="graphData"
- :dashboard-width="elWidth"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
- :index="`${index}-${graphIndex}`"
- />
+ <div class="position-relative draggable-panel js-draggable-panel">
+ <div
+ v-if="isRearrangingPanels"
+ class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
+ @click="removeGraph(groupData.metrics, graphIndex)"
+ >
+ <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
+ ><icon name="close"
+ /></a>
</div>
- </div>
- </vue-draggable>
- </template>
- <template v-else>
- <monitor-time-series-chart
- v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
- :key="graphIndex"
- class="col-12 col-lg-6 pb-3"
- :graph-data="graphData"
- :deployment-data="deploymentData"
- :thresholds="getGraphAlertValues(graphData.queries)"
- :container-width="elWidth"
- :project-path="projectPath"
- group-id="monitor-time-series-chart"
- >
- <div
- class="d-flex align-items-center"
- :class="alertWidgetAvailable ? 'justify-content-between' : 'justify-content-end'"
- >
- <alert-widget
- v-if="alertWidgetAvailable && graphData"
- :modal-id="`alert-modal-${index}-${graphIndex}`"
+
+ <panel-type
+ :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
+ :graph-data="graphData"
:alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.queries"
- :alerts-to-manage="getGraphAlerts(graphData.queries)"
- @setAlerts="setAlerts"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ :index="`${index}-${graphIndex}`"
/>
- <gl-dropdown
- v-gl-tooltip
- class="ml-2 mr-3"
- toggle-class="btn btn-transparent border-0"
- :right="true"
- :no-caret="true"
- :title="__('More actions')"
- >
- <template slot="button-content">
- <icon name="ellipsis_v" class="text-secondary" />
- </template>
- <gl-dropdown-item
- v-track-event="downloadCSVOptions(graphData.title)"
- :href="downloadCsv(graphData)"
- download="chart_metrics.csv"
- >
- {{ __('Download CSV') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-track-event="
- generateLinkToChartOptions(
- generateLink(groupData.group, graphData.title, graphData.y_label),
- )
- "
- class="js-chart-link"
- :data-clipboard-text="
- generateLink(groupData.group, graphData.title, graphData.y_label)
- "
- @click="showToast"
- >
- {{ __('Generate link to chart') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="alertWidgetAvailable"
- v-gl-modal="`alert-modal-${index}-${graphIndex}`"
- >
- {{ __('Alerts') }}
- </gl-dropdown-item>
- </gl-dropdown>
</div>
- </monitor-time-series-chart>
- </template>
+ </div>
+ </vue-draggable>
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
index 4616a767295..8749019c5cd 100644
--- a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
@@ -55,17 +55,13 @@ export default {
};
},
},
+ watch: {
+ selectedTimeWindow() {
+ this.verifyTimeRange();
+ },
+ },
mounted() {
- const range = getTimeWindow(this.selectedTimeWindow);
- if (range) {
- this.selectedTimeWindowText = this.timeWindows[range];
- } else {
- this.customTime = {
- from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
- to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
- };
- this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
- }
+ this.verifyTimeRange();
},
methods: {
activeTimeWindow(key) {
@@ -87,6 +83,18 @@ export default {
closeDropdown() {
this.$refs.dropdown.hide();
},
+ verifyTimeRange() {
+ const range = getTimeWindow(this.selectedTimeWindow);
+ if (range) {
+ this.selectedTimeWindowText = this.timeWindows[range];
+ } else {
+ this.customTime = {
+ from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
+ to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
+ };
+ this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index 7857aaa6ecc..f75839c7c6b 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -35,9 +35,9 @@ export default {
};
},
computed: {
- ...mapState('monitoringDashboard', ['groups', 'metricsWithData']),
+ ...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']),
charts() {
- const groupWithMetrics = this.groups.find(group =>
+ const groupWithMetrics = this.dashboard.panel_groups.find(group =>
group.metrics.find(chart => this.chartHasData(chart)),
) || { metrics: [] };
@@ -78,9 +78,6 @@ export default {
}, sidebarAnimationDuration);
},
setInitialState() {
- this.setFeatureFlags({
- prometheusEndpointEnabled: true,
- });
this.setEndpoints({
dashboardEndpoint: removeParams(['start', 'end'], this.dashboardUrl),
});
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index ee3a2bae79b..3cb6ccb64b1 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -45,7 +45,7 @@ export default {
<div v-if="showPanels" class="card prometheus-panel">
<div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4>
- <a role="button" @click="collapse">
+ <a role="button" class="js-graph-group-toggle" @click="collapse">
<icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" />
</a>
</div>
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 1a14d06f4c8..cafb4b0b479 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -11,7 +11,9 @@ import {
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
+import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
+import MonitorHeatmapChart from './charts/heatmap.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
@@ -19,7 +21,7 @@ import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default {
components: {
MonitorSingleStatChart,
- MonitorTimeSeriesChart,
+ MonitorHeatmapChart,
MonitorEmptyChart,
Icon,
GlDropdown,
@@ -40,10 +42,6 @@ export default {
type: Object,
required: true,
},
- dashboardWidth: {
- type: Number,
- required: true,
- },
index: {
type: String,
required: false,
@@ -71,6 +69,12 @@ export default {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
+ monitorChartComponent() {
+ if (this.isPanelType('anomaly-chart')) {
+ return MonitorAnomalyChart;
+ }
+ return MonitorTimeSeriesChart;
+ },
},
methods: {
getGraphAlerts(queries) {
@@ -97,14 +101,19 @@ export default {
v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData"
/>
- <monitor-time-series-chart
+ <monitor-heatmap-chart
+ v-else-if="isPanelType('heatmap') && graphDataHasMetrics"
+ :graph-data="graphData"
+ :container-width="dashboardWidth"
+ />
+ <component
+ :is="monitorChartComponent"
v-else-if="graphDataHasMetrics"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.queries)"
- :container-width="dashboardWidth"
- group-id="monitor-area-chart"
+ group-id="panel-type-chart"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -146,6 +155,6 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-time-series-chart>
+ </component>
<monitor-empty-chart v-else :graph-title="graphData.title" />
</template>
diff --git a/app/assets/javascripts/monitoring/components/shared/prometheus_header.vue b/app/assets/javascripts/monitoring/components/shared/prometheus_header.vue
new file mode 100644
index 00000000000..153c8f389db
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/shared/prometheus_header.vue
@@ -0,0 +1,15 @@
+<script>
+export default {
+ props: {
+ graphTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title js-graph-title">{{ graphTitle }}</h5>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 2836fe4fc26..1a1fcdd0e66 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -14,13 +14,28 @@ export const graphTypes = {
};
export const symbolSizes = {
+ anomaly: 8,
default: 14,
};
+export const areaOpacityValues = {
+ default: 0.2,
+};
+
+export const colorValues = {
+ primaryColor: '#1f78d1', // $blue-500 (see variables.scss)
+ anomalySymbol: '#db3b21',
+ anomalyAreaColor: '#1f78d1',
+};
+
export const lineTypes = {
default: 'solid',
};
+export const lineWidths = {
+ default: 2,
+};
+
export const timeWindows = {
thirtyMinutes: __('30 minutes'),
threeHours: __('3 hours'),
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 6aa1fb5e9c6..a14145d480b 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -11,13 +11,6 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
- if (gon.features) {
- store.dispatch('monitoringDashboard/setFeatureFlags', {
- prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
- additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
- });
- }
-
const [currentDashboard] = getParameterValues('dashboard');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 2cf34ddb45b..6a8e3cc82f5 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -7,7 +7,7 @@ import { s__, __ } from '../../locale';
const MAX_REQUESTS = 3;
-function backOffRequest(makeRequestCallback) {
+export function backOffRequest(makeRequestCallback) {
let requestCounter = 0;
return backOff((next, stop) => {
makeRequestCallback()
@@ -35,14 +35,6 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
-export const setFeatureFlags = (
- { commit },
- { prometheusEndpointEnabled, additionalPanelTypesEnabled },
-) => {
- commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
- commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
-};
-
export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
@@ -79,29 +71,7 @@ export const fetchData = ({ dispatch }, params) => {
dispatch('fetchEnvironmentsData');
};
-export const fetchMetricsData = ({ state, dispatch }, params) => {
- if (state.useDashboardEndpoint) {
- return dispatch('fetchDashboard', params);
- }
-
- dispatch('requestMetricsData');
-
- return backOffRequest(() => axios.get(state.metricsEndpoint, { params }))
- .then(resp => resp.data)
- .then(response => {
- if (!response || !response.data || !response.success) {
- dispatch('receiveMetricsDataFailure', null);
- createFlash(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
- }
- dispatch('receiveMetricsDataSuccess', response.data);
- })
- .catch(error => {
- dispatch('receiveMetricsDataFailure', error);
- if (state.setShowErrorBanner) {
- createFlash(s__('Metrics|There was an error while retrieving metrics'));
- }
- });
-};
+export const fetchMetricsData = ({ dispatch }, params) => dispatch('fetchDashboard', params);
export const fetchDashboard = ({ state, dispatch }, params) => {
dispatch('requestMetricsDashboard');
@@ -111,11 +81,13 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
params.dashboard = state.currentDashboard;
}
- return axios
- .get(state.dashboardEndpoint, { params })
+ return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data)
.then(response => {
- dispatch('receiveMetricsDashboardSuccess', { response, params });
+ dispatch('receiveMetricsDashboardSuccess', {
+ response,
+ params,
+ });
})
.catch(error => {
dispatch('receiveMetricsDashboardFailure', error);
@@ -166,7 +138,7 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
commit(types.REQUEST_METRICS_DATA);
const promises = [];
- state.groups.forEach(group => {
+ state.dashboard.panel_groups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
promises.push(dispatch('fetchPrometheusMetric', { metric, params }));
@@ -221,5 +193,15 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
});
};
+/**
+ * Set a new array of metrics to a panel group
+ * @param {*} data An object containing
+ * - `key` with a unique panel key
+ * - `metrics` with the metrics array
+ */
+export const setPanelGroupMetrics = ({ commit }, data) => {
+ commit(types.SET_PANEL_GROUP_METRICS, data);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 9c546427c6e..fa15a2ba800 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -9,10 +9,9 @@ export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCC
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
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_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';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
+export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 320b33d3d69..696af5aed75 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
+import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
-import { normalizeMetrics, sortMetrics, normalizeMetric, normalizeQueryResult } from './utils';
+import { normalizeMetrics, normalizeMetric, normalizeQueryResult } from './utils';
const normalizePanel = panel => panel.metrics.map(normalizeMetric);
@@ -10,10 +11,12 @@ export default {
state.showEmptyState = true;
},
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
- state.groups = groupData.map(group => {
+ state.dashboard.panel_groups = groupData.map((group, i) => {
+ const key = `${slugify(group.group || 'default')}-${i}`;
let { metrics = [], panels = [] } = group;
// each panel has metric information that needs to be normalized
+
panels = panels.map(panel => ({
...panel,
metrics: normalizePanel(panel),
@@ -22,24 +25,21 @@ export default {
// for backwards compatibility, and to limit Vue template changes:
// for each group alias panels to metrics
// for each panel alias metrics to queries
- if (state.useDashboardEndpoint) {
- metrics = panels.map(panel => ({
- ...panel,
- queries: panel.metrics,
- }));
- }
+ metrics = panels.map(panel => ({
+ ...panel,
+ queries: panel.metrics,
+ }));
return {
...group,
panels,
- metrics: normalizeMetrics(sortMetrics(metrics)),
+ key,
+ metrics: normalizeMetrics(metrics),
};
});
- if (!state.groups.length) {
+ if (!state.dashboard.panel_groups.length) {
state.emptyState = 'noData';
- } else {
- state.showEmptyState = false;
}
},
[types.RECEIVE_METRICS_DATA_FAILURE](state, error) {
@@ -65,7 +65,7 @@ export default {
state.showEmptyState = false;
- state.groups.forEach(group => {
+ state.dashboard.panel_groups.forEach(group => {
group.metrics.forEach(metric => {
metric.queries.forEach(query => {
if (query.metric_id === metricId) {
@@ -86,9 +86,6 @@ export default {
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
},
- [types.SET_DASHBOARD_ENABLED](state, enabled) {
- state.useDashboardEndpoint = enabled;
- },
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
},
@@ -97,12 +94,13 @@ export default {
state.emptyState = 'noData';
},
[types.SET_ALL_DASHBOARDS](state, dashboards) {
- state.allDashboards = dashboards;
- },
- [types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) {
- state.additionalPanelTypesEnabled = enabled;
+ state.allDashboards = dashboards || [];
},
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
+ [types.SET_PANEL_GROUP_METRICS](state, payload) {
+ const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
+ panelGroup.metrics = payload.metrics;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index e894e988f6a..87e94311176 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -7,12 +7,12 @@ export default () => ({
environmentsEndpoint: null,
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
- useDashboardEndpoint: false,
- additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
- groups: [],
+ dashboard: {
+ panel_groups: [],
+ },
deploymentData: [],
environments: [],
metricsWithData: [],
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index a19829f0c65..8a396b15a31 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -82,12 +82,6 @@ export const normalizeMetric = (metric = {}) =>
'id',
);
-export const sortMetrics = metrics =>
- _.chain(metrics)
- .sortBy('title')
- .sortBy('weight')
- .value();
-
export const normalizeQueryResult = timeSeries => {
let normalizedResult = {};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 00f188c1d5a..2ae1647011d 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -1,7 +1,6 @@
import dateformat from 'dateformat';
import { secondsIn, dateTimePickerRegex, dateFormats } from './constants';
-
-const secondsToMilliseconds = seconds => seconds * 1000;
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
export const getTimeDiff = timeWindow => {
const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds
@@ -131,4 +130,20 @@ export const downloadCSVOptions = title => {
return { category, action, label: 'Chart title', property: title };
};
+/**
+ * This function validates the graph data contains exactly 3 queries plus
+ * value validations from graphDataValidatorForValues.
+ * @param {Object} isValues
+ * @param {Object} graphData the graph data response from a prometheus request
+ * @returns {boolean} true if the data is valid
+ */
+export const graphDataValidatorForAnomalyValues = graphData => {
+ const anomalySeriesCount = 3; // metric, upper, lower
+ return (
+ graphData.queries &&
+ graphData.queries.length === anomalySeriesCount &&
+ graphDataValidatorForValues(false, graphData)
+ );
+};
+
export default {};