import { clone } 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 { 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 { // 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 { if (state.queryModel.segments.length === 1 && state.queryModel.segments[0].type === 'series-ref') { return; } if (fromIndex === 0) { addSelectMetricSegment(state); return; } const path = state.queryModel.getSegmentPathUpTo(fromIndex + 1); 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, fromIndex); state.segments = state.segments.splice(0, fromIndex); addSelectMetricSegment(state); } } else if (segments[0].expandable) { if (state.segments.length === fromIndex) { addSelectMetricSegment(state); } else { await checkOtherSegments(state, fromIndex + 1); } } } catch (err) { 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 { 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; } const 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') ); if (state.queryModel.target.target !== 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; }