Files
grafana/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/utils.ts
Erik Sundell bab78a9e64 CloudWatch: Add support for AWS Metric Insights (#42487)
* add support for code editor and builder

* refactor cloudwatch migration

* Add tooltip to editor field (#56)

* add tooltip

* add old tooltips

* Bug bash feedback fixes (#58)

* make ASC the default option

* update sql preview whenever sql changes

* don't allow queries without aggregation

* set default value for aggregation

* use new input field

* cleanup

* pr feedback

* prevent unnecessary rerenders

* use frame error instead of main error

* remove not used snapshot

* Use dimension filter in schema picker  (#63)

* use dimension key filter in group by and schema labels

* add dimension filter also to code editor

* add tests

* fix build error

* fix strict error

* remove debug code

* fix annotation editor (#64)

* fix annotation editor

* fix broken test

* revert annotation backend change

* PR feedback (#67)

* pr feedback

* removed dimension filter from group by

* add spacing between common fields and rest

* do not generate deep link for metric queries (#70)

* update docs (#69)

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>

* fix lint problem caused by merge conflict

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
2021-11-30 10:53:31 +01:00

347 lines
10 KiB
TypeScript

import { SelectableValue } from './../../../../../../../packages/grafana-data/src/types/select';
import { SCHEMA } from '../../cloudwatch-sql/language';
import {
QueryEditorExpressionType,
QueryEditorPropertyType,
QueryEditorFunctionParameterExpression,
QueryEditorArrayExpression,
QueryEditorOperatorExpression,
QueryEditorGroupByExpression,
} from '../../expressions';
import { SQLExpression, CloudWatchMetricsQuery, Dimensions } from '../../types';
export function getMetricNameFromExpression(selectExpression: SQLExpression['select']): string | undefined {
return selectExpression?.parameters?.[0].name;
}
export function getNamespaceFromExpression(fromExpression: SQLExpression['from']): string | undefined {
// It's just a simple `FROM "AWS/EC2"` expression
if (fromExpression?.type === QueryEditorExpressionType.Property) {
return fromExpression.property.name; // PR TODO: do we need to test the type here? It can only be string?
}
// It's a more complicated `FROM SCHEMA("AWS/EC2", ...)` expression
if (fromExpression?.type === QueryEditorExpressionType.Function) {
// TODO: do we need to test the name of the function?
return fromExpression.parameters?.[0].name;
}
return undefined;
}
export function getSchemaLabelKeys(fromExpression: SQLExpression['from']): string[] | undefined {
// Schema label keys are second to n arguments in the from expression function
if (fromExpression?.type === QueryEditorExpressionType.Function && fromExpression?.parameters?.length) {
if (fromExpression?.parameters?.length <= 1) {
return [];
}
// ignore the first arg (the namespace)
const paramExpressions = fromExpression?.parameters.slice(1);
return paramExpressions.reduce<string[]>((acc, curr) => (curr.name ? [...acc, curr.name] : acc), []);
}
return undefined;
}
export function isUsingWithSchema(fromExpression: SQLExpression['from']): boolean {
return fromExpression?.type === QueryEditorExpressionType.Function && fromExpression.name === SCHEMA;
}
/** Given a partial operator expression, return a non-partial if it's valid, or undefined */
export function sanitizeOperator(
expression: Partial<QueryEditorOperatorExpression>
): QueryEditorOperatorExpression | undefined {
const key = expression.property?.name;
const value = expression.operator?.value;
const operator = expression.operator?.name;
if (key && value && operator) {
return {
type: QueryEditorExpressionType.Operator,
property: {
type: QueryEditorPropertyType.String,
name: key,
},
operator: {
value,
name: operator,
},
};
}
return undefined;
}
/**
* Given an array of Expressions, flattens them to the leaf Operator expressions.
* Note, this loses context of any nested ANDs or ORs, so will not be useful once we support nested conditions */
function flattenOperatorExpressions(
expressions: QueryEditorArrayExpression['expressions']
): QueryEditorOperatorExpression[] {
return expressions.flatMap((expression) => {
if (expression.type === QueryEditorExpressionType.Operator) {
return expression;
}
if (expression.type === QueryEditorExpressionType.And || expression.type === QueryEditorExpressionType.Or) {
return flattenOperatorExpressions(expression.expressions);
}
// Expressions that we don't expect to find in the WHERE filter will be ignored
return [];
});
}
/** Returns a flattened list of WHERE filters, losing all context of nested filters or AND vs OR. Not suitable
* if the UI supports nested conditions
*/
export function getFlattenedFilters(sql: SQLExpression): QueryEditorOperatorExpression[] {
const where = sql.where;
return flattenOperatorExpressions(where?.expressions ?? []);
}
/**
* Given an array of Expressions, flattens them to the leaf Operator expressions.
* Note, this loses context of any nested ANDs or ORs, so will not be useful once we support nested conditions */
function flattenGroupByExpressions(
expressions: QueryEditorArrayExpression['expressions']
): QueryEditorGroupByExpression[] {
return expressions.flatMap((expression) => {
if (expression.type === QueryEditorExpressionType.GroupBy) {
return expression;
}
// Expressions that we don't expect to find in the GROUP BY will be ignored
return [];
});
}
/** Returns a flattened list of GROUP BY expressions, losing all context of nested filters or AND vs OR.
*/
export function getFlattenedGroupBys(sql: SQLExpression): QueryEditorGroupByExpression[] {
const groupBy = sql.groupBy;
return flattenGroupByExpressions(groupBy?.expressions ?? []);
}
/** Converts a string array to a Dimensions object with null values **/
export function stringArrayToDimensions(arr: string[]): Dimensions {
return arr.reduce((acc, curr) => {
if (curr) {
return { ...acc, [curr]: null };
}
return acc;
}, {});
}
export function setSql(query: CloudWatchMetricsQuery, sql: SQLExpression): CloudWatchMetricsQuery {
return {
...query,
sql: {
...(query.sql ?? {}),
...sql,
},
};
}
export function setNamespace(query: CloudWatchMetricsQuery, namespace: string | undefined): CloudWatchMetricsQuery {
const sql = query.sql ?? {};
if (namespace === undefined) {
return setSql(query, {
from: undefined,
});
}
// It's just a simple `FROM "AWS/EC2"` expression
if (!sql.from || sql.from.type === QueryEditorExpressionType.Property) {
return setSql(query, {
from: {
type: QueryEditorExpressionType.Property,
property: {
type: QueryEditorPropertyType.String,
name: namespace,
},
},
});
}
// It's a more complicated `FROM SCHEMA("AWS/EC2", ...)` expression
if (sql.from.type === QueryEditorExpressionType.Function) {
const namespaceParam: QueryEditorFunctionParameterExpression = {
type: QueryEditorExpressionType.FunctionParameter,
name: namespace,
};
const labelKeys = (sql.from.parameters ?? []).slice(1);
return setSql(query, {
from: {
type: QueryEditorExpressionType.Function,
name: SCHEMA,
parameters: [namespaceParam, ...labelKeys],
},
});
}
// TODO: do the with schema bit
return query;
}
export function setSchemaLabels(
query: CloudWatchMetricsQuery,
schemaLabels: Array<SelectableValue<string>> | SelectableValue<string>
): CloudWatchMetricsQuery {
const sql = query.sql ?? {};
schemaLabels = Array.isArray(schemaLabels) ? schemaLabels.map((l) => l.value) : [schemaLabels.value];
// schema labels are the second parameter in the schema function. `... FROM SCHEMA("AWS/EC2", label1, label2 ...)`
if (sql.from?.type === QueryEditorExpressionType.Function && sql.from.parameters?.length) {
const parameters: QueryEditorFunctionParameterExpression[] = (schemaLabels ?? []).map((label: string) => ({
type: QueryEditorExpressionType.FunctionParameter,
name: label,
}));
const namespaceParam = (sql.from.parameters ?? [])[0];
return setSql(query, {
from: {
type: QueryEditorExpressionType.Function,
name: SCHEMA,
parameters: [namespaceParam, ...parameters],
},
});
}
return query;
}
export function setMetricName(query: CloudWatchMetricsQuery, metricName: string): CloudWatchMetricsQuery {
const param: QueryEditorFunctionParameterExpression = {
type: QueryEditorExpressionType.FunctionParameter,
name: metricName,
};
return setSql(query, {
select: {
type: QueryEditorExpressionType.Function,
...(query.sql?.select ?? {}),
parameters: [param],
},
});
}
export function setAggregation(query: CloudWatchMetricsQuery, aggregation: string): CloudWatchMetricsQuery {
return setSql(query, {
select: {
type: QueryEditorExpressionType.Function,
...(query.sql?.select ?? {}),
name: aggregation,
},
});
}
export function setOrderBy(query: CloudWatchMetricsQuery, aggregation: string): CloudWatchMetricsQuery {
return setSql(query, {
orderBy: {
type: QueryEditorExpressionType.Function,
name: aggregation,
},
});
}
export function setWithSchema(query: CloudWatchMetricsQuery, withSchema: boolean): CloudWatchMetricsQuery {
const namespace = getNamespaceFromExpression((query.sql ?? {}).from);
if (withSchema) {
const namespaceParam: QueryEditorFunctionParameterExpression = {
type: QueryEditorExpressionType.FunctionParameter,
name: namespace,
};
return setSql(query, {
from: {
type: QueryEditorExpressionType.Function,
name: SCHEMA,
parameters: [namespaceParam],
},
});
}
return setSql(query, {
from: {
type: QueryEditorExpressionType.Property,
property: {
type: QueryEditorPropertyType.String,
name: namespace,
},
},
});
}
/** Sets the left hand side (InstanceId) in an OperatorExpression
* Accepts a partial expression to use in an editor
*/
export function setOperatorExpressionProperty(
expression: Partial<QueryEditorOperatorExpression>,
property: string
): QueryEditorOperatorExpression {
return {
type: QueryEditorExpressionType.Operator,
property: {
type: QueryEditorPropertyType.String,
name: property,
},
operator: expression.operator ?? {},
};
}
/** Sets the operator ("==") in an OperatorExpression
* Accepts a partial expression to use in an editor
*/
export function setOperatorExpressionName(
expression: Partial<QueryEditorOperatorExpression>,
name: string
): QueryEditorOperatorExpression {
return {
type: QueryEditorExpressionType.Operator,
property: expression.property ?? {
type: QueryEditorPropertyType.String,
},
operator: {
...expression.operator,
name,
},
};
}
/** Sets the right hand side ("i-abc123445") in an OperatorExpression
* Accepts a partial expression to use in an editor
*/
export function setOperatorExpressionValue(
expression: Partial<QueryEditorOperatorExpression>,
value: string
): QueryEditorOperatorExpression {
return {
type: QueryEditorExpressionType.Operator,
property: expression.property ?? {
type: QueryEditorPropertyType.String,
},
operator: {
...expression.operator,
value,
},
};
}
/** Creates a GroupByExpression for a specified field
*/
export function setGroupByField(field: string): QueryEditorGroupByExpression {
return {
type: QueryEditorExpressionType.GroupBy,
property: {
type: QueryEditorPropertyType.String,
name: field,
},
};
}