mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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>
335 lines
9.3 KiB
TypeScript
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;
|
|
}
|