Commit 58c69cb3 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent dcd9852800af
......@@ -13,7 +13,7 @@
- .default-before_script
- .assets-compile-cache
- .only:changes-code-backstage-qa
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-19.03.1
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
......
......@@ -312,8 +312,7 @@ gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader', '~> 1.4.0'
# Perf bar
# https://gitlab.com/gitlab-org/gitlab/issues/13996
gem 'gitlab-peek', '~> 0.0.1', require: 'peek'
gem 'peek', '~> 1.1'
# Snowplow events tracking
gem 'snowplow-tracker', '~> 0.6.1'
......@@ -381,7 +380,7 @@ group :development, :test do
gem 'knapsack', '~> 1.17'
gem 'stackprof', '~> 0.2.10', require: false
gem 'stackprof', '~> 0.2.13', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
......
......@@ -373,8 +373,6 @@ GEM
gitlab-license (1.0.0)
gitlab-markup (1.7.0)
gitlab-net-dns (0.9.1)
gitlab-peek (0.0.1)
railties (>= 4.0.0)
gitlab-sidekiq-fetcher (0.5.2)
sidekiq (~> 5)
gitlab-styles (2.8.0)
......@@ -724,6 +722,8 @@ GEM
parser (2.6.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.1.0)
railties (>= 4.0.0)
pg (1.1.4)
po_to_json (1.0.1)
json (>= 1.6.0)
......@@ -1003,7 +1003,7 @@ GEM
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sshkey (2.0.0)
stackprof (0.2.10)
stackprof (0.2.13)
state_machines (0.5.0)
state_machines-activemodel (0.7.1)
activemodel (>= 4.1)
......@@ -1201,7 +1201,6 @@ DEPENDENCIES
gitlab-license (~> 1.0)
gitlab-markup (~> 1.7.0)
gitlab-net-dns (~> 0.9.1)
gitlab-peek (~> 0.0.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 2.7)
gitlab_chronic_duration (~> 0.10.6.2)
......@@ -1276,6 +1275,7 @@ DEPENDENCIES
omniauth_crowd (~> 2.2.0)
omniauth_openid_connect (~> 0.3.3)
org-ruby (~> 0.9.12)
peek (~> 1.1)
pg (~> 1.1)
premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.9.10)
......@@ -1340,7 +1340,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0)
sshkey (~> 2.0)
stackprof (~> 0.2.10)
stackprof (~> 0.2.13)
state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.10.0)
......
......@@ -29,7 +29,7 @@ const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})
* time series chart, the boundary band shows the normal
* range of values the metric should take.
*
* This component accepts 3 queries, which contain the
* This component accepts 3 metrics, which contain the
* "metric", "upper" limit and "lower" limit.
*
* The upper and lower series are "stacked areas" visually
......@@ -62,10 +62,10 @@ export default {
},
computed: {
series() {
return this.graphData.queries.map(query => {
const values = query.result[0] ? query.result[0].values : [];
return this.graphData.metrics.map(metric => {
const values = metric.result && metric.result[0] ? metric.result[0].values : [];
return {
label: query.label,
label: metric.label,
// NaN values may disrupt avg., max. & min. calculations in the legend, filter them out
data: values.filter(([, value]) => !Number.isNaN(value)),
};
......@@ -83,7 +83,7 @@ export default {
return min < 0 ? -min : 0;
},
metricData() {
const originalMetricQuery = this.graphData.queries[0];
const originalMetricQuery = this.graphData.metrics[0];
const metricQuery = { ...originalMetricQuery };
metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
......@@ -93,7 +93,7 @@ export default {
return {
...this.graphData,
type: 'line-chart',
queries: [metricQuery],
metrics: [metricQuery],
};
},
metricSeriesConfig() {
......
......@@ -32,8 +32,8 @@ export default {
},
computed: {
chartData() {
const queryData = this.graphData.queries.reduce((acc, query) => {
const series = makeDataSeries(query.result, {
const queryData = this.graphData.metrics.reduce((acc, query) => {
const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
});
......@@ -45,13 +45,13 @@ export default {
};
},
xAxisTitle() {
return this.graphData.queries[0].result[0].x_label !== undefined
? this.graphData.queries[0].result[0].x_label
return this.graphData.metrics[0].result[0].x_label !== undefined
? this.graphData.metrics[0].result[0].x_label
: '';
},
yAxisTitle() {
return this.graphData.queries[0].result[0].y_label !== undefined
? this.graphData.queries[0].result[0].y_label
return this.graphData.metrics[0].result[0].y_label !== undefined
? this.graphData.metrics[0].result[0].y_label
: '';
},
xAxisType() {
......
......@@ -24,7 +24,7 @@ export default {
},
computed: {
chartData() {
return this.queries.result.reduce(
return this.metrics.result.reduce(
(acc, result, i) => [...acc, ...result.values.map((value, j) => [i, j, value[1]])],
[],
);
......@@ -36,7 +36,7 @@ export default {
return this.graphData.y_label || '';
},
xAxisLabels() {
return this.queries.result.map(res => Object.values(res.metric)[0]);
return this.metrics.result.map(res => Object.values(res.metric)[0]);
},
yAxisLabels() {
return this.result.values.map(val => {
......@@ -46,10 +46,10 @@ export default {
});
},
result() {
return this.queries.result[0];
return this.metrics.result[0];
},
queries() {
return this.graphData.queries[0];
metrics() {
return this.graphData.metrics[0];
},
},
};
......
......@@ -17,7 +17,7 @@ export default {
},
computed: {
queryInfo() {
return this.graphData.queries[0];
return this.graphData.metrics[0];
},
engineeringNotation() {
return `${roundOffFloat(this.queryInfo.result[0].value[1], 1)}${this.queryInfo.unit}`;
......
......@@ -105,7 +105,7 @@ export default {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
// Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
return this.graphData.queries.reduce((acc, query) => {
return this.graphData.metrics.reduce((acc, query) => {
const { appearance } = query;
const lineType =
appearance && appearance.line && appearance.line.type
......@@ -121,7 +121,7 @@ export default {
? appearance.area.opacity
: undefined,
};
const series = makeDataSeries(query.result, {
const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
lineStyle: {
type: lineType,
......
......@@ -250,14 +250,9 @@ export default {
'setEndpoints',
'setPanelGroupMetrics',
]),
chartsWithData(charts) {
return charts.filter(chart =>
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
updateMetrics(key, metrics) {
updateMetrics(key, panels) {
this.setPanelGroupMetrics({
metrics,
panels,
key,
});
},
......@@ -292,8 +287,13 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
chartsWithData(panels) {
return panels.filter(panel =>
panel.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
groupHasData(group) {
return this.chartsWithData(group.metrics).length > 0;
return this.chartsWithData(group.panels).length > 0;
},
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
......@@ -453,14 +453,14 @@ export default {
:collapse-group="groupHasData(groupData)"
>
<vue-draggable
:value="groupData.metrics"
:value="groupData.panels"
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"
v-for="(graphData, graphIndex) in groupData.panels"
:key="`panel-type-${graphIndex}`"
class="col-12 col-lg-6 px-2 mb-2 draggable"
:class="{ 'draggable-enabled': isRearrangingPanels }"
......@@ -469,7 +469,7 @@ export default {
<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)"
@click="removeGraph(groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
......
......@@ -37,11 +37,14 @@ export default {
computed: {
...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']),
charts() {
if (!this.dashboard || !this.dashboard.panel_groups) {
return [];
}
const groupWithMetrics = this.dashboard.panel_groups.find(group =>
group.metrics.find(chart => this.chartHasData(chart)),
) || { metrics: [] };
group.panels.find(chart => this.chartHasData(chart)),
) || { panels: [] };
return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart));
return groupWithMetrics.panels.filter(chart => this.chartHasData(chart));
},
isSingleChart() {
return this.charts.length === 1;
......
......@@ -54,10 +54,14 @@ export default {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
graphDataHasMetrics() {
return this.graphData.queries[0].result.length > 0;
return (
this.graphData.metrics &&
this.graphData.metrics[0].result &&
this.graphData.metrics[0].result.length > 0
);
},
csvText() {
const chartData = this.graphData.queries[0].result[0].values;
const chartData = this.graphData.metrics[0].result[0].values;
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
return chartData.reduce((csv, data) => {
......@@ -112,7 +116,7 @@ export default {
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.queries)"
:thresholds="getGraphAlertValues(graphData.metrics)"
group-id="panel-type-chart"
>
<div class="d-flex align-items-center">
......@@ -120,8 +124,8 @@ export default {
v-if="alertWidgetAvailable && graphData"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<gl-dropdown
......
import Vue from 'vue';
import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetrics, normalizeMetric, normalizeQueryResult } from './utils';
import { normalizeMetric, normalizeQueryResult } from './utils';
const normalizePanel = panel => panel.metrics.map(normalizeMetric);
const normalizePanelMetrics = (metrics, defaultLabel) =>
metrics.map(metric => ({
...normalizeMetric(metric),
label: metric.label || defaultLabel,
}));
export default {
[types.REQUEST_METRICS_DATA](state) {
......@@ -13,28 +17,18 @@ export default {
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
state.dashboard.panel_groups = groupData.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
let { metrics = [], panels = [] } = group;
let { panels = [] } = group;
// each panel has metric information that needs to be normalized
panels = panels.map(panel => ({
...panel,
metrics: normalizePanel(panel),
}));
// for backwards compatibility, and to limit Vue template changes:
// for each group alias panels to metrics
// for each panel alias metrics to queries
metrics = panels.map(panel => ({
...panel,
queries: panel.metrics,
metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
}));
return {
...group,
panels,
key,
metrics: normalizeMetrics(metrics),
};
});
......@@ -58,6 +52,7 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
state.environments = [];
},
[types.SET_QUERY_RESULT](state, { metricId, result }) {
if (!metricId || !result || result.length === 0) {
return;
......@@ -65,14 +60,17 @@ export default {
state.showEmptyState = false;
/**
* Search the dashboard state for a matching id
*/
state.dashboard.panel_groups.forEach(group => {
group.metrics.forEach(metric => {
metric.queries.forEach(query => {
if (query.metric_id === metricId) {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
if (metric.metric_id === metricId) {
state.metricsWithData.push(metricId);
// ensure dates/numbers are correctly formatted for charts
const normalizedResults = result.map(normalizeQueryResult);
Vue.set(query, 'result', Object.freeze(normalizedResults));
Vue.set(metric, 'result', Object.freeze(normalizedResults));
}
});
});
......@@ -101,6 +99,6 @@ export default {
},
[types.SET_PANEL_GROUP_METRICS](state, payload) {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
panelGroup.metrics = payload.metrics;
panelGroup.panels = payload.panels;
},
};
import _ from 'underscore';
function checkQueryEmptyData(query) {
return {
...query,
result: query.result.filter(timeSeries => {
const newTimeSeries = timeSeries;
const hasValue = series =>
!Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined);
const hasNonNullValue = timeSeries.values.find(hasValue);
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
return newTimeSeries.values.length > 0;
}),
};
}
function removeTimeSeriesNoData(queries) {
return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
}
// Metrics and queries are currently stored 1:1, so `queries` is an array of length one.
// We want to group queries onto a single chart by title & y-axis label.
// This function will no longer be required when metrics:queries are 1:many,
// though there is no consequence if the function stays in use.
// @param metrics [Array<Object>]
// Ex) [
// { id: 1, title: 'title', y_label: 'MB', queries: [{ ...query1Attrs }] },
// { id: 2, title: 'title', y_label: 'MB', queries: [{ ...query2Attrs }] },
// { id: 3, title: 'new title', y_label: 'MB', queries: [{ ...query3Attrs }] }
// ]
// @return [Array<Object>]
// Ex) [
// { title: 'title', y_label: 'MB', queries: [{ metricId: 1, ...query1Attrs },
// { metricId: 2, ...query2Attrs }] },
// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]}
// ]
export function groupQueriesByChartInfo(metrics) {
const metricsByChart = metrics.reduce((accumulator, metric) => {
const { queries, ...chart } = metric;
const chartKey = `${chart.title}|${chart.y_label}`;
accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
queries.forEach(queryAttrs => {
let metricId;
if (chart.id) {
metricId = chart.id.toString();
} else if (queryAttrs.metric_id) {
metricId = queryAttrs.metric_id.toString();
} else {
metricId = null;
}
accumulator[chartKey].queries.push({ metricId, ...queryAttrs });
});
return accumulator;
}, {});
return Object.values(metricsByChart);
}
export const uniqMetricsId = metric => `${metric.metric_id}_${metric.id}`;
/**
* Not to confuse with normalizeMetrics (plural)
* Metrics loaded from project-defined dashboards do not have a metric_id.
* This method creates a unique ID combining metric_id and id, if either is present.
* This is hopefully a temporary solution until BE processes metrics before passing to fE
* @param {Object} metric - metric
* @returns {Object} - normalized metric with a uniqueID
*/
export const normalizeMetric = (metric = {}) =>
_.omit(
{
...metric,
metric_id: uniqMetricsId(metric),
metricId: uniqMetricsId(metric),
},
'id',
);
......@@ -93,6 +31,11 @@ export const normalizeQueryResult = timeSeries => {
Number(value),
]),
};
// Check result for empty data
normalizedResult.values = normalizedResult.values.filter(series => {
const hasValue = d => !Number.isNaN(d[1]) && (d[1] !== null || d[1] !== undefined);
return series.find(hasValue);
});
} else if (timeSeries.value) {
normalizedResult = {
...timeSeries,
......@@ -102,21 +45,3 @@ export const normalizeQueryResult = timeSeries => {
return normalizedResult;
};
export const normalizeMetrics = metrics => {
const groupedMetrics = groupQueriesByChartInfo(metrics);
return groupedMetrics.map(metric => {
const queries = metric.queries.map(query => ({
...query,
// custom metrics do not require a label, so we should ensure this attribute is defined
label: query.label || metric.y_label,
result: (query.result || []).map(normalizeQueryResult),
}));
return {
...metric,
queries: removeTimeSeriesNoData(queries),
};
});
};
......@@ -72,10 +72,9 @@ export const ISODateToString = date => dateformat(date, dateFormats.dateTimePick
*/
export const graphDataValidatorForValues = (isValues, graphData) => {
const responseValueKeyName = isValues ? 'value' : 'values';
return (
Array.isArray(graphData.queries) &&
graphData.queries.filter(query => {
Array.isArray(graphData.metrics) &&
graphData.metrics.filter(query => {
if (Array.isArray(query.result)) {
return (
query.result.filter(res => Array.isArray(res[responseValueKeyName])).length ===
......@@ -83,7 +82,7 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
);
}
return false;
}).length === graphData.queries.length
}).length === graphData.metrics.filter(query => query.result).length
);
};
......@@ -131,7 +130,7 @@ export const downloadCSVOptions = title => {
};
/**
* This function validates the graph data contains exactly 3 queries plus
* This function validates the graph data contains exactly 3 metrics plus
* value validations from graphDataValidatorForValues.
* @param {Object} isValues
* @param {Object} graphData the graph data response from a prometheus request
......@@ -140,8 +139,8 @@ export const downloadCSVOptions = title => {
export const graphDataValidatorForAnomalyValues = graphData => {
const anomalySeriesCount = 3; // metric, upper, lower
return (
graphData.queries &&
graphData.queries.length === anomalySeriesCount &&
graphData.metrics &&
graphData.metrics.length === anomalySeriesCount &&
graphDataValidatorForValues(false, graphData)
);
};
......
......@@ -40,15 +40,14 @@
# Any other value will be ignored.
class SnippetsFinder < UnionFinder
include FinderMethods
include Gitlab::Utils::StrongMemoize
attr_accessor :current_user, :project, :author, :scope, :explore
attr_accessor :current_user, :params
delegate :explore, :only_personal, :only_project, :scope, to: :params
def initialize(current_user = nil, params = {})
@current_user = current_user
@project = params[:project]
@author = params[:author]
@scope = params[:scope].to_s
@explore = params[:explore]
@params = OpenStruct.new(params)
if project && author
raise(
......@@ -60,8 +59,15 @@ def initialize(current_user = nil, params = {})
end
def execute
base = init_collection
base.with_optional_visibility(visibility_from_scope).fresh
# The snippet query can be expensive, therefore if the