mirror of
https://github.com/grafana/grafana.git
synced 2026-08-04 10:33:21 -05:00
Graphite: Migrate to React (part 1: move state to redux) (#36484)
* 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 * 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 * Load function definitions before parsing the target on initial load * 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 * Remove redundant awaits * Use redux Action * Use more specific type for GraphiteTag Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
co-authored by
Giordano Ricci
parent
71a9f7ce3c
commit
dbe7d3298d
@@ -25,6 +25,7 @@ export function graphiteAddFunc($compile: any) {
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
// TODO: ctrl.state is not ready yet when link() is called. This will be moved to a separate provider.
|
||||
ctrl.datasource.getFuncDefs().then((funcDefs: FuncDef[]) => {
|
||||
const allFunctions = map(funcDefs, 'name').sort();
|
||||
|
||||
@@ -36,7 +37,7 @@ export function graphiteAddFunc($compile: any) {
|
||||
minLength: 1,
|
||||
items: 10,
|
||||
updater: (value: any) => {
|
||||
let funcDef: any = ctrl.datasource.getFuncDef(value);
|
||||
let funcDef: any = ctrl.state.datasource.getFuncDef(value);
|
||||
if (!funcDef) {
|
||||
// try find close match
|
||||
value = value.toLowerCase();
|
||||
@@ -96,7 +97,7 @@ export function graphiteAddFunc($compile: any) {
|
||||
|
||||
let funcDef;
|
||||
try {
|
||||
funcDef = ctrl.datasource.getFuncDef($('a', this).text());
|
||||
funcDef = ctrl.state.datasource.getFuncDef($('a', this).text());
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { assign, clone, each, last, map, partial } from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { actions } from './state/actions';
|
||||
|
||||
/** @ngInject */
|
||||
export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
||||
@@ -28,15 +29,15 @@ export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
||||
let cancelBlur: any = null;
|
||||
|
||||
ctrl.handleRemoveFunction = (func: any) => {
|
||||
ctrl.removeFunction(func);
|
||||
ctrl.dispatch(actions.removeFunction({ func }));
|
||||
};
|
||||
|
||||
ctrl.handleMoveLeft = (func: any) => {
|
||||
ctrl.moveFunction(func, -1);
|
||||
ctrl.dispatch(actions.moveFunction({ func, offset: -1 }));
|
||||
};
|
||||
|
||||
ctrl.handleMoveRight = (func: any) => {
|
||||
ctrl.moveFunction(func, 1);
|
||||
ctrl.dispatch(actions.moveFunction({ func, offset: 1 }));
|
||||
};
|
||||
|
||||
function clickFuncParam(this: any, paramIndex: any) {
|
||||
@@ -102,7 +103,10 @@ export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
||||
scheduledRelinkIfNeeded();
|
||||
|
||||
$scope.$apply(() => {
|
||||
ctrl.targetChanged();
|
||||
// WIP: at the moment function params are mutated directly by func_editor
|
||||
// after migrating to react it will be done by passing param value to
|
||||
// updateFunctionParam action
|
||||
ctrl.dispatch(actions.updateFunctionParam({ func }));
|
||||
});
|
||||
|
||||
if ($link.hasClass('query-part__last') && newValue === '') {
|
||||
|
||||
@@ -974,6 +974,13 @@ export class FuncInstance {
|
||||
params: any;
|
||||
text: any;
|
||||
added: boolean;
|
||||
/**
|
||||
* Hidden functions are not displayed in UI but available in text editor
|
||||
* This is used for seriesByTagUsed function which when used switches
|
||||
* the editor to tag-only mode. Defined tags are provided to seriesByTagUsed
|
||||
* as parameters.
|
||||
*/
|
||||
hidden?: boolean;
|
||||
|
||||
constructor(funcDef: any, options?: { withDefaultParams: any }) {
|
||||
this.def = funcDef;
|
||||
|
||||
@@ -3,6 +3,9 @@ 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 = '=' | '=~' | '!=' | '!=~';
|
||||
|
||||
@@ -12,11 +15,22 @@ export type GraphiteTag = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
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: any;
|
||||
target: any;
|
||||
functions: any[] = [];
|
||||
segments: any[] = [];
|
||||
datasource: GraphiteDatasource;
|
||||
target: GraphiteTarget;
|
||||
functions: FuncInstance[] = [];
|
||||
segments: GraphiteSegment[] = [];
|
||||
tags: GraphiteTag[] = [];
|
||||
error: any;
|
||||
seriesByTagUsed = false;
|
||||
@@ -272,12 +286,12 @@ export default class GraphiteQuery {
|
||||
|
||||
addTag(tag: { key: any; operator: GraphiteTagOperator; value: string }) {
|
||||
const newTagParam = renderTagString(tag);
|
||||
this.getSeriesByTagFunc().params.push(newTagParam);
|
||||
this.getSeriesByTagFunc()!.params.push(newTagParam);
|
||||
this.tags.push(tag);
|
||||
}
|
||||
|
||||
removeTag(index: number) {
|
||||
this.getSeriesByTagFunc().params.splice(index, 1);
|
||||
this.getSeriesByTagFunc()!.params.splice(index, 1);
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -290,7 +304,7 @@ export default class GraphiteQuery {
|
||||
}
|
||||
|
||||
const newTagParam = renderTagString(tag);
|
||||
this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
|
||||
this.getSeriesByTagFunc()!.params[tagIndex] = newTagParam;
|
||||
this.tags[tagIndex] = tag;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.target.textEditor">
|
||||
<input type="text" class="gf-form-input" style="font-family: monospace;" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
|
||||
<div class="gf-form" ng-show="ctrl.state.target.textEditor">
|
||||
<input type="text" class="gf-form-input" style="font-family: monospace;" ng-value="ctrl.state.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged($event)"></input>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ctrl.target.textEditor">
|
||||
@@ -10,7 +10,7 @@
|
||||
<label class="gf-form-label width-6 query-keyword">Series</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
|
||||
<div ng-if="ctrl.state.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.state.queryModel.tags" class="gf-form">
|
||||
<gf-form-dropdown
|
||||
model="tag.key"
|
||||
allow-custom="true"
|
||||
@@ -21,7 +21,7 @@
|
||||
get-options="ctrl.getTags($index, $query)"
|
||||
on-change="ctrl.tagChanged(tag, $index)"
|
||||
></gf-form-dropdown>
|
||||
|
||||
|
||||
<gf-form-dropdown
|
||||
model="tag.operator"
|
||||
label-mode="true"
|
||||
@@ -39,19 +39,19 @@
|
||||
placeholder="Tag value"
|
||||
get-options="ctrl.getTagValues(tag, $index, $query)"
|
||||
on-change="ctrl.tagChanged(tag, $index)"
|
||||
/>
|
||||
<label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
|
||||
></gf-form-dropdown>
|
||||
<label class="gf-form-label query-keyword" ng-if="$index !== ctrl.state.queryModel.tags.length - 1">AND</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
|
||||
<div ng-if="ctrl.state.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.state.addTagSegments" role="menuitem" class="gf-form">
|
||||
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" debounce="true" />
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
|
||||
<div ng-if="!ctrl.state.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.state.segments" role="menuitem" class="gf-form">
|
||||
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.paused" class="gf-form">
|
||||
<div ng-if="ctrl.state.paused" class="gf-form">
|
||||
<a ng-click="ctrl.unpause()" class="gf-form-label query-part"><icon name="'play'"></icon></a>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<label class="gf-form-label width-6 query-keyword">Functions</label>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="func in ctrl.queryModel.functions" class="gf-form">
|
||||
<div ng-repeat="func in ctrl.state.queryModel.functions" class="gf-form">
|
||||
<span graphite-func-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
|
||||
</div>
|
||||
|
||||
@@ -78,5 +78,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</query-editor-row>
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import './add_graphite_func';
|
||||
import './func_editor';
|
||||
|
||||
import { each, eachRight, map, remove } from 'lodash';
|
||||
import GraphiteQuery, { GraphiteTagOperator } from './graphite_query';
|
||||
import GraphiteQuery from './graphite_query';
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||
import { auto } from 'angular';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
|
||||
const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
|
||||
const TAG_PREFIX = 'tag: ';
|
||||
import { actions } from './state/actions';
|
||||
import { getAltSegments, getTagOperators, getTags, getTagsAsSegments, getTagValues } from './state/providers';
|
||||
import { createStore, GraphiteQueryEditorState } from './state/store';
|
||||
import {
|
||||
AngularDropdownOptions,
|
||||
GraphiteActionDispatcher,
|
||||
GraphiteQueryEditorAngularDependencies,
|
||||
GraphiteSegment,
|
||||
GraphiteTag,
|
||||
} from './types';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
/**
|
||||
* @deprecated Moved to state/store
|
||||
*
|
||||
* Note: methods marked with WIP are kept for easier diffing with previous changes. They will be removed when
|
||||
* GraphiteQueryCtrl is replaced with a react component.
|
||||
*/
|
||||
export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
@@ -24,437 +33,227 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
supportsTags = false;
|
||||
paused = false;
|
||||
|
||||
// to avoid error flooding, these errors are shown only once per session
|
||||
private _tagsAutoCompleteErrorShown = false;
|
||||
private _metricAutoCompleteErrorShown = false;
|
||||
private state: GraphiteQueryEditorState;
|
||||
private readonly dispatch: GraphiteActionDispatcher;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
$scope: any,
|
||||
$injector: auto.IInjectorService,
|
||||
private uiSegmentSrv: any,
|
||||
private templateSrv: TemplateSrv,
|
||||
$timeout: any
|
||||
private templateSrv: TemplateSrv
|
||||
) {
|
||||
super($scope, $injector);
|
||||
this.supportsTags = this.datasource.supportsTags;
|
||||
this.paused = false;
|
||||
this.target.target = this.target.target || '';
|
||||
|
||||
this.datasource.waitForFuncDefsLoaded().then(() => {
|
||||
this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
|
||||
this.buildSegments(false);
|
||||
// This controller will be removed once it's root partial (query.editor.html) renders only React components.
|
||||
// All component will be wrapped in ReactQueryEditor receiving DataSourceApi in QueryRow.renderQueryEditor
|
||||
// The init() action will be removed and the store will be created in ReactQueryEditor. Note that properties
|
||||
// passed to React component in QueryRow.renderQueryEditor are different than properties passed to Angular editor
|
||||
// and will be mapped/provided in a way described below:
|
||||
const deps = {
|
||||
// WIP: to be removed. It's not passed to ReactQueryEditor but it's used only to:
|
||||
// - get refId of the query (refId be passed in query property),
|
||||
// - and to refresh changes (this will be handled by onChange passed to ReactQueryEditor)
|
||||
// - it's needed to get other targets to interpolate the query (this will be added in QueryRow)
|
||||
panelCtrl: this.panelCtrl,
|
||||
|
||||
// WIP: to be replaced with query property passed to ReactQueryEditor
|
||||
target: this.target,
|
||||
|
||||
// WIP: same object will be passed to ReactQueryEditor
|
||||
datasource: this.datasource,
|
||||
|
||||
// This is used to create view models for Angular <metric-segment> component (view models are MetricSegment objects)
|
||||
// It will be simplified to produce data needed by React <SegmentAsync/> component
|
||||
uiSegmentSrv: this.uiSegmentSrv,
|
||||
|
||||
// WIP: will be replaced with:
|
||||
// import { getTemplateSrv } from 'app/features/templating/template_srv';
|
||||
templateSrv: this.templateSrv,
|
||||
};
|
||||
|
||||
const [dispatch, state] = createStore((state) => {
|
||||
this.state = state;
|
||||
// HACK: inefficient but not invoked frequently. It's needed to inform angular watcher about state changes
|
||||
// for state shared between React/AngularJS. Actions invoked from React component will not mark the scope
|
||||
// as dirty and the view won't be updated. It has to happen manually on each state change.
|
||||
this.$scope.$digest();
|
||||
});
|
||||
|
||||
this.removeTagValue = '-- remove tag --';
|
||||
this.state = state;
|
||||
this.dispatch = dispatch;
|
||||
|
||||
this.dispatch(actions.init(deps as GraphiteQueryEditorAngularDependencies));
|
||||
}
|
||||
|
||||
parseTarget() {
|
||||
this.queryModel.parseTarget();
|
||||
this.buildSegments();
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
toggleEditorMode() {
|
||||
this.target.textEditor = !this.target.textEditor;
|
||||
this.parseTarget();
|
||||
async toggleEditorMode() {
|
||||
await this.dispatch(actions.toggleEditorMode());
|
||||
}
|
||||
|
||||
buildSegments(modifyLastSegment = true) {
|
||||
this.segments = map(this.queryModel.segments, (segment) => {
|
||||
return this.uiSegmentSrv.newSegment(segment);
|
||||
});
|
||||
|
||||
const checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0;
|
||||
|
||||
promiseToDigest(this.$scope)(this.checkOtherSegments(checkOtherSegmentsIndex, modifyLastSegment));
|
||||
|
||||
if (this.queryModel.seriesByTagUsed) {
|
||||
this.fixTagSegments();
|
||||
}
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
addSelectMetricSegment() {
|
||||
this.queryModel.addSelectMetricSegment();
|
||||
this.segments.push(this.uiSegmentSrv.newSelectMetric());
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
checkOtherSegments(fromIndex: number, modifyLastSegment = true) {
|
||||
if (this.queryModel.segments.length === 1 && this.queryModel.segments[0].type === 'series-ref') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (fromIndex === 0) {
|
||||
this.addSelectMetricSegment();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const path = this.queryModel.getSegmentPathUpTo(fromIndex + 1);
|
||||
if (path === '') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.datasource
|
||||
.metricFindQuery(path)
|
||||
.then((segments: any) => {
|
||||
if (segments.length === 0) {
|
||||
if (path !== '' && modifyLastSegment) {
|
||||
this.queryModel.segments = this.queryModel.segments.splice(0, fromIndex);
|
||||
this.segments = this.segments.splice(0, fromIndex);
|
||||
this.addSelectMetricSegment();
|
||||
}
|
||||
} else if (segments[0].expandable) {
|
||||
if (this.segments.length === fromIndex) {
|
||||
this.addSelectMetricSegment();
|
||||
} else {
|
||||
return this.checkOtherSegments(fromIndex + 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.handleMetricsAutoCompleteError(err);
|
||||
});
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
setSegmentFocus(segmentIndex: any) {
|
||||
each(this.segments, (segment, index) => {
|
||||
segment.focus = segmentIndex === index;
|
||||
});
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
getAltSegments(index: number, prefix: string) {
|
||||
let query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
|
||||
if (index > 0) {
|
||||
query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
|
||||
}
|
||||
const options = {
|
||||
range: this.panelCtrl.range,
|
||||
requestId: 'get-alt-segments',
|
||||
};
|
||||
|
||||
return this.datasource
|
||||
.metricFindQuery(query, options)
|
||||
.then((segments: any[]) => {
|
||||
const altSegments = map(segments, (segment) => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: segment.text,
|
||||
expandable: segment.expandable,
|
||||
});
|
||||
});
|
||||
|
||||
if (index > 0 && altSegments.length === 0) {
|
||||
return altSegments;
|
||||
}
|
||||
|
||||
// add query references
|
||||
if (index === 0) {
|
||||
eachRight(this.panelCtrl.panel.targets, (target) => {
|
||||
if (target.refId === this.queryModel.target.refId) {
|
||||
return;
|
||||
}
|
||||
|
||||
altSegments.unshift(
|
||||
this.uiSegmentSrv.newSegment({
|
||||
type: 'series-ref',
|
||||
value: '#' + target.refId,
|
||||
expandable: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// add template variables
|
||||
eachRight(this.templateSrv.getVariables(), (variable) => {
|
||||
altSegments.unshift(
|
||||
this.uiSegmentSrv.newSegment({
|
||||
type: 'template',
|
||||
value: '$' + variable.name,
|
||||
expandable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// add wildcard option
|
||||
altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
|
||||
|
||||
if (this.supportsTags && index === 0) {
|
||||
this.removeTaggedEntry(altSegments);
|
||||
return this.addAltTagSegments(prefix, altSegments);
|
||||
} else {
|
||||
return altSegments;
|
||||
}
|
||||
})
|
||||
.catch((err: any): any[] => {
|
||||
this.handleMetricsAutoCompleteError(err);
|
||||
return [];
|
||||
});
|
||||
/**
|
||||
* Get list of options for an empty segment or a segment with metric when it's clicked/opened.
|
||||
*
|
||||
* This is used for new segments and segments with metrics selected.
|
||||
*/
|
||||
async getAltSegments(index: number, text: string): Promise<GraphiteSegment[]> {
|
||||
return await getAltSegments(this.state, index, text);
|
||||
}
|
||||
|
||||
addAltTagSegments(prefix: string, altSegments: any[]) {
|
||||
return this.getTagsAsSegments(prefix).then((tagSegments: any[]) => {
|
||||
tagSegments = map(tagSegments, (segment) => {
|
||||
segment.value = TAG_PREFIX + segment.value;
|
||||
return segment;
|
||||
});
|
||||
return altSegments.concat(...tagSegments);
|
||||
});
|
||||
// WIP: moved to state/providers (the same name)
|
||||
}
|
||||
|
||||
removeTaggedEntry(altSegments: any[]) {
|
||||
altSegments = remove(altSegments, (s) => s.value === '_tagged');
|
||||
// WIP: moved to state/providers (the same name)
|
||||
}
|
||||
|
||||
segmentValueChanged(segment: { type: string; value: string; expandable: any }, segmentIndex: number) {
|
||||
this.error = null;
|
||||
this.queryModel.updateSegmentValue(segment, segmentIndex);
|
||||
|
||||
if (this.queryModel.functions.length > 0 && this.queryModel.functions[0].def.fake) {
|
||||
this.queryModel.functions = [];
|
||||
}
|
||||
|
||||
if (segment.type === 'tag') {
|
||||
const tag = removeTagPrefix(segment.value);
|
||||
this.pause();
|
||||
this.addSeriesByTagFunc(tag);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (segment.expandable) {
|
||||
return promiseToDigest(this.$scope)(
|
||||
this.checkOtherSegments(segmentIndex + 1).then(() => {
|
||||
this.setSegmentFocus(segmentIndex + 1);
|
||||
this.targetChanged();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.spliceSegments(segmentIndex + 1);
|
||||
}
|
||||
|
||||
this.setSegmentFocus(segmentIndex + 1);
|
||||
this.targetChanged();
|
||||
|
||||
return null;
|
||||
/**
|
||||
* Apply changes to a given metric segment
|
||||
*/
|
||||
async segmentValueChanged(segment: GraphiteSegment, index: number) {
|
||||
await this.dispatch(actions.segmentValueChanged({ segment, index }));
|
||||
}
|
||||
|
||||
spliceSegments(index: any) {
|
||||
this.segments = this.segments.splice(0, index);
|
||||
this.queryModel.segments = this.queryModel.segments.splice(0, index);
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
emptySegments() {
|
||||
this.queryModel.segments = [];
|
||||
this.segments = [];
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
targetTextChanged() {
|
||||
this.updateModelTarget();
|
||||
this.refresh();
|
||||
async targetTextChanged(event: ChangeEvent<HTMLInputElement>) {
|
||||
await this.dispatch(actions.updateQuery({ query: event.target.value }));
|
||||
}
|
||||
|
||||
updateModelTarget() {
|
||||
this.queryModel.updateModelTarget(this.panelCtrl.panel.targets);
|
||||
// WIP: moved to state/helpers as handleTargetChanged()
|
||||
}
|
||||
|
||||
targetChanged() {
|
||||
if (this.queryModel.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldTarget = this.queryModel.target.target;
|
||||
this.updateModelTarget();
|
||||
|
||||
if (this.queryModel.target !== oldTarget && !this.paused) {
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
addFunction(funcDef: any) {
|
||||
const newFunc = this.datasource.createFuncInstance(funcDef, {
|
||||
withDefaultParams: true,
|
||||
});
|
||||
newFunc.added = true;
|
||||
this.queryModel.addFunction(newFunc);
|
||||
this.smartlyHandleNewAliasByNode(newFunc);
|
||||
|
||||
if (this.segments.length === 1 && this.segments[0].fake) {
|
||||
this.emptySegments();
|
||||
}
|
||||
|
||||
if (!newFunc.params.length && newFunc.added) {
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
if (newFunc.def.name === 'seriesByTag') {
|
||||
this.parseTarget();
|
||||
}
|
||||
async addFunction(name: string) {
|
||||
await this.dispatch(actions.addFunction({ name }));
|
||||
}
|
||||
|
||||
removeFunction(func: any) {
|
||||
this.queryModel.removeFunction(func);
|
||||
this.targetChanged();
|
||||
// WIP: converted to "removeFunction" action and handled in state/store reducer
|
||||
// It's now dispatched in func_editor
|
||||
}
|
||||
|
||||
moveFunction(func: any, offset: any) {
|
||||
this.queryModel.moveFunction(func, offset);
|
||||
this.targetChanged();
|
||||
// WIP: converted to "moveFunction" action and handled in state/store reducer
|
||||
// It's now dispatched in func_editor
|
||||
}
|
||||
|
||||
addSeriesByTagFunc(tag: string) {
|
||||
const newFunc = this.datasource.createFuncInstance('seriesByTag', {
|
||||
withDefaultParams: false,
|
||||
});
|
||||
const tagParam = `${tag}=`;
|
||||
newFunc.params = [tagParam];
|
||||
this.queryModel.addFunction(newFunc);
|
||||
newFunc.added = true;
|
||||
|
||||
this.emptySegments();
|
||||
this.targetChanged();
|
||||
this.parseTarget();
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
// It's now dispatched in func_editor
|
||||
}
|
||||
|
||||
smartlyHandleNewAliasByNode(func: { def: { name: string }; params: number[]; added: boolean }) {
|
||||
if (func.def.name !== 'aliasByNode') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.segments.length; i++) {
|
||||
if (this.segments[i].value.indexOf('*') >= 0) {
|
||||
func.params[0] = i;
|
||||
func.added = false;
|
||||
this.targetChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
getAllTags() {
|
||||
return this.datasource.getTags().then((values: any[]) => {
|
||||
const altTags = map(values, 'text');
|
||||
altTags.splice(0, 0, this.removeTagValue);
|
||||
return mapToDropdownOptions(altTags);
|
||||
});
|
||||
// WIP: removed. It was not used.
|
||||
}
|
||||
|
||||
getTags(index: number, tagPrefix: any) {
|
||||
const tagExpressions = this.queryModel.renderTagExpressions(index);
|
||||
return this.datasource
|
||||
.getTagsAutoComplete(tagExpressions, tagPrefix)
|
||||
.then((values: any) => {
|
||||
const altTags = map(values, 'text');
|
||||
altTags.splice(0, 0, this.removeTagValue);
|
||||
return mapToDropdownOptions(altTags);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.handleTagsAutoCompleteError(err);
|
||||
});
|
||||
/**
|
||||
* Get list of tags for editing exiting tag with <gf-form-dropdown>
|
||||
*/
|
||||
async getTags(index: number, query: string): Promise<AngularDropdownOptions[]> {
|
||||
return await getTags(this.state, index, query);
|
||||
}
|
||||
|
||||
getTagsAsSegments(tagPrefix: string) {
|
||||
const tagExpressions = this.queryModel.renderTagExpressions();
|
||||
return this.datasource
|
||||
.getTagsAutoComplete(tagExpressions, tagPrefix)
|
||||
.then((values: any) => {
|
||||
return map(values, (val) => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: val.text,
|
||||
type: 'tag',
|
||||
expandable: false,
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.handleTagsAutoCompleteError(err);
|
||||
});
|
||||
/**
|
||||
* Get tag list when adding a new tag with <metric-segment>
|
||||
*/
|
||||
async getTagsAsSegments(query: string): Promise<GraphiteSegment[]> {
|
||||
return await getTagsAsSegments(this.state, query);
|
||||
}
|
||||
|
||||
getTagOperators() {
|
||||
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
|
||||
/**
|
||||
* Get list of available tag operators
|
||||
*/
|
||||
getTagOperators(): AngularDropdownOptions[] {
|
||||
return getTagOperators();
|
||||
}
|
||||
|
||||
getAllTagValues(tag: { key: any }) {
|
||||
const tagKey = tag.key;
|
||||
return this.datasource.getTagValues(tagKey).then((values: any[]) => {
|
||||
const altValues = map(values, 'text');
|
||||
return mapToDropdownOptions(altValues);
|
||||
});
|
||||
// WIP: removed. It was not used.
|
||||
}
|
||||
|
||||
getTagValues(tag: { key: any }, index: number, valuePrefix: any) {
|
||||
const tagExpressions = this.queryModel.renderTagExpressions(index);
|
||||
const tagKey = tag.key;
|
||||
return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then((values: any[]) => {
|
||||
const altValues = map(values, 'text');
|
||||
// Add template variables as additional values
|
||||
eachRight(this.templateSrv.getVariables(), (variable) => {
|
||||
altValues.push('${' + variable.name + ':regex}');
|
||||
});
|
||||
return mapToDropdownOptions(altValues);
|
||||
});
|
||||
/**
|
||||
* Get list of available tag values
|
||||
*/
|
||||
async getTagValues(tag: GraphiteTag, index: number, query: string): Promise<AngularDropdownOptions[]> {
|
||||
return await getTagValues(this.state, tag, index, query);
|
||||
}
|
||||
|
||||
tagChanged(tag: any, tagIndex: any) {
|
||||
this.queryModel.updateTag(tag, tagIndex);
|
||||
this.targetChanged();
|
||||
/**
|
||||
* Apply changes when a tag is changed
|
||||
*/
|
||||
async tagChanged(tag: GraphiteTag, index: number) {
|
||||
await this.dispatch(actions.tagChanged({ tag, index }));
|
||||
}
|
||||
|
||||
addNewTag(segment: { value: any }) {
|
||||
const newTagKey = segment.value;
|
||||
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
|
||||
this.queryModel.addTag(newTag);
|
||||
this.targetChanged();
|
||||
this.fixTagSegments();
|
||||
async addNewTag(segment: GraphiteSegment) {
|
||||
await this.dispatch(actions.addNewTag({ segment }));
|
||||
}
|
||||
|
||||
removeTag(index: any) {
|
||||
this.queryModel.removeTag(index);
|
||||
this.targetChanged();
|
||||
// WIP: removed. It was not used.
|
||||
// Tags are removed by selecting the segment called "-- remove tag --"
|
||||
}
|
||||
|
||||
fixTagSegments() {
|
||||
// Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
|
||||
this.addTagSegments = [this.uiSegmentSrv.newPlusButton()];
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
showDelimiter(index: number) {
|
||||
return index !== this.queryModel.tags.length - 1;
|
||||
// WIP: removed. It was not used because of broken syntax in the template. The logic has been moved directly to the template
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.paused = true;
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
unpause() {
|
||||
this.paused = false;
|
||||
this.panelCtrl.refresh();
|
||||
async unpause() {
|
||||
await this.dispatch(actions.unpause());
|
||||
}
|
||||
|
||||
getCollapsedText() {
|
||||
return this.target.target;
|
||||
// WIP: removed. It was not used.
|
||||
}
|
||||
|
||||
private handleTagsAutoCompleteError(error: Error): void {
|
||||
console.error(error);
|
||||
if (!this._tagsAutoCompleteErrorShown) {
|
||||
this._tagsAutoCompleteErrorShown = true;
|
||||
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
|
||||
}
|
||||
handleTagsAutoCompleteError(error: Error): void {
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
|
||||
private handleMetricsAutoCompleteError(error: Error): void {
|
||||
console.error(error);
|
||||
if (!this._metricAutoCompleteErrorShown) {
|
||||
this._metricAutoCompleteErrorShown = true;
|
||||
dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
|
||||
}
|
||||
handleMetricsAutoCompleteError(error: Error): void {
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
}
|
||||
}
|
||||
|
||||
function mapToDropdownOptions(results: any[]) {
|
||||
return map(results, (value) => {
|
||||
return { text: value, value: value };
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagPrefix(value: string): string {
|
||||
return value.replace(TAG_PREFIX, '');
|
||||
}
|
||||
// WIP: moved to state/providers (the same names)
|
||||
// function mapToDropdownOptions(results: any[]) {}
|
||||
// function removeTagPrefix(value: string): string {}
|
||||
|
||||
@@ -16,6 +16,15 @@ jest.mock('app/store/store', () => ({
|
||||
}));
|
||||
const mockDispatch = dispatch as jest.Mock;
|
||||
|
||||
async function changeTarget(ctx: any, target: string, refId?: string): Promise<void> {
|
||||
await ctx.ctrl.toggleEditorMode();
|
||||
ctx.ctrl.state.target.target = target;
|
||||
if (refId) {
|
||||
ctx.ctrl.state.target.refId = refId;
|
||||
}
|
||||
await ctx.ctrl.toggleEditorMode();
|
||||
}
|
||||
|
||||
describe('GraphiteQueryCtrl', () => {
|
||||
const ctx = {
|
||||
datasource: {
|
||||
@@ -36,21 +45,23 @@ describe('GraphiteQueryCtrl', () => {
|
||||
targets: [ctx.target],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
GraphiteQueryCtrl.prototype.target = ctx.target;
|
||||
GraphiteQueryCtrl.prototype.datasource = ctx.datasource;
|
||||
GraphiteQueryCtrl.prototype.panelCtrl = ctx.panelCtrl;
|
||||
|
||||
ctx.ctrl = new GraphiteQueryCtrl(
|
||||
{},
|
||||
{ $digest: jest.fn() },
|
||||
{} as any,
|
||||
//@ts-ignore
|
||||
new uiSegmentSrv({ trustAsHtml: (html) => html }, { highlightVariablesAsHtml: () => {} }),
|
||||
//@ts-ignore
|
||||
new TemplateSrvStub(),
|
||||
{}
|
||||
new TemplateSrvStub()
|
||||
);
|
||||
|
||||
// resolve async code called by the constructor
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
@@ -59,19 +70,19 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
it('should not delete last segment if no metrics are found', () => {
|
||||
expect(ctx.ctrl.segments[2].value).not.toBe('select metric');
|
||||
expect(ctx.ctrl.segments[2].value).toBe('*');
|
||||
expect(ctx.ctrl.state.segments[2].value).not.toBe('select metric');
|
||||
expect(ctx.ctrl.state.segments[2].value).toBe('*');
|
||||
});
|
||||
|
||||
it('should parse expression and build function model', () => {
|
||||
expect(ctx.ctrl.queryModel.functions.length).toBe(2);
|
||||
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when toggling edit mode to raw and back again', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.toggleEditorMode();
|
||||
ctx.ctrl.toggleEditorMode();
|
||||
beforeEach(async () => {
|
||||
await ctx.ctrl.toggleEditorMode();
|
||||
await ctx.ctrl.toggleEditorMode();
|
||||
});
|
||||
|
||||
it('should validate metric key exists', () => {
|
||||
@@ -80,20 +91,20 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
it('should delete last segment if no metrics are found', () => {
|
||||
expect(ctx.ctrl.segments[0].value).toBe('test');
|
||||
expect(ctx.ctrl.segments[1].value).toBe('prod');
|
||||
expect(ctx.ctrl.segments[2].value).toBe('select metric');
|
||||
expect(ctx.ctrl.state.segments[0].value).toBe('test');
|
||||
expect(ctx.ctrl.state.segments[1].value).toBe('prod');
|
||||
expect(ctx.ctrl.state.segments[2].value).toBe('select metric');
|
||||
});
|
||||
|
||||
it('should parse expression and build function model', () => {
|
||||
expect(ctx.ctrl.queryModel.functions.length).toBe(2);
|
||||
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when middle segment value of test.prod.* is changed', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const segment = { type: 'segment', value: 'test', expandable: true };
|
||||
ctx.ctrl.segmentValueChanged(segment, 1);
|
||||
await ctx.ctrl.segmentValueChanged(segment, 1);
|
||||
});
|
||||
|
||||
it('should validate metric key exists', () => {
|
||||
@@ -102,30 +113,29 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
it('should delete last segment if no metrics are found', () => {
|
||||
expect(ctx.ctrl.segments[0].value).toBe('test');
|
||||
expect(ctx.ctrl.segments[1].value).toBe('test');
|
||||
expect(ctx.ctrl.segments[2].value).toBe('select metric');
|
||||
expect(ctx.ctrl.state.segments[0].value).toBe('test');
|
||||
expect(ctx.ctrl.state.segments[1].value).toBe('test');
|
||||
expect(ctx.ctrl.state.segments[2].value).toBe('select metric');
|
||||
});
|
||||
|
||||
it('should parse expression and build function model', () => {
|
||||
expect(ctx.ctrl.queryModel.functions.length).toBe(2);
|
||||
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding function', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'test.prod.*.count';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, 'test.prod.*.count');
|
||||
await ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
|
||||
});
|
||||
|
||||
it('should add function with correct node number', () => {
|
||||
expect(ctx.ctrl.queryModel.functions[0].params[0]).toBe(2);
|
||||
expect(ctx.ctrl.state.queryModel.functions[0].params[0]).toBe(2);
|
||||
});
|
||||
|
||||
it('should update target', () => {
|
||||
expect(ctx.ctrl.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
|
||||
expect(ctx.ctrl.state.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
|
||||
});
|
||||
|
||||
it('should call refresh', () => {
|
||||
@@ -134,58 +144,52 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
describe('when adding function before any metric segment', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = '';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
|
||||
await changeTarget(ctx, '');
|
||||
await ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
|
||||
});
|
||||
|
||||
it('should add function and remove select metric link', () => {
|
||||
expect(ctx.ctrl.segments.length).toBe(0);
|
||||
expect(ctx.ctrl.state.segments.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initializing a target with single param func using variable', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'movingAverage(prod.count, $var)';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
ctx.ctrl.parseTarget();
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
await changeTarget(ctx, 'movingAverage(prod.count, $var)');
|
||||
});
|
||||
|
||||
it('should add 2 segments', () => {
|
||||
expect(ctx.ctrl.segments.length).toBe(2);
|
||||
expect(ctx.ctrl.state.segments.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should add function param', () => {
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
|
||||
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initializing target without metric expression and function with series-ref', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
ctx.ctrl.parseTarget();
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
await changeTarget(ctx, 'asPercent(metric.node.count, #A)');
|
||||
});
|
||||
|
||||
it('should add segments', () => {
|
||||
expect(ctx.ctrl.segments.length).toBe(3);
|
||||
expect(ctx.ctrl.state.segments.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should have correct func params', () => {
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
|
||||
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getting altSegments and metricFindQuery returns empty array', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'test.count';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.getAltSegments(1).then((results: any) => {
|
||||
ctx.altSegments = results;
|
||||
});
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
await changeTarget(ctx, 'test.count');
|
||||
ctx.altSegments = await ctx.ctrl.getAltSegments(1, '');
|
||||
});
|
||||
|
||||
it('should have no segments', () => {
|
||||
@@ -196,8 +200,8 @@ describe('GraphiteQueryCtrl', () => {
|
||||
describe('when autocomplete for metric names is not available', () => {
|
||||
silenceConsoleOutput();
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
|
||||
ctx.ctrl.datasource.metricFindQuery = jest.fn().mockReturnValue(
|
||||
ctx.ctrl.state.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
|
||||
ctx.ctrl.state.datasource.metricFindQuery = jest.fn().mockReturnValue(
|
||||
new Promise(() => {
|
||||
throw new Error();
|
||||
})
|
||||
@@ -222,25 +226,6 @@ describe('GraphiteQueryCtrl', () => {
|
||||
await ctx.ctrl.getAltSegments(0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('checkOtherSegments should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await ctx.ctrl.checkOtherSegments(1, false);
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
})
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('checkOtherSegments should display the error message only once', async () => {
|
||||
await ctx.ctrl.checkOtherSegments(1, false);
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
|
||||
await ctx.ctrl.checkOtherSegments(1, false);
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when autocomplete for tags is not available', () => {
|
||||
@@ -294,16 +279,17 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
describe('targetChanged', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.target.target = '';
|
||||
ctx.ctrl.targetChanged();
|
||||
beforeEach(async () => {
|
||||
const newQuery = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, newQuery);
|
||||
await ctx.ctrl.targetTextChanged({
|
||||
target: { value: newQuery },
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('should rebuild target after expression model', () => {
|
||||
expect(ctx.ctrl.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
|
||||
expect(ctx.ctrl.state.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
|
||||
});
|
||||
|
||||
it('should call panelCtrl.refresh', () => {
|
||||
@@ -312,75 +298,71 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
describe('when updating targets with nested query', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, 'scaleToSeconds(#A, 60)');
|
||||
});
|
||||
|
||||
it('should add function params', () => {
|
||||
expect(ctx.ctrl.queryModel.segments.length).toBe(1);
|
||||
expect(ctx.ctrl.queryModel.segments[0].value).toBe('#A');
|
||||
expect(ctx.ctrl.state.queryModel.segments.length).toBe(1);
|
||||
expect(ctx.ctrl.state.queryModel.segments[0].value).toBe('#A');
|
||||
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
|
||||
expect(ctx.ctrl.queryModel.functions[0].params[0]).toBe(60);
|
||||
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
|
||||
expect(ctx.ctrl.state.queryModel.functions[0].params[0]).toBe(60);
|
||||
});
|
||||
|
||||
it('target should remain the same', () => {
|
||||
expect(ctx.ctrl.target.target).toBe('scaleToSeconds(#A, 60)');
|
||||
expect(ctx.ctrl.state.target.target).toBe('scaleToSeconds(#A, 60)');
|
||||
});
|
||||
|
||||
it('targetFull should include nested queries', () => {
|
||||
ctx.ctrl.panelCtrl.panel.targets = [
|
||||
it('targetFull should include nested queries', async () => {
|
||||
ctx.ctrl.state.panelCtrl.panel.targets = [
|
||||
{
|
||||
target: 'nested.query.count',
|
||||
refId: 'A',
|
||||
},
|
||||
];
|
||||
|
||||
ctx.ctrl.updateModelTarget();
|
||||
await ctx.ctrl.targetTextChanged({ target: { value: 'nested.query.count' } } as any);
|
||||
|
||||
expect(ctx.ctrl.target.target).toBe('scaleToSeconds(#A, 60)');
|
||||
expect(ctx.ctrl.state.target.target).toBe('scaleToSeconds(#A, 60)');
|
||||
|
||||
expect(ctx.ctrl.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
|
||||
expect(ctx.ctrl.state.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating target used in other query', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'metrics.a.count';
|
||||
ctx.ctrl.target.refId = 'A';
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
await changeTarget(ctx, 'metrics.a.count', 'A');
|
||||
|
||||
ctx.ctrl.panelCtrl.panel.targets = [ctx.ctrl.target, { target: 'sumSeries(#A)', refId: 'B' }];
|
||||
ctx.ctrl.state.panelCtrl.panel.targets = [ctx.ctrl.target, { target: 'sumSeries(#A)', refId: 'B' }];
|
||||
|
||||
ctx.ctrl.updateModelTarget();
|
||||
await ctx.ctrl.targetTextChanged({ target: { value: 'metrics.a.count' } } as any);
|
||||
});
|
||||
|
||||
it('targetFull of other query should update', () => {
|
||||
expect(ctx.ctrl.panel.targets[1].targetFull).toBe('sumSeries(metrics.a.count)');
|
||||
expect(ctx.ctrl.state.panelCtrl.panel.targets[1].targetFull).toBe('sumSeries(metrics.a.count)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding seriesByTag function', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = '';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, '');
|
||||
await ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
|
||||
});
|
||||
|
||||
it('should update functions', () => {
|
||||
expect(ctx.ctrl.queryModel.getSeriesByTagFuncIndex()).toBe(0);
|
||||
expect(ctx.ctrl.state.queryModel.getSeriesByTagFuncIndex()).toBe(0);
|
||||
});
|
||||
|
||||
it('should update seriesByTagUsed flag', () => {
|
||||
expect(ctx.ctrl.queryModel.seriesByTagUsed).toBe(true);
|
||||
expect(ctx.ctrl.state.queryModel.seriesByTagUsed).toBe(true);
|
||||
});
|
||||
|
||||
it('should update target', () => {
|
||||
expect(ctx.ctrl.target.target).toBe('seriesByTag()');
|
||||
expect(ctx.ctrl.state.target.target).toBe('seriesByTag()');
|
||||
});
|
||||
|
||||
it('should call refresh', () => {
|
||||
@@ -389,10 +371,9 @@ describe('GraphiteQueryCtrl', () => {
|
||||
});
|
||||
|
||||
describe('when parsing seriesByTag function', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||
});
|
||||
|
||||
it('should add tags', () => {
|
||||
@@ -400,39 +381,37 @@ describe('GraphiteQueryCtrl', () => {
|
||||
{ key: 'tag1', operator: '=', value: 'value1' },
|
||||
{ key: 'tag2', operator: '!=~', value: 'value2' },
|
||||
];
|
||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
||||
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should add plus button', () => {
|
||||
expect(ctx.ctrl.addTagSegments.length).toBe(1);
|
||||
expect(ctx.ctrl.state.addTagSegments.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tag added', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = 'seriesByTag()';
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.addNewTag({ value: 'tag1' });
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, 'seriesByTag()');
|
||||
await ctx.ctrl.addNewTag({ value: 'tag1' });
|
||||
});
|
||||
|
||||
it('should update tags with default value', () => {
|
||||
const expected = [{ key: 'tag1', operator: '=', value: '' }];
|
||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
||||
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should update target', () => {
|
||||
const expected = "seriesByTag('tag1=')";
|
||||
expect(ctx.ctrl.target.target).toEqual(expected);
|
||||
expect(ctx.ctrl.state.target.target).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tag changed', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.tagChanged({ key: 'tag1', operator: '=', value: 'new_value' }, 0);
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||
await ctx.ctrl.tagChanged({ key: 'tag1', operator: '=', value: 'new_value' }, 0);
|
||||
});
|
||||
|
||||
it('should update tags', () => {
|
||||
@@ -440,31 +419,30 @@ describe('GraphiteQueryCtrl', () => {
|
||||
{ key: 'tag1', operator: '=', value: 'new_value' },
|
||||
{ key: 'tag2', operator: '!=~', value: 'value2' },
|
||||
];
|
||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
||||
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should update target', () => {
|
||||
const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
|
||||
expect(ctx.ctrl.target.target).toEqual(expected);
|
||||
expect(ctx.ctrl.state.target.target).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tag removed', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
|
||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
ctx.ctrl.parseTarget();
|
||||
ctx.ctrl.tagChanged({ key: ctx.ctrl.removeTagValue });
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||
await ctx.ctrl.tagChanged({ key: ctx.ctrl.state.removeTagValue });
|
||||
});
|
||||
|
||||
it('should update tags', () => {
|
||||
const expected = [{ key: 'tag2', operator: '!=~', value: 'value2' }];
|
||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
||||
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should update target', () => {
|
||||
const expected = "seriesByTag('tag2!=~value2')";
|
||||
expect(ctx.ctrl.target.target).toEqual(expected);
|
||||
expect(ctx.ctrl.state.target.target).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { GraphiteQueryEditorAngularDependencies, GraphiteSegment, GraphiteTag } from '../types';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { FuncInstance } from '../gfunc';
|
||||
|
||||
/**
|
||||
* List of possible actions changing the state of QueryEditor
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is used only during the transition to react. It will be removed after migrating all components.
|
||||
*/
|
||||
const init = createAction<GraphiteQueryEditorAngularDependencies>('init');
|
||||
|
||||
// Metrics & Tags
|
||||
const segmentValueChanged = createAction<{ segment: GraphiteSegment; index: number }>('segment-value-changed');
|
||||
|
||||
// Tags
|
||||
const addNewTag = createAction<{ segment: GraphiteSegment }>('add-new-tag');
|
||||
const tagChanged = createAction<{ tag: GraphiteTag; index: number }>('tag-changed');
|
||||
const unpause = createAction('unpause');
|
||||
|
||||
// Functions
|
||||
const addFunction = createAction<{ name: string }>('add-function');
|
||||
const removeFunction = createAction<{ func: FuncInstance }>('remove-function');
|
||||
const moveFunction = createAction<{ func: FuncInstance; offset: number }>('move-function');
|
||||
// TODO: at the moment parameters are modified directly, new value of the param will be passed in the action
|
||||
const updateFunctionParam = createAction<{ func: FuncInstance }>('update-function-param');
|
||||
|
||||
// Text editor
|
||||
const updateQuery = createAction<{ query: string }>('update-query');
|
||||
const toggleEditorMode = createAction('toggle-editor');
|
||||
|
||||
export const actions = {
|
||||
init,
|
||||
segmentValueChanged,
|
||||
tagChanged,
|
||||
addNewTag,
|
||||
unpause,
|
||||
addFunction,
|
||||
removeFunction,
|
||||
moveFunction,
|
||||
updateFunctionParam,
|
||||
updateQuery,
|
||||
toggleEditorMode,
|
||||
};
|
||||
@@ -0,0 +1,213 @@
|
||||
import { GraphiteQueryEditorState } from './store';
|
||||
import { each, map } from 'lodash';
|
||||
import { dispatch } from '../../../../store/store';
|
||||
import { notifyApp } from '../../../../core/reducers/appNotification';
|
||||
import { createErrorNotification } from '../../../../core/copy/appNotification';
|
||||
|
||||
/**
|
||||
* 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 = ['=', '!=', '=~', '!=~'];
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
state.segments = map(state.queryModel.segments, (segment) => {
|
||||
return state.uiSegmentSrv.newSegment(segment);
|
||||
});
|
||||
|
||||
const checkOtherSegmentsIndex = state.queryModel.checkOtherSegmentsIndex || 0;
|
||||
|
||||
await checkOtherSegments(state, checkOtherSegmentsIndex, modifyLastSegment);
|
||||
|
||||
if (state.queryModel.seriesByTagUsed) {
|
||||
fixTagSegments(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "select metric" segment at the end
|
||||
*/
|
||||
export function addSelectMetricSegment(state: GraphiteQueryEditorState): void {
|
||||
state.queryModel.addSelectMetricSegment();
|
||||
state.segments.push(state.uiSegmentSrv.newSelectMetric());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes segment being in focus. After changing the value, next segment gets focus.
|
||||
*
|
||||
* Note: It's a bit hidden feature. After selecting one metric, and pressing down arrow the dropdown can be expanded.
|
||||
* But there's nothing indicating what's in focus and how to expand the dropdown.
|
||||
*/
|
||||
export function setSegmentFocus(state: GraphiteQueryEditorState, segmentIndex: number): void {
|
||||
each(state.segments, (segment, index) => {
|
||||
segment.focus = segmentIndex === index;
|
||||
});
|
||||
}
|
||||
|
||||
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: { def: { name: string }; params: number[]; added: boolean }
|
||||
): 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "+" button for adding tags once at least one tag is selected
|
||||
*/
|
||||
export function fixTagSegments(state: GraphiteQueryEditorState): void {
|
||||
// Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
|
||||
state.addTagSegments = [state.uiSegmentSrv.newPlusButton()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
state.queryModel.updateModelTarget(state.panelCtrl.panel.targets);
|
||||
|
||||
if (state.queryModel.target.target !== oldTarget && !state.paused) {
|
||||
state.panelCtrl.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When metrics autocomplete fails - the error is shown, but only once per page view
|
||||
*/
|
||||
export function handleMetricsAutoCompleteError(
|
||||
state: GraphiteQueryEditorState,
|
||||
error: Error
|
||||
): GraphiteQueryEditorState {
|
||||
console.error(error);
|
||||
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 {
|
||||
console.error(error);
|
||||
if (!state.tagsAutoCompleteErrorShown) {
|
||||
state.tagsAutoCompleteErrorShown = true;
|
||||
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { GraphiteQueryEditorState } from './store';
|
||||
import { eachRight, map, remove } from 'lodash';
|
||||
import {
|
||||
TAG_PREFIX,
|
||||
GRAPHITE_TAG_OPERATORS,
|
||||
handleMetricsAutoCompleteError,
|
||||
handleTagsAutoCompleteError,
|
||||
} from './helpers';
|
||||
import { AngularDropdownOptions, GraphiteSegment, GraphiteTag } from '../types';
|
||||
|
||||
/**
|
||||
* Providers are hooks for views to provide temporal data for autocomplete. They don't modify the state.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return list of available options for a segment with given index
|
||||
*
|
||||
* It may be:
|
||||
* - mixed list of metrics and tags (only when nothing was selected)
|
||||
* - list of metric names (if a metric name was selected for this segment)
|
||||
*/
|
||||
export async function getAltSegments(
|
||||
state: GraphiteQueryEditorState,
|
||||
index: number,
|
||||
prefix: string
|
||||
): Promise<GraphiteSegment[]> {
|
||||
let query = prefix.length > 0 ? '*' + prefix + '*' : '*';
|
||||
if (index > 0) {
|
||||
query = state.queryModel.getSegmentPathUpTo(index) + '.' + query;
|
||||
}
|
||||
const options = {
|
||||
range: state.panelCtrl.range,
|
||||
requestId: 'get-alt-segments',
|
||||
};
|
||||
|
||||
try {
|
||||
const segments = await state.datasource.metricFindQuery(query, options);
|
||||
const altSegments = map(segments, (segment) => {
|
||||
return state.uiSegmentSrv.newSegment({
|
||||
value: segment.text,
|
||||
expandable: segment.expandable,
|
||||
});
|
||||
});
|
||||
|
||||
if (index > 0 && altSegments.length === 0) {
|
||||
return altSegments;
|
||||
}
|
||||
|
||||
// add query references
|
||||
if (index === 0) {
|
||||
eachRight(state.panelCtrl.panel.targets, (target) => {
|
||||
if (target.refId === state.queryModel.target.refId) {
|
||||
return;
|
||||
}
|
||||
|
||||
altSegments.unshift(
|
||||
state.uiSegmentSrv.newSegment({
|
||||
type: 'series-ref',
|
||||
value: '#' + target.refId,
|
||||
expandable: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// add template variables
|
||||
eachRight(state.templateSrv.getVariables(), (variable) => {
|
||||
altSegments.unshift(
|
||||
state.uiSegmentSrv.newSegment({
|
||||
type: 'template',
|
||||
value: '$' + variable.name,
|
||||
expandable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// add wildcard option
|
||||
altSegments.unshift(state.uiSegmentSrv.newSegment('*'));
|
||||
|
||||
if (state.supportsTags && index === 0) {
|
||||
removeTaggedEntry(altSegments);
|
||||
return await addAltTagSegments(state, prefix, altSegments);
|
||||
} else {
|
||||
return altSegments;
|
||||
}
|
||||
} catch (err) {
|
||||
handleMetricsAutoCompleteError(state, err);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getTagOperators(): AngularDropdownOptions[] {
|
||||
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tags as dropdown options
|
||||
*/
|
||||
export async function getTags(
|
||||
state: GraphiteQueryEditorState,
|
||||
index: number,
|
||||
tagPrefix: string
|
||||
): Promise<AngularDropdownOptions[]> {
|
||||
try {
|
||||
const tagExpressions = state.queryModel.renderTagExpressions(index);
|
||||
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
|
||||
|
||||
const altTags = map(values, 'text');
|
||||
altTags.splice(0, 0, state.removeTagValue);
|
||||
return mapToDropdownOptions(altTags);
|
||||
} catch (err) {
|
||||
handleTagsAutoCompleteError(state, err);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* List of tags when a tag is added. getTags is used for editing.
|
||||
* When adding - segment is used. When editing - dropdown is used.
|
||||
*/
|
||||
export async function getTagsAsSegments(
|
||||
state: GraphiteQueryEditorState,
|
||||
tagPrefix: string
|
||||
): Promise<GraphiteSegment[]> {
|
||||
let tagsAsSegments: GraphiteSegment[] = [];
|
||||
try {
|
||||
const tagExpressions = state.queryModel.renderTagExpressions();
|
||||
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
|
||||
tagsAsSegments = map(values, (val) => {
|
||||
return state.uiSegmentSrv.newSegment({
|
||||
value: val.text,
|
||||
type: 'tag',
|
||||
expandable: false,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
tagsAsSegments = [];
|
||||
handleTagsAutoCompleteError(state, err);
|
||||
}
|
||||
|
||||
return tagsAsSegments;
|
||||
}
|
||||
|
||||
export async function getTagValues(
|
||||
state: GraphiteQueryEditorState,
|
||||
tag: GraphiteTag,
|
||||
index: number,
|
||||
valuePrefix: string
|
||||
): Promise<AngularDropdownOptions[]> {
|
||||
const tagExpressions = state.queryModel.renderTagExpressions(index);
|
||||
const tagKey = tag.key;
|
||||
const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {});
|
||||
const altValues = map(values, 'text');
|
||||
// Add template variables as additional values
|
||||
eachRight(state.templateSrv.getVariables(), (variable) => {
|
||||
altValues.push('${' + variable.name + ':regex}');
|
||||
});
|
||||
|
||||
return mapToDropdownOptions(altValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add segments with tags prefixed with "tag: " to include them in the same list as metrics
|
||||
*/
|
||||
async function addAltTagSegments(
|
||||
state: GraphiteQueryEditorState,
|
||||
prefix: string,
|
||||
altSegments: GraphiteSegment[]
|
||||
): Promise<GraphiteSegment[]> {
|
||||
let tagSegments = await getTagsAsSegments(state, prefix);
|
||||
|
||||
tagSegments = map(tagSegments, (segment) => {
|
||||
segment.value = TAG_PREFIX + segment.value;
|
||||
return segment;
|
||||
});
|
||||
|
||||
return altSegments.concat(...tagSegments);
|
||||
}
|
||||
|
||||
function removeTaggedEntry(altSegments: GraphiteSegment[]) {
|
||||
remove(altSegments, (s) => s.value === '_tagged');
|
||||
}
|
||||
|
||||
function mapToDropdownOptions(results: string[]) {
|
||||
return map(results, (value) => {
|
||||
return { text: value, value: value };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import GraphiteQuery from '../graphite_query';
|
||||
import { GraphiteActionDispatcher, GraphiteSegment, GraphiteTagOperator } from '../types';
|
||||
import { GraphiteDatasource } from '../datasource';
|
||||
import { TemplateSrv } from '../../../../features/templating/template_srv';
|
||||
import { actions } from './actions';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import {
|
||||
addSeriesByTagFunc,
|
||||
buildSegments,
|
||||
checkOtherSegments,
|
||||
emptySegments,
|
||||
fixTagSegments,
|
||||
handleTargetChanged,
|
||||
parseTarget,
|
||||
pause,
|
||||
removeTagPrefix,
|
||||
setSegmentFocus,
|
||||
smartlyHandleNewAliasByNode,
|
||||
spliceSegments,
|
||||
} from './helpers';
|
||||
import { Action } from 'redux';
|
||||
|
||||
export type GraphiteQueryEditorState = {
|
||||
/**
|
||||
* Extra segment with plus button when tags are rendered
|
||||
*/
|
||||
addTagSegments: GraphiteSegment[];
|
||||
|
||||
supportsTags: boolean;
|
||||
paused: boolean;
|
||||
removeTagValue: string;
|
||||
|
||||
datasource: GraphiteDatasource;
|
||||
|
||||
uiSegmentSrv: any;
|
||||
templateSrv: TemplateSrv;
|
||||
panelCtrl: any;
|
||||
|
||||
target: { target: string; textEditor: boolean };
|
||||
|
||||
segments: GraphiteSegment[];
|
||||
queryModel: GraphiteQuery;
|
||||
|
||||
error: Error | null;
|
||||
|
||||
tagsAutoCompleteErrorShown: boolean;
|
||||
metricAutoCompleteErrorShown: boolean;
|
||||
};
|
||||
|
||||
const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise<GraphiteQueryEditorState> => {
|
||||
state = { ...state };
|
||||
|
||||
if (actions.init.match(action)) {
|
||||
const deps = action.payload;
|
||||
deps.target.target = deps.target.target || '';
|
||||
|
||||
await deps.datasource.waitForFuncDefsLoaded();
|
||||
|
||||
state = {
|
||||
...state,
|
||||
...deps,
|
||||
queryModel: new GraphiteQuery(deps.datasource, deps.target, getTemplateSrv()),
|
||||
supportsTags: deps.datasource.supportsTags,
|
||||
paused: false,
|
||||
removeTagValue: '-- remove tag --',
|
||||
};
|
||||
|
||||
await buildSegments(state, false);
|
||||
}
|
||||
if (actions.segmentValueChanged.match(action)) {
|
||||
const { segment, index: segmentIndex } = action.payload;
|
||||
|
||||
state.error = null;
|
||||
state.queryModel.updateSegmentValue(segment, segmentIndex);
|
||||
|
||||
if (state.queryModel.functions.length > 0 && state.queryModel.functions[0].def.fake) {
|
||||
state.queryModel.functions = [];
|
||||
}
|
||||
|
||||
if (segment.type === 'tag') {
|
||||
const tag = removeTagPrefix(segment.value);
|
||||
pause(state);
|
||||
await addSeriesByTagFunc(state, tag);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (segment.expandable) {
|
||||
await checkOtherSegments(state, segmentIndex + 1);
|
||||
setSegmentFocus(state, segmentIndex + 1);
|
||||
handleTargetChanged(state);
|
||||
} else {
|
||||
spliceSegments(state, segmentIndex + 1);
|
||||
}
|
||||
|
||||
setSegmentFocus(state, segmentIndex + 1);
|
||||
handleTargetChanged(state);
|
||||
}
|
||||
if (actions.tagChanged.match(action)) {
|
||||
const { tag, index: tagIndex } = action.payload;
|
||||
state.queryModel.updateTag(tag, tagIndex);
|
||||
handleTargetChanged(state);
|
||||
}
|
||||
if (actions.addNewTag.match(action)) {
|
||||
const segment = action.payload.segment;
|
||||
const newTagKey = segment.value;
|
||||
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
|
||||
state.queryModel.addTag(newTag);
|
||||
handleTargetChanged(state);
|
||||
fixTagSegments(state);
|
||||
}
|
||||
if (actions.unpause.match(action)) {
|
||||
state.paused = false;
|
||||
state.panelCtrl.refresh();
|
||||
}
|
||||
if (actions.addFunction.match(action)) {
|
||||
const newFunc = state.datasource.createFuncInstance(action.payload.name, {
|
||||
withDefaultParams: true,
|
||||
});
|
||||
newFunc.added = true;
|
||||
state.queryModel.addFunction(newFunc);
|
||||
smartlyHandleNewAliasByNode(state, newFunc);
|
||||
|
||||
if (state.segments.length === 1 && state.segments[0].fake) {
|
||||
emptySegments(state);
|
||||
}
|
||||
|
||||
if (!newFunc.params.length && newFunc.added) {
|
||||
handleTargetChanged(state);
|
||||
}
|
||||
|
||||
if (newFunc.def.name === 'seriesByTag') {
|
||||
await parseTarget(state);
|
||||
}
|
||||
}
|
||||
if (actions.removeFunction.match(action)) {
|
||||
state.queryModel.removeFunction(action.payload.func);
|
||||
handleTargetChanged(state);
|
||||
}
|
||||
if (actions.moveFunction.match(action)) {
|
||||
const { func, offset } = action.payload;
|
||||
state.queryModel.moveFunction(func, offset);
|
||||
handleTargetChanged(state);
|
||||
}
|
||||
if (actions.updateFunctionParam.match(action)) {
|
||||
handleTargetChanged(state);
|
||||
}
|
||||
if (actions.updateQuery.match(action)) {
|
||||
state.target.target = action.payload.query;
|
||||
handleTargetChanged(state);
|
||||
// handleTargetChanged() builds target from segments/tags/functions only,
|
||||
// it doesn't handle refresh when target is change explicitly
|
||||
state.panelCtrl.refresh();
|
||||
}
|
||||
if (actions.toggleEditorMode.match(action)) {
|
||||
state.target.textEditor = !state.target.textEditor;
|
||||
await parseTarget(state);
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
};
|
||||
|
||||
export const createStore = (
|
||||
onChange: (state: GraphiteQueryEditorState) => void
|
||||
): [GraphiteActionDispatcher, GraphiteQueryEditorState] => {
|
||||
let state = {} as GraphiteQueryEditorState;
|
||||
|
||||
const dispatch = async (action: Action) => {
|
||||
state = await reducer(action, state);
|
||||
onChange(state);
|
||||
};
|
||||
|
||||
return [dispatch, state];
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||
import { GraphiteDatasource } from './datasource';
|
||||
import { TemplateSrv } from '../../../features/templating/template_srv';
|
||||
|
||||
export interface GraphiteQuery extends DataQuery {
|
||||
target?: string;
|
||||
@@ -53,3 +55,34 @@ export type GraphiteMetricLokiMatcher = {
|
||||
value: string;
|
||||
labelName?: string;
|
||||
};
|
||||
|
||||
export type GraphiteSegment = {
|
||||
value: string;
|
||||
type?: 'tag' | 'metric' | 'series-ref';
|
||||
expandable?: boolean;
|
||||
focus?: boolean;
|
||||
fake?: boolean;
|
||||
};
|
||||
|
||||
export type GraphiteTagOperator = '=' | '!=' | '=~' | '!=~';
|
||||
|
||||
export type GraphiteTag = {
|
||||
key: string;
|
||||
operator: GraphiteTagOperator;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type GraphiteActionDispatcher = (action: any) => Promise<void>;
|
||||
|
||||
export type GraphiteQueryEditorAngularDependencies = {
|
||||
panelCtrl: any;
|
||||
target: any;
|
||||
datasource: GraphiteDatasource;
|
||||
uiSegmentSrv: any;
|
||||
templateSrv: TemplateSrv;
|
||||
};
|
||||
|
||||
export type AngularDropdownOptions = {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user