Files
grafana/public/app/plugins/datasource/graphite/graphite_query.ts
Piotr Jamróz bd95856ff9 Graphite: Migrate to React (part 6: Remove AngularJS Query Editor) (#37919)
* Add UMLs

* Add rendered diagrams

* Move QueryCtrl to flux

* Remove redundant param in the reducer

* Use named imports for lodash and fix typing for GraphiteTagOperator

* Add missing async/await

* Extract providers to a separate file

* Clean up async await

* Rename controller functions back to main

* Simplify creating actions

* Re-order controller functions

* Separate helpers from actions

* Rename vars

* Simplify helpers

* Move controller methods to state reducers

* Remove docs (they are added in design doc)

* Move actions.ts to state folder

* Add docs

* Add old methods stubs for easier review

* Check how state dependencies will be mapped

* Rename state to store

* Rename state to store

* Rewrite spec tests for Graphite Query Controller

* Update docs

* Update docs

* Add GraphiteTextEditor

* Add play button

* Add AddGraphiteFunction

* Use Segment to simplify AddGraphiteFunction

* Memoize function defs

* Fix useCallback deps

* Update public/app/plugins/datasource/graphite/state/helpers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/helpers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/helpers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Add more type definitions

* Remove submitOnClickAwayOption

This behavior is actually needed to remove parameters in functions

* Load function definitions before parsing the target on initial load

* Add button padding

* Fix loading function definitions

* Change targetChanged to updateQuery to avoid mutating state directly

It's also needed for extra refresh/runQuery execution as handleTargetChanged doesn't handle changing the raw query

* Fix updating query after adding a function

* Simplify updating function params

* Migrate function editor to react

* Simplify setting Segment Select min width

* Remove unnecessary changes to SegmentInput

* Extract view logic to a helper and update types definitions

* Clean up types

* Update FuncDef types and add tests

* Show red border for unknown functions

* Autofocus on new params

* Extract params mapping to a helper

* Split code between params and function editor

* Focus on the first param when a function is added even if it's an optional argument

* Add function editor tests

* Remove todo marker

* Fix adding new functions

* Allow empty value in selects for removing function params

* Add placeholders and fix styling

* Add more docs

* Create basic implementation for metrics and tags

* Post merge fixes

These files are not .ts

* Remove mapping to Angular dropdowns

* Simplify mapping tag names, values and operators

* Simplify mapping metrics

* Fix removing tags and autocomplete

* Simplify debouncing providers

* Ensure options are loaded twice and segment is opened

* Remove focusing new segments logic (not supported by React's segment)

* Clean up

* Move debouncing to components

* Simplify mapping to selectable options

* Add docs

* Group all components

* Remove unused controller methods

* Create Dispatch context

* Group Series and Tags Sections

* Create Functions section

* Create Section component

* use getStyles

* remove redundant async/await

* Remove

* remove redundant async/await

* Remove console.log and silent test console output

* Do not display the name of the selected dropdown option

* Move Section to grafana-ui

* Update storybook

* Simplify SectionLabel

* Fix Influx tests

* Fix API Extractor warnings

* Fix API Extractor warnings

* Do not show hidden functions

* Use block docs for better doc generation

* Handle undefined values provided for autocomplete

* Basic integration

* Move creating state to context.tsx

* Update tests

* Rename test

* Clean up dependencies

panel.targets is not needed for interpolation - it happens in the data source itself. It was used only to show query ref in the the dropdown for the segment.

* Update time range when it changes

* Change action name

* Simplify segments cloning

* Remove redundant variable

* Use styles instead of direct css

* Update docs

* Remove angular wrappers

* Remove redundant tests

* Section -> SegmentSection

* Simplify section styling

* Remove redundant div

* Fix unit tests

* Simplify SegmentSection component

* Use theme.spacing

* Use empty label instead of a single space label

* Remove targetFull

It was used in the past two store the query interpolated with sub-queries inside the model and send both to the backed (interpolated and not interpolated). This has been changed though - the logic has been moved away from model to the data source where interpolation happens and now only interpolated query is passed meaning targetFull is not needed anymore.

* Revert "Remove targetFull"

This reverts commit 499f8b33

* Bring back calculating targetFull

* Clean up

* Add missing dep

* Add missing dep in tests

* Fix time range synchronization

* Fix warning message

* Remove unused type

* Synchronize changes to the query

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
2021-09-02 10:18:14 +02:00

335 lines
9.3 KiB
TypeScript

import { compact, each, findIndex, flatten, get, join, keyBy, last, map, reduce, without } from 'lodash';
import { arrayMove } from 'app/core/utils/arrayMove';
import { Parser } from './parser';
import { TemplateSrv } from '@grafana/runtime';
import { ScopedVars } from '@grafana/data';
import { FuncInstance } from './gfunc';
import { GraphiteSegment } from './types';
import { GraphiteDatasource } from './datasource';
export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~';
export type GraphiteTag = {
key: string;
operator: GraphiteTagOperator;
value: string;
};
export type GraphiteTarget = {
refId: string | number;
target: string;
/**
* Contains full query after interpolating sub-queries (e.g. "function(#A)" referencing query with refId=A)
*/
targetFull: string;
textEditor: boolean;
paused: boolean;
};
export default class GraphiteQuery {
datasource: GraphiteDatasource;
target: GraphiteTarget;
functions: FuncInstance[] = [];
segments: GraphiteSegment[] = [];
tags: GraphiteTag[] = [];
error: any;
seriesByTagUsed = false;
checkOtherSegmentsIndex = 0;
removeTagValue: string;
templateSrv: any;
scopedVars: any;
/** @ngInject */
constructor(datasource: any, target: any, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
this.datasource = datasource;
this.target = target;
this.templateSrv = templateSrv;
this.scopedVars = scopedVars;
this.parseTarget();
this.removeTagValue = '-- remove tag --';
}
parseTarget() {
this.functions = [];
this.segments = [];
this.tags = [];
this.seriesByTagUsed = false;
this.error = null;
if (this.target.textEditor) {
return;
}
const parser = new Parser(this.target.target);
const astNode = parser.getAst();
if (astNode === null) {
this.checkOtherSegmentsIndex = 0;
return;
}
if (astNode.type === 'error') {
this.error = astNode.message + ' at position: ' + astNode.pos;
this.target.textEditor = true;
return;
}
try {
this.parseTargetRecursive(astNode, null);
} catch (err) {
console.error('error parsing target:', err.message);
this.error = err.message;
this.target.textEditor = true;
}
this.checkOtherSegmentsIndex = this.segments.length - 1;
}
getSegmentPathUpTo(index: number) {
const arr = this.segments.slice(0, index);
return reduce(
arr,
(result, segment) => {
return result ? result + '.' + segment.value : segment.value;
},
''
);
}
parseTargetRecursive(astNode: any, func: any): any {
if (astNode === null) {
return null;
}
switch (astNode.type) {
case 'function':
const innerFunc = this.datasource.createFuncInstance(astNode.name, {
withDefaultParams: false,
});
each(astNode.params, (param) => {
this.parseTargetRecursive(param, innerFunc);
});
innerFunc.updateText();
this.functions.push(innerFunc);
// extract tags from seriesByTag function and hide function
if (innerFunc.def.name === 'seriesByTag' && !this.seriesByTagUsed) {
this.seriesByTagUsed = true;
innerFunc.hidden = true;
this.tags = this.splitSeriesByTagParams(innerFunc);
}
break;
case 'series-ref':
if (this.segments.length > 0 || this.getSeriesByTagFuncIndex() >= 0) {
this.addFunctionParameter(func, astNode.value);
} else {
this.segments.push(astNode);
}
break;
case 'bool':
case 'string':
case 'number':
this.addFunctionParameter(func, astNode.value);
break;
case 'metric':
if (this.segments.length || this.tags.length) {
this.addFunctionParameter(func, join(map(astNode.segments, 'value'), '.'));
} else {
this.segments = astNode.segments;
}
break;
}
}
updateSegmentValue(segment: any, index: number) {
this.segments[index].value = segment.value;
}
addSelectMetricSegment() {
this.segments.push({ value: 'select metric' });
}
addFunction(newFunc: any) {
this.functions.push(newFunc);
}
addFunctionParameter(func: any, value: string) {
if (func.params.length >= func.def.params.length && !get(last(func.def.params), 'multiple', false)) {
throw { message: 'too many parameters for function ' + func.def.name };
}
func.params.push(value);
}
removeFunction(func: any) {
this.functions = without(this.functions, func);
}
moveFunction(func: any, offset: number) {
const index = this.functions.indexOf(func);
arrayMove(this.functions, index, index + offset);
}
updateModelTarget(targets: any) {
const wrapFunction = (target: string, func: any) => {
return func.render(target, (value: string) => {
return this.templateSrv.replace(value, this.scopedVars);
});
};
if (!this.target.textEditor) {
const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.select metric$/, '');
this.target.target = reduce(this.functions, wrapFunction, metricPath);
}
this.updateRenderedTarget(this.target, targets);
// loop through other queries and update targetFull as needed
for (const target of targets || []) {
if (target.refId !== this.target.refId) {
this.updateRenderedTarget(target, targets);
}
}
// clean-up added param
this.functions.forEach((func) => (func.added = false));
}
updateRenderedTarget(target: { refId: string | number; target: any; targetFull: any }, targets: any) {
// render nested query
const targetsByRefId = keyBy(targets, 'refId');
// no references to self
delete targetsByRefId[target.refId];
const nestedSeriesRefRegex = /\#([A-Z])/g;
let targetWithNestedQueries = target.target;
// Use ref count to track circular references
function countTargetRefs(targetsByRefId: any, refId: string) {
let refCount = 0;
each(targetsByRefId, (t, id) => {
if (id !== refId) {
const match = nestedSeriesRefRegex.exec(t.target);
const count = match && match.length ? match.length - 1 : 0;
refCount += count;
}
});
targetsByRefId[refId].refCount = refCount;
}
each(targetsByRefId, (t, id) => {
countTargetRefs(targetsByRefId, id);
});
// Keep interpolating until there are no query references
// The reason for the loop is that the referenced query might contain another reference to another query
while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
const updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match: string, g1: string) => {
const t = targetsByRefId[g1];
if (!t) {
return match;
}
// no circular references
if (t.refCount === 0) {
delete targetsByRefId[g1];
}
t.refCount--;
return t.target;
});
if (updated === targetWithNestedQueries) {
break;
}
targetWithNestedQueries = updated;
}
delete target.targetFull;
if (target.target !== targetWithNestedQueries) {
target.targetFull = targetWithNestedQueries;
}
}
splitSeriesByTagParams(func: { params: any }) {
const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
return flatten(
map(func.params, (param: string) => {
const matches = tagPattern.exec(param);
if (matches) {
const tag = matches.slice(1);
if (tag.length === 3) {
return {
key: tag[0],
operator: tag[1] as GraphiteTagOperator,
value: tag[2],
};
}
}
return [];
})
);
}
getSeriesByTagFuncIndex() {
return findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
}
getSeriesByTagFunc() {
const seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
if (seriesByTagFuncIndex >= 0) {
return this.functions[seriesByTagFuncIndex];
} else {
return undefined;
}
}
addTag(tag: { key: any; operator: GraphiteTagOperator; value: string }) {
const newTagParam = renderTagString(tag);
this.getSeriesByTagFunc()!.params.push(newTagParam);
this.tags.push(tag);
}
removeTag(index: number) {
this.getSeriesByTagFunc()!.params.splice(index, 1);
this.tags.splice(index, 1);
}
updateTag(tag: { key: string; operator: GraphiteTagOperator; value: string }, tagIndex: number) {
this.error = null;
if (tag.key === this.removeTagValue) {
this.removeTag(tagIndex);
if (this.tags.length === 0) {
this.removeFunction(this.getSeriesByTagFunc());
this.checkOtherSegmentsIndex = 0;
this.seriesByTagUsed = false;
}
return;
}
this.getSeriesByTagFunc()!.params[tagIndex] = renderTagString(tag);
this.tags[tagIndex] = tag;
}
renderTagExpressions(excludeIndex = -1) {
return compact(
map(this.tags, (tagExpr, index) => {
// Don't render tag that we want to lookup
if (index !== excludeIndex) {
return tagExpr.key + tagExpr.operator + tagExpr.value;
} else {
return undefined;
}
})
);
}
}
function renderTagString(tag: { key: any; operator?: any; value?: any }) {
return tag.key + tag.operator + tag.value;
}