summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring/utils.js
blob: 01cae7127e5a52ce3792681173c59bb9c3d492a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
import { pickBy, mapKeys } from 'lodash';
import {
  queryToObject,
  mergeUrlParams,
  removeParams,
  updateHistory,
} from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import {
  timeRangeParamNames,
  timeRangeFromParams,
  timeRangeToParams,
} from '~/lib/utils/datetime_range';
import { VARIABLE_PREFIX } from './constants';

/**
 * Extracts the initial state and props from HTML dataset
 * and places them in separate objects to setup bundle.
 * @param {*} dataset
 */
export const stateAndPropsFromDataset = (dataset = {}) => {
  const {
    currentDashboard,
    deploymentsEndpoint,
    dashboardEndpoint,
    dashboardsEndpoint,
    panelPreviewEndpoint,
    dashboardTimezone,
    canAccessOperationsSettings,
    operationsSettingsPath,
    projectPath,
    logsPath,
    externalDashboardUrl,
    currentEnvironmentName,
    customDashboardBasePath,
    addDashboardDocumentationPath,
    ...dataProps
  } = dataset;

  // HTML attributes are always strings, parse other types.
  dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
  dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
  dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);

  return {
    initState: {
      currentDashboard,
      deploymentsEndpoint,
      dashboardEndpoint,
      dashboardsEndpoint,
      panelPreviewEndpoint,
      dashboardTimezone,
      canAccessOperationsSettings,
      operationsSettingsPath,
      projectPath,
      logsPath,
      externalDashboardUrl,
      currentEnvironmentName,
      customDashboardBasePath,
      addDashboardDocumentationPath,
    },
    dataProps,
  };
};

/**
 * List of non time range url parameters
 * This will be removed once we add support for free text variables
 * via the dashboard yaml files in https://gitlab.com/gitlab-org/gitlab/-/issues/215689
 */
export const dashboardParams = ['dashboard', 'group', 'title', 'y_label', 'embedded'];

/**
 * 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 (queryRange) 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.metrics) &&
    graphData.metrics.filter((query) => {
      if (Array.isArray(query.result)) {
        return (
          query.result.filter((res) => Array.isArray(res[responseValueKeyName])).length ===
          query.result.length
        );
      }
      return false;
    }).length === graphData.metrics.filter((query) => query.result).length
  );
};

/**
 * Checks that element that triggered event is located on cluster health check dashboard
 * @param {HTMLElement}  element to check against
 * @returns {boolean}
 */
const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(':clusters:show');

/* eslint-disable @gitlab/require-i18n-strings */
/**
 * Tracks snowplow event when user generates link to metric chart
 * @param {String}  chart link that will be sent as a property for the event
 * @return {Object} config object for event tracking
 */
export const generateLinkToChartOptions = (chartLink) => {
  const isCLusterHealthBoard = isClusterHealthBoard();

  const category = isCLusterHealthBoard
    ? 'Cluster Monitoring'
    : 'Incident Management::Embedded metrics';
  const action = isCLusterHealthBoard
    ? 'generate_link_to_cluster_metric_chart'
    : 'generate_link_to_metrics_chart';

  return { category, action, label: 'Chart link', property: chartLink };
};

/**
 * Tracks snowplow event when user downloads CSV of cluster metric
 * @param {String}  chart title that will be sent as a property for the event
 * @return {Object} config object for event tracking
 */
export const downloadCSVOptions = (title) => {
  const isCLusterHealthBoard = isClusterHealthBoard();

  const category = isCLusterHealthBoard
    ? 'Cluster Monitoring'
    : 'Incident Management::Embedded metrics';
  const action = isCLusterHealthBoard
    ? 'download_csv_of_cluster_metric_chart'
    : 'download_csv_of_metrics_dashboard_chart';

  return { category, action, label: 'Chart title', property: title };
};
/* eslint-enable @gitlab/require-i18n-strings */

/**
 * Generate options for snowplow to track adding a new metric via the dashboard
 * custom metric modal
 * @return {Object} config object for event tracking
 */
export const getAddMetricTrackingOptions = () => ({
  category: document.body.dataset.page,
  action: 'click_button',
  label: 'add_new_metric',
  property: 'modal',
});

/**
 * 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
 * @returns {boolean} true if the data is valid
 */
export const graphDataValidatorForAnomalyValues = (graphData) => {
  const anomalySeriesCount = 3; // metric, upper, lower
  return (
    graphData.metrics &&
    graphData.metrics.length === anomalySeriesCount &&
    graphDataValidatorForValues(false, graphData)
  );
};

/**
 * Returns a time range from the current URL params
 *
 * @returns {Object|null} The time range defined by the
 * current URL, reading from search query or `window.location.search`.
 * Returns `null` if no parameters form a time range.
 */
export const timeRangeFromUrl = (search = window.location.search) => {
  const params = queryToObject(search);
  return timeRangeFromParams(params);
};

/**
 * Variable labels are used as names for the dropdowns and also
 * as URL params. Prefixing the name reduces the risk of
 * collision with other URL params
 *
 * @param {String} label label for the template variable
 * @returns {String}
 */
export const addPrefixToLabel = (label) => `${VARIABLE_PREFIX}${label}`;

/**
 * Before the templating variables are passed to the backend the
 * prefix needs to be removed.
 *
 * This method removes the prefix at the beginning of the string.
 *
 * @param {String} label label to remove prefix from
 * @returns {String}
 */
export const removePrefixFromLabel = (label) =>
  (label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), '');

