402 lines
12 KiB
JavaScript
402 lines
12 KiB
JavaScript
import { pickBy, mapKeys } from 'lodash';
|
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
|
import {
|
|
timeRangeParamNames,
|
|
timeRangeFromParams,
|
|
timeRangeToParams,
|
|
} from '~/lib/utils/datetime_range';
|
|
import {
|
|
queryToObject,
|
|
mergeUrlParams,
|
|
removeParams,
|
|
updateHistory,
|
|
} from '~/lib/utils/url_utility';
|
|
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,
|
|
externalDashboardUrl,
|
|
currentEnvironmentName,
|
|
customDashboardBasePath,
|
|
addDashboardDocumentationPath,
|
|
...dataProps
|
|
} = dataset;
|
|
|
|
// HTML attributes are always strings, parse other types.
|
|
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
|
|
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
|
|
|
|
return {
|
|
initState: {
|
|
currentDashboard,
|
|
deploymentsEndpoint,
|
|
dashboardEndpoint,
|
|
dashboardsEndpoint,
|
|
panelPreviewEndpoint,
|
|
dashboardTimezone,
|
|
canAccessOperationsSettings,
|
|
operationsSettingsPath,
|
|
projectPath,
|
|
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');
|
|
|
|
/**
|
|
* 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' // eslint-disable-line @gitlab/require-i18n-strings
|
|
: '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 }; // eslint-disable-line @gitlab/require-i18n-strings
|
|
};
|
|
|
|
/**
|
|
* 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' // eslint-disable-line @gitlab/require-i18n-strings
|
|
: '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-disable-line @gitlab/require-i18n-strings
|
|
};
|
|
/* 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, { legacySpacesDecode: true });
|
|
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, { legacySpacesDecode: true });
|
|
// 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, { legacySpacesDecode: true });
|
|
|
|
// 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 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),
|
|
}),
|
|
{},
|
|
);
|