mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Add and configure eslint-plugin-import * Fix the lint:ts npm command * Autofix + prettier all the files * Manually fix remaining files * Move jquery code in jest-setup to external file to safely reorder imports * Resolve issue caused by circular dependencies within Prometheus * Update .betterer.results * Fix missing // @ts-ignore * ignore iconBundle.ts * Fix missing // @ts-ignore
326 lines
10 KiB
TypeScript
326 lines
10 KiB
TypeScript
import { capitalize } from 'lodash';
|
|
import pluralize from 'pluralize';
|
|
|
|
import { SelectableValue } from '@grafana/data/src';
|
|
|
|
import { LabelParamEditor } from '../components/LabelParamEditor';
|
|
import { PromVisualQueryOperationCategory } from '../types';
|
|
|
|
import {
|
|
QueryBuilderOperation,
|
|
QueryBuilderOperationDef,
|
|
QueryBuilderOperationParamDef,
|
|
QueryBuilderOperationParamValue,
|
|
QueryWithOperations,
|
|
} from './types';
|
|
|
|
export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
const params = renderParams(model, def, innerExpr);
|
|
const str = model.id + '(';
|
|
|
|
if (innerExpr) {
|
|
params.push(innerExpr);
|
|
}
|
|
|
|
return str + params.join(', ') + ')';
|
|
}
|
|
|
|
export function functionRendererRight(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
const params = renderParams(model, def, innerExpr);
|
|
const str = model.id + '(';
|
|
|
|
if (innerExpr) {
|
|
params.unshift(innerExpr);
|
|
}
|
|
|
|
return str + params.join(', ') + ')';
|
|
}
|
|
|
|
function rangeRendererWithParams(
|
|
model: QueryBuilderOperation,
|
|
def: QueryBuilderOperationDef,
|
|
innerExpr: string,
|
|
renderLeft: boolean
|
|
) {
|
|
if (def.params.length < 2) {
|
|
throw `Cannot render a function with params of length [${def.params.length}]`;
|
|
}
|
|
|
|
let rangeVector = (model.params ?? [])[0] ?? '5m';
|
|
|
|
// Next frame the remaining parameters, but get rid of the first one because it's used to move the
|
|
// instant vector into a range vector.
|
|
const params = renderParams(
|
|
{
|
|
...model,
|
|
params: model.params.slice(1),
|
|
},
|
|
{
|
|
...def,
|
|
params: def.params.slice(1),
|
|
defaultParams: def.defaultParams.slice(1),
|
|
},
|
|
innerExpr
|
|
);
|
|
|
|
const str = model.id + '(';
|
|
|
|
// Depending on the renderLeft variable, render parameters to the left or right
|
|
// renderLeft === true (renderLeft) => (param1, param2, rangeVector[...])
|
|
// renderLeft === false (renderRight) => (rangeVector[...], param1, param2)
|
|
if (innerExpr) {
|
|
renderLeft ? params.push(`${innerExpr}[${rangeVector}]`) : params.unshift(`${innerExpr}[${rangeVector}]`);
|
|
}
|
|
|
|
// stick everything together
|
|
return str + params.join(', ') + ')';
|
|
}
|
|
|
|
export function rangeRendererRightWithParams(
|
|
model: QueryBuilderOperation,
|
|
def: QueryBuilderOperationDef,
|
|
innerExpr: string
|
|
) {
|
|
return rangeRendererWithParams(model, def, innerExpr, false);
|
|
}
|
|
|
|
export function rangeRendererLeftWithParams(
|
|
model: QueryBuilderOperation,
|
|
def: QueryBuilderOperationDef,
|
|
innerExpr: string
|
|
) {
|
|
return rangeRendererWithParams(model, def, innerExpr, true);
|
|
}
|
|
|
|
function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
return (model.params ?? []).map((value, index) => {
|
|
const paramDef = def.params[index];
|
|
if (paramDef.type === 'string') {
|
|
return '"' + value + '"';
|
|
}
|
|
|
|
return value;
|
|
});
|
|
}
|
|
|
|
export function defaultAddOperationHandler<T extends QueryWithOperations>(def: QueryBuilderOperationDef, query: T) {
|
|
const newOperation: QueryBuilderOperation = {
|
|
id: def.id,
|
|
params: def.defaultParams,
|
|
};
|
|
|
|
return {
|
|
...query,
|
|
operations: [...query.operations, newOperation],
|
|
};
|
|
}
|
|
|
|
export function getPromAndLokiOperationDisplayName(funcName: string) {
|
|
return capitalize(funcName.replace(/_/g, ' '));
|
|
}
|
|
|
|
export function getOperationParamId(operationIndex: number, paramIndex: number) {
|
|
return `operations.${operationIndex}.param.${paramIndex}`;
|
|
}
|
|
|
|
export function getRangeVectorParamDef(withRateInterval = false): QueryBuilderOperationParamDef {
|
|
const param: QueryBuilderOperationParamDef = {
|
|
name: 'Range',
|
|
type: 'string',
|
|
options: [
|
|
{
|
|
label: '$__interval',
|
|
value: '$__interval',
|
|
// tooltip: 'Dynamic interval based on max data points, scrape and min interval',
|
|
},
|
|
{ label: '1m', value: '1m' },
|
|
{ label: '5m', value: '5m' },
|
|
{ label: '10m', value: '10m' },
|
|
{ label: '1h', value: '1h' },
|
|
{ label: '24h', value: '24h' },
|
|
],
|
|
};
|
|
|
|
if (withRateInterval) {
|
|
(param.options as Array<SelectableValue<string>>).unshift({
|
|
label: '$__rate_interval',
|
|
value: '$__rate_interval',
|
|
// tooltip: 'Always above 4x scrape interval',
|
|
});
|
|
}
|
|
|
|
return param;
|
|
}
|
|
|
|
/**
|
|
* This function is shared between Prometheus and Loki variants
|
|
*/
|
|
export function createAggregationOperation<T extends QueryWithOperations>(
|
|
name: string,
|
|
overrides: Partial<QueryBuilderOperationDef> = {}
|
|
): QueryBuilderOperationDef[] {
|
|
const operations: QueryBuilderOperationDef[] = [
|
|
{
|
|
id: name,
|
|
name: getPromAndLokiOperationDisplayName(name),
|
|
params: [
|
|
{
|
|
name: 'By label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
},
|
|
],
|
|
defaultParams: [],
|
|
alternativesKey: 'plain aggregations',
|
|
category: PromVisualQueryOperationCategory.Aggregations,
|
|
renderer: functionRendererLeft,
|
|
paramChangedHandler: getOnLabelAddedHandler(`__${name}_by`),
|
|
explainHandler: getAggregationExplainer(name, ''),
|
|
addOperationHandler: defaultAddOperationHandler,
|
|
...overrides,
|
|
},
|
|
{
|
|
id: `__${name}_by`,
|
|
name: `${getPromAndLokiOperationDisplayName(name)} by`,
|
|
params: [
|
|
{
|
|
name: 'Label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
editor: LabelParamEditor,
|
|
},
|
|
],
|
|
defaultParams: [''],
|
|
alternativesKey: 'aggregations by',
|
|
category: PromVisualQueryOperationCategory.Aggregations,
|
|
renderer: getAggregationByRenderer(name),
|
|
paramChangedHandler: getLastLabelRemovedHandler(name),
|
|
explainHandler: getAggregationExplainer(name, 'by'),
|
|
addOperationHandler: defaultAddOperationHandler,
|
|
hideFromList: true,
|
|
...overrides,
|
|
},
|
|
{
|
|
id: `__${name}_without`,
|
|
name: `${getPromAndLokiOperationDisplayName(name)} without`,
|
|
params: [
|
|
{
|
|
name: 'Label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
editor: LabelParamEditor,
|
|
},
|
|
],
|
|
defaultParams: [''],
|
|
alternativesKey: 'aggregations by',
|
|
category: PromVisualQueryOperationCategory.Aggregations,
|
|
renderer: getAggregationWithoutRenderer(name),
|
|
paramChangedHandler: getLastLabelRemovedHandler(name),
|
|
explainHandler: getAggregationExplainer(name, 'without'),
|
|
addOperationHandler: defaultAddOperationHandler,
|
|
hideFromList: true,
|
|
...overrides,
|
|
},
|
|
];
|
|
|
|
return operations;
|
|
}
|
|
|
|
export function createAggregationOperationWithParam(
|
|
name: string,
|
|
paramsDef: { params: QueryBuilderOperationParamDef[]; defaultParams: QueryBuilderOperationParamValue[] },
|
|
overrides: Partial<QueryBuilderOperationDef> = {}
|
|
): QueryBuilderOperationDef[] {
|
|
const operations = createAggregationOperation(name, overrides);
|
|
operations[0].params.unshift(...paramsDef.params);
|
|
operations[1].params.unshift(...paramsDef.params);
|
|
operations[2].params.unshift(...paramsDef.params);
|
|
operations[0].defaultParams = paramsDef.defaultParams;
|
|
operations[1].defaultParams = [...paramsDef.defaultParams, ''];
|
|
operations[2].defaultParams = [...paramsDef.defaultParams, ''];
|
|
operations[1].renderer = getAggregationByRendererWithParameter(name);
|
|
operations[2].renderer = getAggregationByRendererWithParameter(name);
|
|
return operations;
|
|
}
|
|
|
|
function getAggregationByRenderer(aggregation: string) {
|
|
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`;
|
|
};
|
|
}
|
|
|
|
function getAggregationWithoutRenderer(aggregation: string) {
|
|
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
return `${aggregation} without(${model.params.join(', ')}) (${innerExpr})`;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Very simple poc implementation, needs to be modified to support all aggregation operators
|
|
*/
|
|
function getAggregationExplainer(aggregationName: string, mode: 'by' | 'without' | '') {
|
|
return function aggregationExplainer(model: QueryBuilderOperation) {
|
|
const labels = model.params.map((label) => `\`${label}\``).join(' and ');
|
|
const labelWord = pluralize('label', model.params.length);
|
|
|
|
switch (mode) {
|
|
case 'by':
|
|
return `Calculates ${aggregationName} over dimensions while preserving ${labelWord} ${labels}.`;
|
|
case 'without':
|
|
return `Calculates ${aggregationName} over the dimensions ${labels}. All other labels are preserved.`;
|
|
default:
|
|
return `Calculates ${aggregationName} over the dimensions.`;
|
|
}
|
|
};
|
|
}
|
|
|
|
function getAggregationByRendererWithParameter(aggregation: string) {
|
|
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
function mapType(p: QueryBuilderOperationParamValue) {
|
|
if (typeof p === 'string') {
|
|
return `\"${p}\"`;
|
|
}
|
|
return p;
|
|
}
|
|
const params = model.params.slice(0, -1);
|
|
const restParams = model.params.slice(1);
|
|
return `${aggregation} by(${restParams.join(', ')}) (${params.map(mapType).join(', ')}, ${innerExpr})`;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This function will transform operations without labels to their plan aggregation operation
|
|
*/
|
|
function getLastLabelRemovedHandler(changeToOperationId: string) {
|
|
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
|
|
// If definition has more params then is defined there are no optional rest params anymore.
|
|
// We then transform this operation into a different one
|
|
if (op.params.length < def.params.length) {
|
|
return {
|
|
...op,
|
|
id: changeToOperationId,
|
|
};
|
|
}
|
|
|
|
return op;
|
|
};
|
|
}
|
|
|
|
function getOnLabelAddedHandler(changeToOperationId: string) {
|
|
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
|
|
// Check if we actually have the label param. As it's optional the aggregation can have one less, which is the
|
|
// case of just simple aggregation without label. When user adds the label it now has the same number of params
|
|
// as it's definition, and now we can change it to it's `_by` variant.
|
|
if (op.params.length === def.params.length) {
|
|
return {
|
|
...op,
|
|
id: changeToOperationId,
|
|
};
|
|
}
|
|
return op;
|
|
};
|
|
}
|