/**
 * Convert parsed template variables to an object
 * with just keys and values. Prepare the variables
 * to be added to the URL. Keys of the object will
 * have a prefix so that these params can be
 * differentiated from other URL params.
 *
 * @param {Object} variables
 * @returns {Object}
 */
export const convertVariablesForURL = (variables) =>
  variables.reduce((acc, { name, value }) => {
    if (value !== null) {
      acc[addPrefixToLabel(name)] = value;
    }
    return acc;
  }, {});

/**
 * User-defined variables from the URL are extracted. The variables
 * begin with a constant prefix so that it doesn't collide with
 * other URL params.
 *
 * @param {String} search URL
 * @returns {Object} The custom variables defined by the user in the URL
 */
export const templatingVariablesFromUrl = (search = window.location.search) => {
  const params = queryToObject(search);
  // pick the params with variable prefix
  const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX));
  // remove the prefix before storing in the Vuex store
  return mapKeys(paramsWithVars, (val, key) => removePrefixFromLabel(key));
};

/**
 * Update the URL with variables. This usually get triggered when
 * the user interacts with the dynamic input elements in the monitoring
 * dashboard header.
 *
 * @param {Object} variables user defined variables
 */
export const setCustomVariablesFromUrl = (variables) => {
  // prep the variables to append to URL
  const parsedVariables = convertVariablesForURL(variables);
  // update the URL
  updateHistory({
    url: mergeUrlParams(parsedVariables, window.location.href),
    title: document.title,
  });
};

/**
 * Returns a URL with no time range based on the current URL.
 *
 * @param {String} New URL
 */
export const removeTimeRangeParams = (url = window.location.href) =>
  removeParams(timeRangeParamNames, url);

/**
 * Returns a URL for the a different time range based on the
 * current URL and a time range.
 *
 * @param {String} New URL
 */
export const timeRangeToUrl = (timeRange, url = window.location.href) => {
  const toUrl = removeTimeRangeParams(url);
  const params = timeRangeToParams(timeRange);
  return mergeUrlParams(params, toUrl);
};

/**
 * Locates a panel (and its corresponding group) given a (URL) search query. Returns
 * it as payload for the store to set the right expandaded panel.
 *
 * Params used to locate a panel are:
 * - group: Group identifier
 * - title: Panel title
 * - y_label: Panel y_label
 *
 * @param {Object} dashboard - Dashboard reference from the Vuex store
 * @param {String} search - URL location search query
 * @returns {Object} payload - Payload for expanded panel to be displayed
 * @returns {String} payload.group - Group where panel is located
 * @returns {Object} payload.panel - Dashboard panel (graphData) reference
 * @throws Will throw an error if Panel cannot be located.
 */
export const expandedPanelPayloadFromUrl = (dashboard, search = window.location.search) => {
  const params = queryToObject(search);

  // Search for the panel if any of the search params is identified
  if (params.group || params.title || params.y_label) {
    const panelGroup = dashboard.panelGroups.find(({ group }) => params.group === group);
    const panel = panelGroup.panels.find(
      // eslint-disable-next-line babel/camelcase
      ({ y_label, title }) => y_label === params.y_label && title === params.title,
    );

    if (!panel) {
      // eslint-disable-next-line @gitlab/require-i18n-strings
      throw new Error('Panel could no found by URL parameters.');
    }
    return { group: panelGroup.group, panel };
  }
  return null;
};

/**
 * Convert panel information to a URL for the user to
 * bookmark or share highlighting a specific panel.
 *
 * If no group/panel is set, the dashboard URL is returned.
 *
 * @param {?String} dashboard - Dashboard path, used as identifier for a dashboard
 * @param {?Object} variables - Custom variables that came from the URL
 * @param {?String} group - Group Identifier
 * @param {?Object} panel - Panel object from the dashboard
 * @param {?String} url - Base URL including current search params
 * @returns Dashboard URL which expands a panel (chart)
 */
export const panelToUrl = (
  dashboard = null,
  variables,
  group,
  panel,
  url = window.location.href,
) => {
  const params = {
    dashboard,
    ...variables,
  };

  if (group && panel) {
    params.group = group;
    params.title = panel.title;
    params.y_label = panel.y_label;
  } else {
    // Remove existing parameters if any
    params.group = null;
    params.title = null;
    params.y_label = null;
  }

  return mergeUrlParams(params, url);
};

/**
 * Get the metric value from first data point.
 * Currently only used for bar charts
 *
 * @param {Array} values data points
 * @returns {Number}
 */
const metricValueMapper = (values) => values[0]?.[1];

/**
 * Get the metric name from metric object
 * Currently only used for bar charts
 * e.g. { handler: '/query' }
 * { method: 'get' }
 *
 * @param {Object} metric metric object
 * @returns {String}
 */
const metricNameMapper = (metric) => Object.values(metric)?.[0];

/**
 * Parse metric object to extract metric value and name in
 * [<metric-value>, <metric-name>] format.
 * Currently only used for bar charts
 *
 * @param {Object} param0 metric object
 * @returns {Array}
 */
const resultMapper = ({ metric, values = [] }) => [
  metricValueMapper(values),
  metricNameMapper(metric),
];

/**
 * Bar charts graph data parser to massage data from
 * backend to a format acceptable by bar charts component
 * in GitLab UI
 *
 * e.g.
 * {
 *   SLO: [
 *      [98, 'api'],
 *      [99, 'web'],
 *      [99, 'database']
 *   ]
 * }
 *
 * @param {Array} data series information
 * @returns {Object}
 */
export const barChartsDataParser = (data = []) =>
  data?.reduce(
    (acc, { result = [], label }) => ({
      ...acc,
      [label]: result.map(resultMapper),
    }),
    {},
  );