Files
Brendan O'Handley d5d07894b1 Graphite: bug fix, strip white space from queries before comparing them (#60133)
strip white space from queries before comparing them
2022-12-14 11:39:29 -05:00

203 lines
6.6 KiB
TypeScript

import { clone, some } from 'lodash';
import { createErrorNotification } from '../../../../core/copy/appNotification';
import { notifyApp } from '../../../../core/reducers/appNotification';
import { dispatch } from '../../../../store/store';
import { FuncInstance } from '../gfunc';
import { GraphiteQuery, GraphiteTagOperator } from '../types';
import { GraphiteQueryEditorState } from './store';
/**
* Helpers used by reducers and providers. They modify state object directly so should operate on a copy of the state.
*/
export const GRAPHITE_TAG_OPERATORS: GraphiteTagOperator[] = ['=', '!=', '=~', '!=~'];
/**
* Tag names and metric names are displayed in a single dropdown. This prefix is used to
* distinguish both in the UI.
*/
export const TAG_PREFIX = 'tag: ';
/**
* Create new AST based on new query.
* Build segments from parsed metric name and functions.
*/
export async function parseTarget(state: GraphiteQueryEditorState): Promise<void> {
state.queryModel.parseTarget();
await buildSegments(state);
}
/**
* Create segments out of the current metric path + add "select metrics" if it's possible to add more to the path
*/
export async function buildSegments(state: GraphiteQueryEditorState, modifyLastSegment = true): Promise<void> {
// Start with a shallow copy from the model, then check if "select metric" segment should be added at the end
state.segments = clone(state.queryModel.segments);
const checkOtherSegmentsIndex = state.queryModel.checkOtherSegmentsIndex || 0;
await checkOtherSegments(state, checkOtherSegmentsIndex, modifyLastSegment);
}
/**
* Add "select metric" segment at the end
*/
export function addSelectMetricSegment(state: GraphiteQueryEditorState): void {
state.queryModel.addSelectMetricSegment();
state.segments.push({ value: 'select metric', fake: true });
}
/**
* Validates the state after adding or changing a segment:
* - adds "select metric" only when more segments can be added to the metric name
* - check if subsequent segments are still valid if in-between segment changes and
* removes invalid segments.
*/
export async function checkOtherSegments(
state: GraphiteQueryEditorState,
fromIndex: number,
modifyLastSegment = true
): Promise<void> {
if (state.queryModel.segments.length === 1 && state.queryModel.segments[0].type === 'series-ref') {
return;
}
if (fromIndex === 0) {
addSelectMetricSegment(state);
return;
}
const currentFromIndex = fromIndex + 1;
const path = state.queryModel.getSegmentPathUpTo(currentFromIndex);
if (path === '') {
return;
}
try {
const segments = await state.datasource.metricFindQuery(path);
if (segments.length === 0) {
if (path !== '' && modifyLastSegment) {
state.queryModel.segments = state.queryModel.segments.splice(0, currentFromIndex);
state.segments = state.segments.splice(0, currentFromIndex);
if (!some(state.segments, { fake: true })) {
addSelectMetricSegment(state);
}
}
} else if (segments[0].expandable) {
if (state.segments.length === fromIndex) {
addSelectMetricSegment(state);
} else {
await checkOtherSegments(state, currentFromIndex);
}
}
} catch (err) {
if (err instanceof Error) {
handleMetricsAutoCompleteError(state, err);
}
}
}
export function spliceSegments(state: GraphiteQueryEditorState, index: number): void {
state.segments = state.segments.splice(0, index);
state.queryModel.segments = state.queryModel.segments.splice(0, index);
}
export function emptySegments(state: GraphiteQueryEditorState): void {
state.queryModel.segments = [];
state.segments = [];
}
/**
* When seriesByTag function is added the UI changes it's state and only tags can be added from now.
*/
export async function addSeriesByTagFunc(state: GraphiteQueryEditorState, tag: string): Promise<void> {
const newFunc = state.datasource.createFuncInstance('seriesByTag', {
withDefaultParams: false,
});
const tagParam = `${tag}=`;
newFunc.params = [tagParam];
state.queryModel.addFunction(newFunc);
newFunc.added = true;
emptySegments(state);
handleTargetChanged(state);
await parseTarget(state);
}
export function smartlyHandleNewAliasByNode(state: GraphiteQueryEditorState, func: FuncInstance): void {
if (func.def.name !== 'aliasByNode') {
return;
}
for (let i = 0; i < state.segments.length; i++) {
if (state.segments[i].value.indexOf('*') >= 0) {
func.params[0] = i;
func.added = false;
handleTargetChanged(state);
return;
}
}
}
/**
* Pauses running the query to allow selecting tag value. This is to prevent getting errors if the query is run
* for a tag with no selected value.
*/
export function pause(state: GraphiteQueryEditorState): void {
state.paused = true;
}
export function removeTagPrefix(value: string): string {
return value.replace(TAG_PREFIX, '');
}
export function handleTargetChanged(state: GraphiteQueryEditorState): void {
if (state.queryModel.error) {
return;
}
let oldTarget = state.queryModel.target.target;
// Interpolate from other queries:
// Because of mixed data sources the list may contain queries for non-Graphite data sources. To ensure a valid query
// is used for interpolation we should check required properties are passed though in theory it allows to interpolate
// with queries that contain "target" property as well.
state.queryModel.updateModelTarget(
(state.queries || []).filter((query) => 'target' in query && typeof (query as GraphiteQuery).target === 'string')
);
// remove spaces from old and new targets
const newTarget = state.queryModel.target.target.replace(/\s+/g, '');
oldTarget = oldTarget.replace(/\s+/g, '');
if (newTarget !== oldTarget && !state.paused) {
state.refresh();
}
}
/**
* When metrics autocomplete fails - the error is shown, but only once per page view
*/
export function handleMetricsAutoCompleteError(
state: GraphiteQueryEditorState,
error: Error
): GraphiteQueryEditorState {
if (!state.metricAutoCompleteErrorShown) {
state.metricAutoCompleteErrorShown = true;
dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
}
return state;
}
/**
* When tags autocomplete fails - the error is shown, but only once per page view
*/
export function handleTagsAutoCompleteError(state: GraphiteQueryEditorState, error: Error): GraphiteQueryEditorState {
if (!state.tagsAutoCompleteErrorShown) {
state.tagsAutoCompleteErrorShown = true;
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
}
return state;
}